From d3b50950ba8d0a59c8d77a0ca55190a38a10505c Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 12 Feb 2021 13:46:44 -0800 Subject: [PATCH 001/249] Move plugin tools code (#3544) --- script/tool/README.md | 8 + script/tool/lib/src/analyze_command.dart | 94 ++++ .../tool/lib/src/build_examples_command.dart | 188 +++++++ script/tool/lib/src/common.dart | 466 ++++++++++++++++++ .../src/create_all_plugins_app_command.dart | 200 ++++++++ .../tool/lib/src/drive_examples_command.dart | 210 ++++++++ .../lib/src/firebase_test_lab_command.dart | 264 ++++++++++ script/tool/lib/src/format_command.dart | 147 ++++++ script/tool/lib/src/java_test_command.dart | 89 ++++ .../tool/lib/src/lint_podspecs_command.dart | 146 ++++++ script/tool/lib/src/list_command.dart | 60 +++ script/tool/lib/src/main.dart | 63 +++ .../tool/lib/src/publish_plugin_command.dart | 223 +++++++++ script/tool/lib/src/test_command.dart | 101 ++++ .../tool/lib/src/version_check_command.dart | 220 +++++++++ script/tool/lib/src/xctest_command.dart | 216 ++++++++ script/tool/pubspec.yaml | 25 + 17 files changed, 2720 insertions(+) create mode 100644 script/tool/README.md create mode 100644 script/tool/lib/src/analyze_command.dart create mode 100644 script/tool/lib/src/build_examples_command.dart create mode 100644 script/tool/lib/src/common.dart create mode 100644 script/tool/lib/src/create_all_plugins_app_command.dart create mode 100644 script/tool/lib/src/drive_examples_command.dart create mode 100644 script/tool/lib/src/firebase_test_lab_command.dart create mode 100644 script/tool/lib/src/format_command.dart create mode 100644 script/tool/lib/src/java_test_command.dart create mode 100644 script/tool/lib/src/lint_podspecs_command.dart create mode 100644 script/tool/lib/src/list_command.dart create mode 100644 script/tool/lib/src/main.dart create mode 100644 script/tool/lib/src/publish_plugin_command.dart create mode 100644 script/tool/lib/src/test_command.dart create mode 100644 script/tool/lib/src/version_check_command.dart create mode 100644 script/tool/lib/src/xctest_command.dart create mode 100644 script/tool/pubspec.yaml diff --git a/script/tool/README.md b/script/tool/README.md new file mode 100644 index 00000000000..162ca0d98a7 --- /dev/null +++ b/script/tool/README.md @@ -0,0 +1,8 @@ +# Flutter Plugin Tools + +To run the tool: + +```sh +dart pub get +dart run lib/src/main.dart +``` diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart new file mode 100644 index 00000000000..8cd57fa0b33 --- /dev/null +++ b/script/tool/lib/src/analyze_command.dart @@ -0,0 +1,94 @@ +// Copyright 2017 The Chromium 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class AnalyzeCommand extends PluginCommand { + AnalyzeCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption(_customAnalysisFlag, + help: + 'Directories (comma seperated) that are allowed to have their own analysis options.', + defaultsTo: []); + } + + static const String _customAnalysisFlag = 'custom-analysis'; + + @override + final String name = 'analyze'; + + @override + final String description = 'Analyzes all packages using package:tuneup.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + print('Verifying analysis settings...'); + final List files = packagesDir.listSync(recursive: true); + for (final FileSystemEntity file in files) { + if (file.basename != 'analysis_options.yaml' && + file.basename != '.analysis_options') { + continue; + } + + final bool whitelisted = argResults[_customAnalysisFlag].any( + (String directory) => + p.isWithin(p.join(packagesDir.path, directory), file.path)); + if (whitelisted) { + continue; + } + + print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); + print( + 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); + throw ToolExit(1); + } + + print('Activating tuneup package...'); + await processRunner.runAndStream( + 'pub', ['global', 'activate', 'tuneup'], + workingDir: packagesDir, exitOnError: true); + + await for (Directory package in getPackages()) { + if (isFlutterPackage(package, fileSystem)) { + await processRunner.runAndStream('flutter', ['packages', 'get'], + workingDir: package, exitOnError: true); + } else { + await processRunner.runAndStream('pub', ['get'], + workingDir: package, exitOnError: true); + } + } + + final List failingPackages = []; + await for (Directory package in getPlugins()) { + final int exitCode = await processRunner.runAndStream( + 'pub', ['global', 'run', 'tuneup', 'check'], + workingDir: package); + if (exitCode != 0) { + failingPackages.add(p.basename(package.path)); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('The following packages have analyzer errors (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('No analyzer errors found!'); + } +} diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart new file mode 100644 index 00000000000..53da9086aba --- /dev/null +++ b/script/tool/lib/src/build_examples_command.dart @@ -0,0 +1,188 @@ +// Copyright 2017 The Chromium 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:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +class BuildExamplesCommand extends PluginCommand { + BuildExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, defaultsTo: false); + argParser.addFlag(kMacos, defaultsTo: false); + argParser.addFlag(kWindows, defaultsTo: false); + argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); + argParser.addFlag(kApk); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'build-examples'; + + @override + final String description = + 'Builds all example apps (IPA for iOS and APK for Android).\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (!argResults[kIpa] && + !argResults[kApk] && + !argResults[kLinux] && + !argResults[kMacos] && + !argResults[kWindows]) { + print( + 'None of --linux, --macos, --windows, --apk nor --ipa were specified, ' + 'so not building anything.'); + return; + } + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + final String enableExperiment = argResults[kEnableExperiment]; + + checkSharding(); + final List failingPackages = []; + await for (Directory plugin in getPlugins()) { + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + + if (argResults[kLinux]) { + print('\nBUILDING Linux for $packageName'); + if (isLinuxPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kLinux, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (linux)'); + } + } else { + print('Linux is not supported by this plugin'); + } + } + + if (argResults[kMacos]) { + print('\nBUILDING macOS for $packageName'); + if (isMacOsPlugin(plugin, fileSystem)) { + // TODO(https://github.com/flutter/flutter/issues/46236): + // Builing macos without running flutter pub get first results + // in an error. + int exitCode = await processRunner.runAndStream( + flutterCommand, ['pub', 'get'], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } else { + exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kMacos, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } + } + } else { + print('macOS is not supported by this plugin'); + } + } + + if (argResults[kWindows]) { + print('\nBUILDING Windows for $packageName'); + if (isWindowsPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kWindows, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (windows)'); + } + } else { + print('Windows is not supported by this plugin'); + } + } + + if (argResults[kIpa]) { + print('\nBUILDING IPA for $packageName'); + if (isIosPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (ipa)'); + } + } else { + print('iOS is not supported by this plugin'); + } + } + + if (argResults[kApk]) { + print('\nBUILDING APK for $packageName'); + if (isAndroidPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (apk)'); + } + } else { + print('Android is not supported by this plugin'); + } + } + } + } + print('\n\n'); + + if (failingPackages.isNotEmpty) { + print('The following build are failing (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + + print('All builds successful!'); + } +} diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart new file mode 100644 index 00000000000..78b91ee8a75 --- /dev/null +++ b/script/tool/lib/src/common.dart @@ -0,0 +1,466 @@ +// Copyright 2017 The Chromium 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:io' as io; +import 'dart:math'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +typedef void Print(Object object); + +/// Key for windows platform. +const String kWindows = 'windows'; + +/// Key for macos platform. +const String kMacos = 'macos'; + +/// Key for linux platform. +const String kLinux = 'linux'; + +/// Key for IPA (iOS) platform. +const String kIos = 'ios'; + +/// Key for APK (Android) platform. +const String kAndroid = 'android'; + +/// Key for Web platform. +const String kWeb = 'web'; + +/// Key for IPA. +const String kIpa = 'ipa'; + +/// Key for APK. +const String kApk = 'apk'; + +/// Key for enable experiment. +const String kEnableExperiment = 'enable-experiment'; + +/// Returns whether the given directory contains a Flutter package. +bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap dependencies = pubspecYaml['dependencies']; + if (dependencies == null) { + return false; + } + return dependencies.containsKey('flutter'); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// 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]: +bool pluginSupportsPlatform( + String platform, FileSystemEntity entity, FileSystem fileSystem) { + assert(platform == kIos || + platform == kAndroid || + platform == kWeb || + platform == kMacos || + platform == kWindows || + platform == kLinux); + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap flutterSection = pubspecYaml['flutter']; + if (flutterSection == null) { + return false; + } + final YamlMap pluginSection = flutterSection['plugin']; + if (pluginSection == null) { + return false; + } + final YamlMap platforms = pluginSection['platforms']; + if (platforms == null) { + // Legacy plugin specs are assumed to support iOS and Android. + if (!pluginSection.containsKey('platforms')) { + return platform == kIos || platform == kAndroid; + } + return false; + } + return platforms.containsKey(platform); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter Android plugin. +bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kAndroid, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter iOS plugin. +bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kIos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter web plugin. +bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWeb, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter Windows plugin. +bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWindows, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter macOS plugin. +bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kMacos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter linux plugin. +bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kLinux, entity, fileSystem); +} + +/// Error thrown when a command needs to exit with a non-zero exit code. +class ToolExit extends Error { + ToolExit(this.exitCode); + + final int exitCode; +} + +abstract class PluginCommand extends Command { + PluginCommand( + this.packagesDir, + this.fileSystem, { + this.processRunner = const ProcessRunner(), + }) { + 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: [], + ); + } + + static const String _pluginsArg = 'plugins'; + static const String _shardIndexArg = 'shardIndex'; + static const String _shardCountArg = 'shardCount'; + static const String _excludeArg = 'exclude'; + + /// The directory containing the plugin packages. + final Directory packagesDir; + + /// The file system. + /// + /// This can be overridden for testing. + final FileSystem fileSystem; + + /// The process runner. + /// + /// This can be overridden for testing. + final ProcessRunner processRunner; + + int _shardIndex; + int _shardCount; + + int get shardIndex { + if (_shardIndex == null) { + checkSharding(); + } + return _shardIndex; + } + + int get shardCount { + if (_shardCount == null) { + checkSharding(); + } + return _shardCount; + } + + void checkSharding() { + final int shardIndex = int.tryParse(argResults[_shardIndexArg]); + final int shardCount = int.tryParse(argResults[_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 (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 one of two 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. + Stream _getAllPlugins() async* { + final Set plugins = Set.from(argResults[_pluginsArg]); + final Set excludedPlugins = + Set.from(argResults[_excludeArg]); + + await for (FileSystemEntity entity + in packagesDir.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; + } + } else if (entity is Directory) { + // Look for Dart packages under this top-level directory. + await for (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: packagesDir.path); + final String basenamePath = p.basename(entity.path); + if (!excludedPlugins.contains(basenamePath) && + !excludedPlugins.contains(relativePath) && + (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath))) { + yield subdir; + } + } + } + } + } + } + + /// 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 (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 && + fileSystem.file(p.join(entity.path, '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 = + fileSystem.directory(p.join(plugin.path, 'example')); + if (!exampleFolder.existsSync()) { + return []; + } + if (isFlutterPackage(exampleFolder, fileSystem)) { + 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, fileSystem)) + .cast(); + } +} + +/// A class used to run processes. +/// +/// We use this instead of directly running the process so it can be overridden +/// in tests. +class ProcessRunner { + 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. + /// + /// Returns the [io.ProcessResult] of the [executable]. + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + return io.Process.run(executable, args, + workingDirectory: workingDir?.path, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding); + } + + /// 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; + } + + /// Run the [executable] with [args], throwing an error on non-zero exit code. + /// + /// Unlike [runAndStream], this does not stream the process output to stdout. + /// It also unconditionally throws an error on a non-zero exit code. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the [io.ProcessResult] of running the [executable]. + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + final io.ProcessResult result = await io.Process.run(executable, args, + workingDirectory: workingDir?.path); + if (result.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error Stderr:\n${result.stdout}'); + throw ToolExit(result.exitCode); + } + return result; + } + + 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/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart new file mode 100644 index 00000000000..0f1431c5aee --- /dev/null +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -0,0 +1,200 @@ +// Copyright 2019 The Chromium 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:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +// TODO(cyanglaz): Add tests for this command. +// https://github.com/flutter/flutter/issues/61049 +class CreateAllPluginsAppCommand extends PluginCommand { + CreateAllPluginsAppCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem); + + @override + String get description => + 'Generate Flutter app that includes all plugins in packages.'; + + @override + String get name => 'all-plugins-app'; + + @override + Future run() async { + final int exitCode = await _createPlugin(); + if (exitCode != 0) { + throw ToolExit(exitCode); + } + + await Future.wait(>[ + _genPubspecWithAllPlugins(), + _updateAppGradle(), + _updateManifest(), + ]); + } + + Future _createPlugin() async { + final io.ProcessResult result = io.Process.runSync( + 'flutter', + [ + 'create', + '--template=app', + '--project-name=all_plugins', + '--android-language=java', + './all_plugins', + ], + ); + + print(result.stdout); + print(result.stderr); + return result.exitCode; + } + + Future _updateAppGradle() async { + final File gradleFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'build.gradle', + )); + if (!gradleFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newGradle = StringBuffer(); + for (String line in gradleFile.readAsLinesSync()) { + newGradle.writeln(line); + if (line.contains('defaultConfig {')) { + newGradle.writeln(' multiDexEnabled true'); + } else if (line.contains('dependencies {')) { + newGradle.writeln( + ' implementation \'com.google.guava:guava:27.0.1-android\'\n', + ); + // Tests for https://github.com/flutter/flutter/issues/43383 + newGradle.writeln( + " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", + ); + } + } + gradleFile.writeAsStringSync(newGradle.toString()); + } + + Future _updateManifest() async { + final File manifestFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'src', + 'main', + 'AndroidManifest.xml', + )); + if (!manifestFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newManifest = StringBuffer(); + for (String line in manifestFile.readAsLinesSync()) { + if (line.contains('package="com.example.all_plugins"')) { + newManifest + ..writeln('package="com.example.all_plugins"') + ..writeln('xmlns:tools="http://schemas.android.com/tools">') + ..writeln() + ..writeln( + '', + ); + } else { + newManifest.writeln(line); + } + } + manifestFile.writeAsStringSync(newManifest.toString()); + } + + Future _genPubspecWithAllPlugins() async { + final Map pluginDeps = + await _getValidPathDependencies(); + final Pubspec pubspec = Pubspec( + 'all_plugins', + description: 'Flutter app containing all 1st party plugins.', + version: Version.parse('1.0.0+1'), + environment: { + 'sdk': VersionConstraint.compatibleWith( + Version.parse('2.0.0'), + ), + }, + dependencies: { + 'flutter': SdkDependency('flutter'), + }..addAll(pluginDeps), + devDependencies: { + 'flutter_test': SdkDependency('flutter'), + }, + dependencyOverrides: pluginDeps, + ); + final File pubspecFile = + fileSystem.file(p.join('all_plugins', 'pubspec.yaml')); + pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + } + + Future> _getValidPathDependencies() async { + final Map pathDependencies = + {}; + + await for (Directory package in getPlugins()) { + final String pluginName = package.path.split('/').last; + final File pubspecFile = + fileSystem.file(p.join(package.path, 'pubspec.yaml')); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + if (pubspec.publishTo != 'none') { + pathDependencies[pluginName] = PathDependency(package.path); + } + } + return pathDependencies; + } + + String _pubspecToString(Pubspec pubspec) { + return ''' +### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. +name: ${pubspec.name} +description: ${pubspec.description} + +version: ${pubspec.version} + +environment:${_pubspecMapString(pubspec.environment)} + +dependencies:${_pubspecMapString(pubspec.dependencies)} + +dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} + +dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} +###'''; + } + + String _pubspecMapString(Map values) { + final StringBuffer buffer = StringBuffer(); + + for (MapEntry entry in values.entries) { + buffer.writeln(); + if (entry.value is VersionConstraint) { + buffer.write(' ${entry.key}: ${entry.value}'); + } else if (entry.value is SdkDependency) { + final SdkDependency dep = entry.value; + buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); + } else if (entry.value is PathDependency) { + final PathDependency dep = entry.value; + buffer.write(' ${entry.key}: \n path: ${dep.path}'); + } else { + throw UnimplementedError( + 'Not available for type: ${entry.value.runtimeType}', + ); + } + } + + return buffer.toString(); + } +} diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart new file mode 100644 index 00000000000..59c642265ba --- /dev/null +++ b/script/tool/lib/src/drive_examples_command.dart @@ -0,0 +1,210 @@ +// Copyright 2019 The Chromium 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'common.dart'; + +class DriveExamplesCommand extends PluginCommand { + DriveExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, + help: 'Runs the Linux implementation of the examples'); + argParser.addFlag(kMacos, + help: 'Runs the macOS implementation of the examples'); + argParser.addFlag(kWindows, + help: 'Runs the Windows implementation of the examples'); + argParser.addFlag(kIos, + help: 'Runs the iOS implementation of the examples'); + argParser.addFlag(kAndroid, + help: 'Runs the Android implementation of the examples'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: + 'Runs the driver tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'drive-examples'; + + @override + final String description = 'Runs driver tests for plugin example apps.\n\n' + 'For each *_test.dart in test_driver/ it drives an application with a ' + 'corresponding name in the test/ or test_driver/ directories.\n\n' + 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' + 'This command requires "flutter" to be in your path.\n\n' + 'If a file with a corresponding name cannot be found, this driver file' + 'will be used to drive the tests that match ' + 'integration_test/*_test.dart.'; + + @override + Future run() async { + checkSharding(); + final List failingTests = []; + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + await for (Directory plugin in getPlugins()) { + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + if (!(await pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { + continue; + } + final Directory driverTests = + fileSystem.directory(p.join(example.path, 'test_driver')); + if (!driverTests.existsSync()) { + // No driver tests available for this example + continue; + } + // Look for driver tests ending in _test.dart in test_driver/ + await for (FileSystemEntity test in driverTests.list()) { + final String driverTestName = + p.relative(test.path, from: driverTests.path); + if (!driverTestName.endsWith('_test.dart')) { + continue; + } + // Try to find a matching app to drive without the _test.dart + final String deviceTestName = driverTestName.replaceAll( + RegExp(r'_test.dart$'), + '.dart', + ); + String deviceTestPath = p.join('test', deviceTestName); + if (!fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + // If the app isn't in test/ folder, look in test_driver/ instead. + deviceTestPath = p.join('test_driver', deviceTestName); + } + + final List targetPaths = []; + if (fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + targetPaths.add(deviceTestPath); + } else { + final Directory integrationTests = + fileSystem.directory(p.join(example.path, 'integration_test')); + + if (await integrationTests.exists()) { + await for (FileSystemEntity integration_test + in integrationTests.list()) { + if (!integration_test.basename.endsWith('_test.dart')) { + continue; + } + targetPaths + .add(p.relative(integration_test.path, from: example.path)); + } + } + + if (targetPaths.isEmpty) { + print(''' +Unable to infer a target application for $driverTestName to drive. +Tried searching for the following: +1. test/$deviceTestName +2. test_driver/$deviceTestName +3. test_driver/*_test.dart +'''); + failingTests.add(p.relative(test.path, from: example.path)); + continue; + } + } + + final List driveArgs = ['drive']; + + final String enableExperiment = argResults[kEnableExperiment]; + if (enableExperiment.isNotEmpty) { + driveArgs.add('--enable-experiment=$enableExperiment'); + } + + if (isLinux && isLinuxPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'linux', + ]); + } + if (isMacos && isMacOsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'macos', + ]); + } + if (isWindows && isWindowsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'windows', + ]); + } + + for (final targetPath in targetPaths) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + ...driveArgs, + '--driver', + p.join('test_driver', driverTestName), + '--target', + targetPath, + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failingTests.add(p.join(packageName, deviceTestPath)); + } + } + } + } + } + print('\n\n'); + + if (failingTests.isNotEmpty) { + print('The following driver tests are failing (see above for details):'); + for (String test in failingTests) { + print(' * $test'); + } + throw ToolExit(1); + } + + print('All driver tests successful!'); + } + + Future pluginSupportedOnCurrentPlatform( + FileSystemEntity plugin, FileSystem fileSystem) async { + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + final bool isIOS = argResults[kIos]; + final bool isAndroid = argResults[kAndroid]; + if (isLinux) { + return isLinuxPlugin(plugin, fileSystem); + } + if (isMacos) { + return isMacOsPlugin(plugin, fileSystem); + } + if (isWindows) { + return isWindowsPlugin(plugin, fileSystem); + } + if (isIOS) { + return isIosPlugin(plugin, fileSystem); + } + if (isAndroid) { + return (isAndroidPlugin(plugin, fileSystem)); + } + // When we are here, no flags are specified. Only return true if the plugin supports mobile for legacy command support. + // TODO(cyanglaz): Make mobile platforms flags also required like other platforms (breaking change). + // https://github.com/flutter/flutter/issues/58285 + final bool isMobilePlugin = + isIosPlugin(plugin, fileSystem) || isAndroidPlugin(plugin, fileSystem); + return isMobilePlugin; + } +} diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart new file mode 100644 index 00000000000..0b4b2a471db --- /dev/null +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -0,0 +1,264 @@ +// Copyright 2018 The Chromium 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:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:uuid/uuid.dart'; + +import 'common.dart'; + +class FirebaseTestLabCommand extends PluginCommand { + FirebaseTestLabCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + 'project', + defaultsTo: 'flutter-infra', + help: 'The Firebase project name.', + ); + argParser.addOption('service-key', + defaultsTo: + p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); + argParser.addOption('test-run-id', + defaultsTo: Uuid().v4(), + help: + 'Optional string to append to the results path, to avoid conflicts. ' + 'Randomly chosen on each invocation if none is provided. ' + 'The default shown here is just an example.'); + argParser.addMultiOption('device', + splitCommas: false, + defaultsTo: [ + 'model=walleye,version=26', + 'model=flame,version=29' + ], + help: + 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); + argParser.addOption('results-bucket', + defaultsTo: 'gs://flutter_firebase_testlab'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'firebase-test-lab'; + + @override + final String description = 'Runs the instrumentation tests of the example ' + 'apps on Firebase Test Lab.\n\n' + 'Runs tests in test_instrumentation folder using the ' + 'instrumentation_test package.'; + + static const String _gradleWrapper = 'gradlew'; + + final Print _print; + + Completer _firebaseProjectConfigured; + + Future _configureFirebaseProject() async { + if (_firebaseProjectConfigured != null) { + return _firebaseProjectConfigured.future; + } else { + _firebaseProjectConfigured = Completer(); + } + await processRunner.runAndExitOnError('gcloud', [ + 'auth', + 'activate-service-account', + '--key-file=${argResults['service-key']}', + ]); + int exitCode = await processRunner.runAndStream('gcloud', [ + 'config', + 'set', + 'project', + argResults['project'], + ]); + if (exitCode == 0) { + _print('\nFirebase project configured.'); + return; + } else { + _print( + '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + } + _firebaseProjectConfigured.complete(null); + } + + @override + Future run() async { + checkSharding(); + final Stream packagesWithTests = getPackages().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join( + d.path, 'example', 'android', 'app', 'src', 'androidTest')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + int resultsCounter = + 0; // We use a unique GCS bucket for each Firebase Test Lab run + await for (Directory package in packagesWithTests) { + // See https://github.com/flutter/flutter/issues/38983 + + final Directory exampleDirectory = + fileSystem.directory(p.join(package.path, 'example')); + final String packageName = + p.relative(package.path, from: packagesDir.path); + _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(exampleDirectory.path, 'android')); + + final String enableExperiment = argResults[kEnableExperiment]; + final String encodedEnableExperiment = + Uri.encodeComponent('--enable-experiment=$enableExperiment'); + + // Ensures that gradle wrapper exists + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + final int exitCode = await processRunner.runAndStream( + 'flutter', + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + continue; + } + + await _configureFirebaseProject(); + + int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleAndroidTest', + '-Pverbose=true', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + + // Look for tests recursively in folders that start with 'test' and that + // live in the root or example folders. + bool isTestDir(FileSystemEntity dir) { + return p.basename(dir.path).startsWith('test') || + p.basename(dir.path) == 'integration_test'; + } + + final List testDirs = + package.listSync().where(isTestDir).toList(); + final Directory example = + fileSystem.directory(p.join(package.path, 'example')); + testDirs.addAll(example.listSync().where(isTestDir).toList()); + for (Directory testDir in testDirs) { + bool isE2ETest(FileSystemEntity file) { + return file.path.endsWith('_e2e.dart') || + (file.parent.basename == 'integration_test' && + file.path.endsWith('_test.dart')); + } + + final List testFiles = testDir + .listSync(recursive: true, followLinks: true) + .where(isE2ETest) + .toList(); + for (FileSystemEntity test in testFiles) { + exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleDebug', + '-Pverbose=true', + '-Ptarget=${test.path}', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; + final String testRunId = argResults['test-run-id']; + final String resultsDir = + 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '5m', + '--results-bucket=${argResults['results-bucket']}', + '--results-dir=${resultsDir}', + ]; + for (String device in argResults['device']) { + args.addAll(['--device', device]); + } + exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: exampleDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + } + } + } + + _print('\n\n'); + if (failingPackages.isNotEmpty) { + _print( + 'The instrumentation tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + _print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + _print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + _print('All Firebase Test Lab tests successful!'); + } +} diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart new file mode 100644 index 00000000000..ec326b96c1f --- /dev/null +++ b/script/tool/lib/src/format_command.dart @@ -0,0 +1,147 @@ +// Copyright 2017 The Chromium 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 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; +import 'package:quiver/iterables.dart'; + +import 'common.dart'; + +const String _googleFormatterUrl = + 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; + +class FormatCommand extends PluginCommand { + FormatCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag('travis', hide: true); + argParser.addOption('clang-format', + defaultsTo: 'clang-format', + help: 'Path to executable of clang-format v5.'); + } + + @override + final String name = 'format'; + + @override + final String description = + 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' + 'This command requires "git", "flutter" and "clang-format" v5 to be in ' + 'your path.'; + + @override + Future run() async { + checkSharding(); + final String googleFormatterPath = await _getGoogleFormatterPath(); + + await _formatDart(); + await _formatJava(googleFormatterPath); + await _formatCppAndObjectiveC(); + + if (argResults['travis']) { + final bool modified = await _didModifyAnything(); + if (modified) { + throw ToolExit(1); + } + } + } + + Future _didModifyAnything() async { + final io.ProcessResult modifiedFiles = await processRunner + .runAndExitOnError('git', ['ls-files', '--modified'], + workingDir: packagesDir); + + print('\n\n'); + + if (modifiedFiles.stdout.isEmpty) { + print('All files formatted correctly.'); + return false; + } + + print('These files are not formatted correctly (see diff below):'); + LineSplitter.split(modifiedFiles.stdout) + .map((String line) => ' $line') + .forEach(print); + + print('\nTo fix run "pub global activate flutter_plugin_tools && ' + 'pub global run flutter_plugin_tools format" or copy-paste ' + 'this command into your terminal:'); + + print('patch -p1 <['diff'], workingDir: packagesDir); + print(diff.stdout); + print('DONE'); + return true; + } + + Future _formatCppAndObjectiveC() async { + print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); + final Iterable allFiles = [] + ..addAll(await _getFilesWithExtension('.h')) + ..addAll(await _getFilesWithExtension('.m')) + ..addAll(await _getFilesWithExtension('.mm')) + ..addAll(await _getFilesWithExtension('.cc')) + ..addAll(await _getFilesWithExtension('.cpp')); + // Split this into multiple invocations to avoid a + // 'ProcessException: Argument list too long'. + final Iterable> batches = partition(allFiles, 100); + for (List batch in batches) { + await processRunner.runAndStream(argResults['clang-format'], + ['-i', '--style=Google']..addAll(batch), + workingDir: packagesDir, exitOnError: true); + } + } + + Future _formatJava(String googleFormatterPath) async { + print('Formatting all .java files...'); + final Iterable javaFiles = await _getFilesWithExtension('.java'); + await processRunner.runAndStream('java', + ['-jar', googleFormatterPath, '--replace']..addAll(javaFiles), + workingDir: packagesDir, exitOnError: true); + } + + Future _formatDart() async { + // This actually should be fine for non-Flutter Dart projects, no need to + // specifically shell out to dartfmt -w in that case. + print('Formatting all .dart files...'); + final Iterable dartFiles = await _getFilesWithExtension('.dart'); + if (dartFiles.isEmpty) { + print( + 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); + } else { + await processRunner.runAndStream( + 'flutter', ['format']..addAll(dartFiles), + workingDir: packagesDir, exitOnError: true); + } + } + + Future> _getFilesWithExtension(String extension) async => + getFiles() + .where((File file) => p.extension(file.path) == extension) + .map((File file) => file.path) + .toList(); + + Future _getGoogleFormatterPath() async { + final String javaFormatterPath = p.join( + p.dirname(p.fromUri(io.Platform.script)), + 'google-java-format-1.3-all-deps.jar'); + final File javaFormatterFile = fileSystem.file(javaFormatterPath); + + if (!javaFormatterFile.existsSync()) { + print('Downloading Google Java Format...'); + final http.Response response = await http.get(_googleFormatterUrl); + javaFormatterFile.writeAsBytesSync(response.bodyBytes); + } + + return javaFormatterPath; + } +} diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart new file mode 100644 index 00000000000..cf605bfc5ce --- /dev/null +++ b/script/tool/lib/src/java_test_command.dart @@ -0,0 +1,89 @@ +// Copyright 2018 The Chromium 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class JavaTestCommand extends PluginCommand { + JavaTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + @override + final String name = 'java-test'; + + @override + final String description = 'Runs the Java tests of the example apps.\n\n' + 'Building the apks of the example apps is required before executing this' + 'command.'; + + static const String _gradleWrapper = 'gradlew'; + + @override + Future run() async { + checkSharding(); + final Stream examplesWithTests = getExamples().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join(d.path, 'android', 'app', 'src', 'test')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + await for (Directory example in examplesWithTests) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + print('\nRUNNING JAVA TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(example.path, 'android')); + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + print('ERROR: Run "flutter build apk" on example app of $packageName' + 'before executing tests.'); + missingFlutterBuild.add(packageName); + continue; + } + + final int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + ['testDebugUnitTest', '--info'], + workingDir: androidDirectory); + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print( + 'The Java tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + print('All Java tests successful!'); + } +} diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart new file mode 100644 index 00000000000..68fd4b61dd6 --- /dev/null +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -0,0 +1,146 @@ +// Copyright 2017 The Chromium 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'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +typedef void Print(Object object); + +/// Lint the CocoaPod podspecs, run the static analyzer on iOS/macOS plugin +/// platform code, and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintPodspecsCommand extends PluginCommand { + LintPodspecsCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption('skip', + help: + 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('ignore-warnings', + help: + 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('no-analyze', + help: + 'Do not pass --analyze flag to "pod lib lint" for podspecs with this basename (example: plugins with known analyzer warnings)', + valueHelp: 'podspec_file_name'); + } + + @override + final String name = 'podspecs'; + + @override + List get aliases => ['podspec']; + + @override + final String description = + 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' + 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; + + final Platform platform; + + final Print _print; + + @override + Future run() async { + if (!platform.isMacOS) { + _print('Detected platform is not macOS, skipping podspec lint'); + return; + } + + checkSharding(); + + await processRunner.runAndExitOnError('which', ['pod'], + workingDir: packagesDir); + + _print('Starting podspec lint test'); + + final List failingPlugins = []; + for (File podspec in await _podspecsToLint()) { + if (!await _lintPodspec(podspec)) { + failingPlugins.add(p.basenameWithoutExtension(podspec.path)); + } + } + + _print('\n\n'); + if (failingPlugins.isNotEmpty) { + _print('The following plugins have podspec errors (see above):'); + failingPlugins.forEach((String plugin) { + _print(' * $plugin'); + }); + throw ToolExit(1); + } + } + + Future> _podspecsToLint() async { + final List podspecs = await getFiles().where((File entity) { + final String filePath = entity.path; + return p.extension(filePath) == '.podspec' && + !argResults['skip'].contains(p.basenameWithoutExtension(filePath)); + }).toList(); + + podspecs.sort( + (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); + return podspecs; + } + + Future _lintPodspec(File podspec) async { + // Do not run the static analyzer on plugins with known analyzer issues. + final String podspecPath = podspec.path; + final bool runAnalyzer = !argResults['no-analyze'] + .contains(p.basenameWithoutExtension(podspecPath)); + + final String podspecBasename = p.basename(podspecPath); + if (runAnalyzer) { + _print('Linting and analyzing $podspecBasename'); + } else { + _print('Linting $podspecBasename'); + } + + // Lint plugin as framework (use_frameworks!). + final ProcessResult frameworkResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: true); + _print(frameworkResult.stdout); + _print(frameworkResult.stderr); + + // Lint plugin as library. + final ProcessResult libraryResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: false); + _print(libraryResult.stdout); + _print(libraryResult.stderr); + + return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; + } + + Future _runPodLint(String podspecPath, + {bool runAnalyzer, bool libraryLint}) async { + final bool allowWarnings = argResults['ignore-warnings'] + .contains(p.basenameWithoutExtension(podspecPath)); + final List arguments = [ + 'lib', + 'lint', + podspecPath, + if (allowWarnings) '--allow-warnings', + if (runAnalyzer) '--analyze', + if (libraryLint) '--use-libraries' + ]; + + return processRunner.run('pod', arguments, + workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); + } +} diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart new file mode 100644 index 00000000000..7f94daac709 --- /dev/null +++ b/script/tool/lib/src/list_command.dart @@ -0,0 +1,60 @@ +// Copyright 2018 The Chromium 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 'package:file/file.dart'; + +import 'common.dart'; + +class ListCommand extends PluginCommand { + ListCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem) { + argParser.addOption( + _type, + defaultsTo: _plugin, + allowed: [_plugin, _example, _package, _file], + help: 'What type of file system content to list.', + ); + } + + static const String _type = 'type'; + static const String _plugin = 'plugin'; + static const String _example = 'example'; + static const String _package = 'package'; + static const String _file = 'file'; + + @override + final String name = 'list'; + + @override + final String description = 'Lists packages or files'; + + @override + Future run() async { + checkSharding(); + switch (argResults[_type]) { + case _plugin: + await for (Directory package in getPlugins()) { + print(package.path); + } + break; + case _example: + await for (Directory package in getExamples()) { + print(package.path); + } + break; + case _package: + await for (Directory package in getPackages()) { + print(package.path); + } + break; + case _file: + await for (File file in getFiles()) { + print(file.path); + } + break; + } + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart new file mode 100644 index 00000000000..bb3f67c0a9e --- /dev/null +++ b/script/tool/lib/src/main.dart @@ -0,0 +1,63 @@ +// Copyright 2017 The Chromium 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:path/path.dart' as p; + +import 'analyze_command.dart'; +import 'build_examples_command.dart'; +import 'common.dart'; +import 'create_all_plugins_app_command.dart'; +import 'drive_examples_command.dart'; +import 'firebase_test_lab_command.dart'; +import 'format_command.dart'; +import 'java_test_command.dart'; +import 'lint_podspecs_command.dart'; +import 'list_command.dart'; +import 'test_command.dart'; +import 'version_check_command.dart'; +import 'xctest_command.dart'; + +void main(List args) { + final FileSystem fileSystem = const LocalFileSystem(); + + Directory packagesDir = fileSystem + .directory(p.join(fileSystem.currentDirectory.path, 'packages')); + + if (!packagesDir.existsSync()) { + if (p.basename(fileSystem.currentDirectory.path) == 'packages') { + packagesDir = fileSystem.currentDirectory; + } else { + print('Error: Cannot find a "packages" sub-directory'); + io.exit(1); + } + } + + final CommandRunner commandRunner = CommandRunner( + 'pub global run flutter_plugin_tools', + 'Productivity utils for hosting multiple plugins within one repository.') + ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) + ..addCommand(BuildExamplesCommand(packagesDir, fileSystem)) + ..addCommand(CreateAllPluginsAppCommand(packagesDir, fileSystem)) + ..addCommand(DriveExamplesCommand(packagesDir, fileSystem)) + ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) + ..addCommand(FormatCommand(packagesDir, fileSystem)) + ..addCommand(JavaTestCommand(packagesDir, fileSystem)) + ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) + ..addCommand(ListCommand(packagesDir, fileSystem)) + ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) + ..addCommand(TestCommand(packagesDir, fileSystem)) + ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) + ..addCommand(XCTestCommand(packagesDir, fileSystem)); + + commandRunner.run(args).catchError((Object e) { + final ToolExit toolExit = e; + io.exit(toolExit.exitCode); + }, test: (Object e) => e is ToolExit); +} diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart new file mode 100644 index 00000000000..f7e3b5deeec --- /dev/null +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -0,0 +1,223 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +/// Wraps pub publish with a few niceties used by the flutter/plugin team. +/// +/// 1. Checks for any modified files in git and refuses to publish if there's an +/// issue. +/// 2. Tags the release with the format -v. +/// 3. Pushes the release to a remote. +/// +/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full +/// usage information. +/// +/// [processRunner], [print], and [stdin] can be overriden for easier testing. +class PublishPluginCommand extends PluginCommand { + PublishPluginCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + Stdin stdinput, + }) : _print = print, + _stdin = stdinput ?? stdin, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _packageOption, + help: 'The package to publish.' + 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', + ); + argParser.addMultiOption(_pubFlagsOption, + help: + 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); + argParser.addFlag( + _tagReleaseOption, + help: 'Whether or not to tag the release.', + defaultsTo: true, + negatable: true, + ); + argParser.addFlag( + _pushTagsOption, + help: + 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', + defaultsTo: true, + negatable: true, + ); + argParser.addOption( + _remoteOption, + help: + 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', + // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. + defaultsTo: 'upstream', + ); + } + + static const String _packageOption = 'package'; + static const String _tagReleaseOption = 'tag-release'; + static const String _pushTagsOption = 'push-tags'; + static const String _pubFlagsOption = 'pub-publish-flags'; + static const String _remoteOption = 'remote'; + + // Version tags should follow -v. For example, + // `flutter_plugin_tools-v0.0.24`. + static const String _tagFormat = '%PACKAGE%-v%VERSION%'; + + @override + final String name = 'publish-plugin'; + + @override + final String description = + 'Attempts to publish the given plugin and tag its release on GitHub.'; + + final Print _print; + final Stdin _stdin; + // The directory of the actual package that we are publishing. + Directory _packageDir; + StreamSubscription _stdinSubscription; + + @override + Future run() async { + checkSharding(); + _print('Checking local repo...'); + _packageDir = _checkPackageDir(); + await _checkGitStatus(); + final bool shouldPushTag = argResults[_pushTagsOption]; + final String remote = argResults[_remoteOption]; + String remoteUrl; + if (shouldPushTag) { + remoteUrl = await _verifyRemote(remote); + } + _print('Local repo is ready!'); + + await _publish(); + _print('Package published!'); + if (!argResults[_tagReleaseOption]) { + return await _finishSuccesfully(); + } + + _print('Tagging release...'); + final String tag = _getTag(); + await processRunner.runAndExitOnError('git', ['tag', tag], + workingDir: _packageDir); + if (!shouldPushTag) { + return await _finishSuccesfully(); + } + + _print('Pushing tag to $remote...'); + await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); + await _finishSuccesfully(); + } + + Future _finishSuccesfully() async { + await _stdinSubscription.cancel(); + _print('Done!'); + } + + Directory _checkPackageDir() { + final String package = argResults[_packageOption]; + if (package == null) { + _print( + 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); + throw ToolExit(1); + } + final Directory _packageDir = packagesDir.childDirectory(package); + if (!_packageDir.existsSync()) { + _print('${_packageDir.absolute.path} does not exist.'); + throw ToolExit(1); + } + return _packageDir; + } + + Future _checkGitStatus() async { + if (!await GitDir.isGitDir(packagesDir.path)) { + _print('$packagesDir is not a valid Git repository.'); + throw ToolExit(1); + } + + final ProcessResult statusResult = await processRunner.runAndExitOnError( + 'git', + [ + 'status', + '--porcelain', + '--ignored', + _packageDir.absolute.path + ], + workingDir: _packageDir); + final String statusOutput = statusResult.stdout; + if (statusOutput.isNotEmpty) { + _print( + "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" + '$statusOutput\n' + 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); + throw ToolExit(1); + } + } + + Future _verifyRemote(String remote) async { + final ProcessResult remoteInfo = await processRunner.runAndExitOnError( + 'git', ['remote', 'get-url', remote], + workingDir: _packageDir); + return remoteInfo.stdout; + } + + Future _publish() async { + final List publishFlags = argResults[_pubFlagsOption]; + _print( + 'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n'); + final Process publish = await processRunner.start( + 'flutter', ['pub', 'publish'] + publishFlags, + workingDirectory: _packageDir); + publish.stdout + .transform(utf8.decoder) + .listen((String data) => _print(data)); + publish.stderr + .transform(utf8.decoder) + .listen((String data) => _print(data)); + _stdinSubscription = _stdin + .transform(utf8.decoder) + .listen((String data) => publish.stdin.writeln(data)); + final int result = await publish.exitCode; + if (result != 0) { + _print('Publish failed. Exiting.'); + throw ToolExit(result); + } + } + + String _getTag() { + final File pubspecFile = + fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final String name = pubspecYaml['name']; + final String version = pubspecYaml['version']; + // We should have failed to publish if these were unset. + assert(name.isNotEmpty && version.isNotEmpty); + return _tagFormat + .replaceAll('%PACKAGE%', name) + .replaceAll('%VERSION%', version); + } + + Future _pushTagToRemote( + {@required String remote, + @required String tag, + @required String remoteUrl}) async { + assert(remote != null && tag != null && remoteUrl != null); + _print('Ready to push $tag to $remoteUrl (y/n)?'); + final String input = _stdin.readLineSync(); + if (input.toLowerCase() != 'y') { + _print('Tag push canceled.'); + throw ToolExit(1); + } + + await processRunner.runAndExitOnError('git', ['push', remote, tag], + workingDir: packagesDir); + } +} diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart new file mode 100644 index 00000000000..e938168cfa8 --- /dev/null +++ b/script/tool/lib/src/test_command.dart @@ -0,0 +1,101 @@ +// Copyright 2017 The Chromium 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class TestCommand extends PluginCommand { + TestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Runs the tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'test'; + + @override + final String description = 'Runs the Dart tests for all packages.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + final List failingPackages = []; + await for (Directory packageDir in getPackages()) { + final String packageName = + p.relative(packageDir.path, from: packagesDir.path); + if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { + print('SKIPPING $packageName - no test subdirectory'); + continue; + } + + print('RUNNING $packageName tests...'); + + final String enableExperiment = argResults[kEnableExperiment]; + + // `flutter test` automatically gets packages. `pub run test` does not. :( + int exitCode = 0; + if (isFlutterPackage(packageDir, fileSystem)) { + final List args = [ + 'test', + '--color', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ]; + + if (isWebPlugin(packageDir, fileSystem)) { + args.add('--platform=chrome'); + } + exitCode = await processRunner.runAndStream( + 'flutter', + args, + workingDir: packageDir, + ); + } else { + exitCode = await processRunner.runAndStream( + 'pub', + ['get'], + workingDir: packageDir, + ); + if (exitCode == 0) { + exitCode = await processRunner.runAndStream( + 'pub', + [ + 'run', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + 'test', + ], + workingDir: packageDir, + ); + } + } + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('Tests for the following packages are failing (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('All tests are passing!'); + } +} diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart new file mode 100644 index 00000000000..2c6b92bbcb7 --- /dev/null +++ b/script/tool/lib/src/version_check_command.dart @@ -0,0 +1,220 @@ +// Copyright 2017 The Chromium 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:io' as io; + +import 'package:meta/meta.dart'; +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +const String _kBaseSha = 'base_sha'; + +class GitVersionFinder { + GitVersionFinder(this.baseGitDir, this.baseSha); + + final GitDir baseGitDir; + final String baseSha; + + static bool isPubspec(String file) { + return file.trim().endsWith('pubspec.yaml'); + } + + Future> getChangedPubSpecs() async { + final io.ProcessResult changedFilesCommand = await baseGitDir + .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); + final List changedFiles = + changedFilesCommand.stdout.toString().split('\n'); + return changedFiles.where(isPubspec).toList(); + } + + Future getPackageVersion(String pubspecPath, String gitRef) async { + final io.ProcessResult gitShow = + await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); + final String fileContent = gitShow.stdout; + final String versionString = loadYaml(fileContent)['version']; + return versionString == null ? null : Version.parse(versionString); + } +} + +enum NextVersionType { + BREAKING_MAJOR, + MAJOR_NULLSAFETY_PRE_RELEASE, + MINOR_NULLSAFETY_PRE_RELEASE, + MINOR, + PATCH, + RELEASE, +} + +Version getNextNullSafetyPreRelease(Version current, Version next) { + String nextNullsafetyPrerelease = 'nullsafety'; + if (current.isPreRelease && + current.preRelease.first is String && + current.preRelease.first == 'nullsafety') { + if (current.preRelease.length == 1) { + nextNullsafetyPrerelease = 'nullsafety.1'; + } else if (current.preRelease.length == 2 && + current.preRelease.last is int) { + nextNullsafetyPrerelease = 'nullsafety.${current.preRelease.last + 1}'; + } + } + return Version( + next.major, + next.minor, + next.patch, + pre: nextNullsafetyPrerelease, + ); +} + +@visibleForTesting +Map getAllowedNextVersions( + Version masterVersion, Version headVersion) { + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMajor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Map allowedNextVersions = + { + masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, + nextNullSafetyMajor: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE, + nextNullSafetyMinor: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE, + masterVersion.nextMinor: NextVersionType.MINOR, + masterVersion.nextPatch: NextVersionType.PATCH, + }; + + if (masterVersion.major < 1 && headVersion.major < 1) { + int nextBuildNumber = -1; + if (masterVersion.build.isEmpty) { + nextBuildNumber = 1; + } else { + final int currentBuildNumber = masterVersion.build.first; + nextBuildNumber = currentBuildNumber + 1; + } + final Version preReleaseVersion = Version( + masterVersion.major, + masterVersion.minor, + masterVersion.patch, + build: nextBuildNumber.toString(), + ); + allowedNextVersions.clear(); + allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[masterVersion.nextMinor] = + NextVersionType.BREAKING_MAJOR; + allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; + allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; + + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextPatch); + + allowedNextVersions[nextNullSafetyMajor] = + NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE; + allowedNextVersions[nextNullSafetyMinor] = + NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE; + } + return allowedNextVersions; +} + +class VersionCheckCommand extends PluginCommand { + VersionCheckCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.gitDir, + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption(_kBaseSha); + } + + /// The git directory to use. By default it uses the parent directory. + /// + /// This can be mocked for testing. + final GitDir gitDir; + + @override + final String name = 'version-check'; + + @override + final String description = + 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + final String rootDir = packagesDir.parent.absolute.path; + final String baseSha = argResults[_kBaseSha]; + + GitDir baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + print('$rootDir is not a valid Git repository.'); + throw ToolExit(2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + + for (final String pubspecPath in changedPubspecs) { + try { + final File pubspecFile = fileSystem.file(pubspecPath); + if (!pubspecFile.existsSync()) { + continue; + } + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + continue; + } + + final Version masterVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, baseSha); + final Version headVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, 'HEAD'); + if (headVersion == null) { + continue; // Example apps don't have versions + } + + final Map allowedNextVersions = + getAllowedNextVersions(masterVersion, headVersion); + + if (!allowedNextVersions.containsKey(headVersion)) { + final String error = '$pubspecPath incorrectly updated version.\n' + 'HEAD: $headVersion, master: $masterVersion.\n' + 'Allowed versions: $allowedNextVersions'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + + bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); + if (isPlatformInterface && + allowedNextVersions[headVersion] == + NextVersionType.BREAKING_MAJOR) { + final String error = '$pubspecPath breaking change detected.\n' + 'Breaking changes to platform interfaces are strongly discouraged.\n'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + } on io.ProcessException { + print('Unable to find pubspec in master for $pubspecPath.' + ' Safe to ignore if the project is new.'); + } + } + + print('No version check errors found!'); + } +} diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart new file mode 100644 index 00000000000..d90b7a8fbfe --- /dev/null +++ b/script/tool/lib/src/xctest_command.dart @@ -0,0 +1,216 @@ +// Copyright 2017 The Chromium 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +const String _kiOSDestination = 'ios-destination'; +const String _kTarget = 'target'; +const String _kSkip = 'skip'; +const String _kXcodeBuildCommand = 'xcodebuild'; +const String _kXCRunCommand = 'xcrun'; +const String _kFoundNoSimulatorsMessage = + 'Cannot find any available simulators, tests failed'; + +/// The command to run iOS' XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. +/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcodeproj". +/// The command takes a "-target" argument which has to match the target of the test target. +/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html +class XCTestCommand extends PluginCommand { + XCTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _kiOSDestination, + help: + 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' + 'this is passed to the `-destination` argument in xcodebuild command.\n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', + ); + argParser.addOption(_kTarget, + help: 'The test target.\n' + 'This is the xcode project test target. This is passed to the `-scheme` argument in the xcodebuild command. \n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the scheme'); + argParser.addMultiOption(_kSkip, + help: 'Plugins to skip while running this command. \n'); + } + + @override + final String name = 'xctest'; + + @override + final String description = 'Runs the xctests in the iOS example apps.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (argResults[_kTarget] == null) { + // TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified. + // https://github.com/flutter/flutter/issues/68419 + print('--$_kTarget must be specified'); + throw ToolExit(1); + } + + String destination = argResults[_kiOSDestination]; + if (destination == null) { + String simulatorId = await _findAvailableIphoneSimulator(); + if (simulatorId == null) { + print(_kFoundNoSimulatorsMessage); + throw ToolExit(1); + } + destination = 'id=$simulatorId'; + } + + checkSharding(); + + final String target = argResults[_kTarget]; + final List skipped = argResults[_kSkip]; + + List failingPackages = []; + await for (Directory plugin in getPlugins()) { + // Start running for package. + final String packageName = + p.relative(plugin.path, from: packagesDir.path); + print('Start running for $packageName ...'); + if (!isIosPlugin(plugin, fileSystem)) { + print('iOS is not supported by this plugin.'); + print('\n\n'); + continue; + } + if (skipped.contains(packageName)) { + print('$packageName was skipped with the --skip flag.'); + print('\n\n'); + continue; + } + for (Directory example in getExamplesForPlugin(plugin)) { + // Look for the test scheme in the example app. + print('Look for target named: $_kTarget ...'); + final List findSchemeArgs = [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ]; + final String completeFindSchemeCommand = + '$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}'; + print(completeFindSchemeCommand); + final io.ProcessResult xcodeprojListResult = await processRunner + .run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example); + if (xcodeprojListResult.exitCode != 0) { + print('Error occurred while running "$completeFindSchemeCommand":\n' + '${xcodeprojListResult.stderr}'); + failingPackages.add(packageName); + print('\n\n'); + continue; + } + + final String xcodeprojListOutput = xcodeprojListResult.stdout; + Map xcodeprojListOutputJson = + jsonDecode(xcodeprojListOutput); + if (!xcodeprojListOutputJson['project']['targets'].contains(target)) { + failingPackages.add(packageName); + print('$target not configured for $packageName, test failed.'); + print( + 'Please check the scheme for the test target if it matches the name $target.\n' + 'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.'); + print('\n\n'); + continue; + } + // Found the scheme, running tests + print('Running XCTests:$target for $packageName ...'); + final List xctestArgs = [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + target, + '-destination', + destination, + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ]; + final String completeTestCommand = + '$_kXcodeBuildCommand ${xctestArgs.join(' ')}'; + print(completeTestCommand); + final int exitCode = await processRunner + .runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example); + if (exitCode == 0) { + print('Successfully ran xctest for $packageName'); + } else { + failingPackages.add(packageName); + } + } + } + + // Command end, print reports. + if (failingPackages.isEmpty) { + print("All XCTests have passed!"); + } else { + print( + 'The following packages are failing XCTests (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + } + + Future _findAvailableIphoneSimulator() async { + // Find the first available destination if not specified. + final List findSimulatorsArguments = [ + 'simctl', + 'list', + '--json' + ]; + final String findSimulatorCompleteCommand = + '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; + print('Looking for available simulators...'); + print(findSimulatorCompleteCommand); + final io.ProcessResult findSimulatorsResult = + await processRunner.run(_kXCRunCommand, findSimulatorsArguments); + if (findSimulatorsResult.exitCode != 0) { + print('Error occurred while running "$findSimulatorCompleteCommand":\n' + '${findSimulatorsResult.stderr}'); + throw ToolExit(1); + } + final Map simulatorListJson = + jsonDecode(findSimulatorsResult.stdout); + final List runtimes = simulatorListJson['runtimes']; + final Map devices = simulatorListJson['devices']; + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String id; + // Looking for runtimes, trying to find one with highest OS version. + for (Map runtimeMap in runtimes.reversed) { + if (!runtimeMap['name'].contains('iOS')) { + continue; + } + final String runtimeID = runtimeMap['identifier']; + final List devicesForRuntime = devices[runtimeID]; + if (devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (Map device in devicesForRuntime.reversed) { + if (device['availabilityError'] != null || + (device['isAvailable'] as bool == false)) { + continue; + } + id = device['udid']; + print('device selected: $device'); + return id; + } + } + return null; + } +} diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml new file mode 100644 index 00000000000..d9fce4ad26a --- /dev/null +++ b/script/tool/pubspec.yaml @@ -0,0 +1,25 @@ +name: flutter_plugin_tools +description: Productivity utils for hosting multiple plugins within one repository. +publish_to: 'none' + +dependencies: + args: "^1.4.3" + path: "^1.6.1" + http: "^0.12.1" + async: "^2.0.7" + yaml: "^2.1.15" + quiver: "^2.0.2" + pub_semver: ^1.4.2 + colorize: ^2.0.0 + git: ^1.0.0 + platform: ^2.2.0 + pubspec_parse: "^0.1.4" + test: ^1.6.4 + meta: ^1.1.7 + file: ^5.0.10 + uuid: ^2.0.4 + http_multi_server: ^2.2.0 + collection: 1.14.13 + +environment: + sdk: ">=2.3.0 <3.0.0" From d12e22a68d651faac77d9b7293d8c66d98cefcef Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 16 Feb 2021 06:41:53 -0800 Subject: [PATCH 002/249] Fix the build-all exclusion list (#3552) build_all_plugins_app.sh contains an exclusion list, which currently contains almost all of the non-app-facing plugins. However, the script those exclusions are passed to expects federated plugin exclusions to be of the form plugin_name/plugin_name_subplugin_name, not just plugin_name_subplugin_name, so in practice almost nothing on that list has actually been doing anything. This fixes the script to allow either mode of exclusion (since clearly people expect using just the name to work), and scrubs everything from the list that clearly wasn't actually needed. --- script/tool/lib/src/common.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index 78b91ee8a75..08622df281b 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -294,8 +294,10 @@ abstract class PluginCommand extends Command { // passed. final String relativePath = p.relative(subdir.path, from: packagesDir.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) || From bd8e34c7b1cd59d7df3e47f486ad576885f625b1 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 16 Feb 2021 16:01:31 -0800 Subject: [PATCH 003/249] Remove iOS stubs (#3490) Plugins that don't actually support iOS are no longer required to have an iOS stub to prevent build failures. This removes all iOS stubs from plugins that don't support iOS. --- script/tool/lib/src/drive_examples_command.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 59c642265ba..0bd531a20f8 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -200,11 +200,10 @@ Tried searching for the following: if (isAndroid) { return (isAndroidPlugin(plugin, fileSystem)); } - // When we are here, no flags are specified. Only return true if the plugin supports mobile for legacy command support. - // TODO(cyanglaz): Make mobile platforms flags also required like other platforms (breaking change). + // When we are here, no flags are specified. Only return true if the plugin + // supports Android for legacy command support. TODO(cyanglaz): Make Android + // flag also required like other platforms (breaking change). // https://github.com/flutter/flutter/issues/58285 - final bool isMobilePlugin = - isIosPlugin(plugin, fileSystem) || isAndroidPlugin(plugin, fileSystem); - return isMobilePlugin; + return isAndroidPlugin(plugin, fileSystem); } } From c885248c16c69da3cae0a8690637def9e46ea18a Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 17 Feb 2021 09:17:44 -0800 Subject: [PATCH 004/249] Publish check (#3556) --- script/tool/lib/src/main.dart | 2 + .../tool/lib/src/publish_check_command.dart | 92 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 script/tool/lib/src/publish_check_command.dart diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index bb3f67c0a9e..fa81597237d 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -7,6 +7,7 @@ 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/publish_check_command.dart'; import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; import 'package:path/path.dart' as p; @@ -51,6 +52,7 @@ void main(List args) { ..addCommand(JavaTestCommand(packagesDir, fileSystem)) ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) ..addCommand(ListCommand(packagesDir, fileSystem)) + ..addCommand(PublishCheckCommand(packagesDir, fileSystem)) ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) ..addCommand(TestCommand(packagesDir, fileSystem)) ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart new file mode 100644 index 00000000000..8d6f6bb9ab6 --- /dev/null +++ b/script/tool/lib/src/publish_check_command.dart @@ -0,0 +1,92 @@ +// Copyright 2017 The Chromium 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 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +class PublishCheckCommand extends PluginCommand { + PublishCheckCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + @override + final String name = 'publish-check'; + + @override + final String description = + 'Checks to make sure that a plugin *could* be published.'; + + @override + Future run() async { + checkSharding(); + final List failedPackages = []; + + await for (Directory plugin in getPlugins()) { + if (!(await passesPublishCheck(plugin))) failedPackages.add(plugin); + } + + if (failedPackages.isNotEmpty) { + final String error = + 'FAIL: The following ${failedPackages.length} package(s) failed the ' + 'publishing check:'; + final String joinedFailedPackages = failedPackages.join('\n'); + + final Colorize colorizedError = Colorize('$error\n$joinedFailedPackages') + ..red(); + print(colorizedError); + throw ToolExit(1); + } + + final Colorize passedMessage = + Colorize('All packages passed publish check!')..green(); + print(passedMessage); + } + + Pubspec tryParsePubspec(Directory package) { + final File pubspecFile = package.childFile('pubspec.yaml'); + + try { + return Pubspec.parse(pubspecFile.readAsStringSync()); + } on Exception catch (exception) { + print( + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}', + ); + return null; + } + } + + Future passesPublishCheck(Directory package) async { + final String packageName = package.basename; + print('Checking that $packageName can be published.'); + + final Pubspec pubspec = tryParsePubspec(package); + if (pubspec == null) { + return false; + } else if (pubspec.publishTo == 'none') { + print('Package $packageName is marked as unpublishable. Skipping.'); + return true; + } + + final int exitCode = await processRunner.runAndStream( + 'flutter', + ['pub', 'publish', '--', '--dry-run'], + workingDir: package, + ); + + if (exitCode == 0) { + print("Package $packageName is able to be published."); + return true; + } else { + print('Unable to publish $packageName'); + return false; + } + } +} From 67e7ab4745378473fc0878473d9595332774907c Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Thu, 18 Feb 2021 09:47:08 -0800 Subject: [PATCH 005/249] Publish check ignores prerelease sdk (#3560) --- .../tool/lib/src/publish_check_command.dart | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 8d6f6bb9ab6..af009952856 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io' as io; import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; @@ -63,6 +64,44 @@ class PublishCheckCommand extends PluginCommand { } } + Future hasValidPublishCheckRun(Directory package) async { + final io.Process process = await io.Process.start( + 'flutter', + ['pub', 'publish', '--', '--dry-run'], + workingDirectory: package.path, + ); + + final StringBuffer outputBuffer = StringBuffer(); + + final Completer stdOutCompleter = Completer(); + process.stdout.listen( + (List event) { + io.stdout.add(event); + outputBuffer.write(String.fromCharCodes(event)); + }, + onDone: () => stdOutCompleter.complete(), + ); + + final Completer stdInCompleter = Completer(); + process.stderr.listen( + (List event) { + io.stderr.add(event); + outputBuffer.write(String.fromCharCodes(event)); + }, + onDone: () => stdInCompleter.complete(), + ); + + if (await process.exitCode == 0) return true; + + await stdOutCompleter.future; + await stdInCompleter.future; + + final String output = outputBuffer.toString(); + return output.contains('Package has 1 warning.') && + output.contains( + 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); + } + Future passesPublishCheck(Directory package) async { final String packageName = package.basename; print('Checking that $packageName can be published.'); @@ -75,13 +114,7 @@ class PublishCheckCommand extends PluginCommand { return true; } - final int exitCode = await processRunner.runAndStream( - 'flutter', - ['pub', 'publish', '--', '--dry-run'], - workingDir: package, - ); - - if (exitCode == 0) { + if (await hasValidPublishCheckRun(package)) { print("Package $packageName is able to be published."); return true; } else { From 55ed162e4222ecc50af32b8eda5a7366700d4047 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 19 Feb 2021 11:56:26 -0800 Subject: [PATCH 006/249] [in_app_purchase] Migrate to NNBD (#3555) --- script/tool/lib/src/publish_check_command.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index af009952856..670fedaf2fa 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -97,7 +97,7 @@ class PublishCheckCommand extends PluginCommand { await stdInCompleter.future; final String output = outputBuffer.toString(); - return output.contains('Package has 1 warning.') && + return output.contains('Package has 1 warning') && output.contains( 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); } From e1b3b296f2fa9686fffd2202d37c3a76affa1a84 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Mon, 1 Mar 2021 18:15:51 -0800 Subject: [PATCH 007/249] Move plugin tool tests over (#3606) --- script/tool/pubspec.yaml | 5 + script/tool/test/analyze_command_test.dart | 93 ++++ .../test/build_examples_command_test.dart | 470 ++++++++++++++++ script/tool/test/common_test.dart | 100 ++++ .../test/drive_examples_command_test.dart | 505 ++++++++++++++++++ script/tool/test/firebase_test_lab_test.dart | 256 +++++++++ .../tool/test/lint_podspecs_command_test.dart | 202 +++++++ script/tool/test/list_command_test.dart | 198 +++++++ script/tool/test/mocks.dart | 33 ++ .../test/publish_plugin_command_test.dart | 378 +++++++++++++ script/tool/test/test_command_test.dart | 154 ++++++ script/tool/test/util.dart | 291 ++++++++++ script/tool/test/version_check_test.dart | 319 +++++++++++ script/tool/test/xctest_command_test.dart | 358 +++++++++++++ 14 files changed, 3362 insertions(+) create mode 100644 script/tool/test/analyze_command_test.dart create mode 100644 script/tool/test/build_examples_command_test.dart create mode 100644 script/tool/test/common_test.dart create mode 100644 script/tool/test/drive_examples_command_test.dart create mode 100644 script/tool/test/firebase_test_lab_test.dart create mode 100644 script/tool/test/lint_podspecs_command_test.dart create mode 100644 script/tool/test/list_command_test.dart create mode 100644 script/tool/test/mocks.dart create mode 100644 script/tool/test/publish_plugin_command_test.dart create mode 100644 script/tool/test/test_command_test.dart create mode 100644 script/tool/test/util.dart create mode 100644 script/tool/test/version_check_test.dart create mode 100644 script/tool/test/xctest_command_test.dart diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index d9fce4ad26a..e4712395900 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -21,5 +21,10 @@ dependencies: http_multi_server: ^2.2.0 collection: 1.14.13 +dev_dependencies: + matcher: ^0.12.6 + mockito: ^4.1.1 + pedantic: 1.8.0 + environment: sdk: ">=2.3.0 <3.0.0" diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart new file mode 100644 index 00000000000..9e7a42bbb68 --- /dev/null +++ b/script/tool/test/analyze_command_test.dart @@ -0,0 +1,93 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/analyze_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final AnalyzeCommand analyzeCommand = AnalyzeCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('analyze_command', 'Test for analyze_command'); + runner.addCommand(analyzeCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('analyzes all packages', () async { + final Directory plugin1Dir = await createFakePlugin('a'); + final Directory plugin2Dir = await createFakePlugin('b'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], plugin1Dir.path), + ProcessCall('flutter', ['packages', 'get'], plugin2Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin1Dir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + plugin2Dir.path), + ])); + }); + + group('verifies analysis settings', () { + test('fails analysis_options.yaml', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('fails .analysis_options', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['.analysis_options'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('takes an allow list', () async { + final Directory pluginDir = + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze', '--custom-analysis', 'foo']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('pub', ['global', 'activate', 'tuneup'], + mockPackagesDir.path), + ProcessCall('flutter', ['packages', 'get'], pluginDir.path), + ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + pluginDir.path), + ])); + }); + }); +} diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart new file mode 100644 index 00000000000..eaf5049dcc0 --- /dev/null +++ b/script/tool/test/build_examples_command_test.dart @@ -0,0 +1,470 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/build_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test build_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final BuildExamplesCommand command = BuildExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'build_examples_command', 'Test for build_example_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('building for iOS when plugin is not set up for iOS results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ipa', '--no-macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + 'iOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Linux when plugin is not set up for Linux results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + 'Linux is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --linux with no + // Linux implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for Linux', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'linux'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('building for macos with no implementation results in no-op', + () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\macOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + test('building for macos', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['pub', 'get'], + pluginExampleDirectory.path), + ProcessCall(flutterCommand, ['build', 'macos'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Windows when plugin is not set up for Windows results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + 'Windows is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for windows', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'windows'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test( + 'building for Android when plugin is not set up for Android results in no-op', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--no-ipa']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + 'Android is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'apk'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for Android', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + ['build', 'apk', '--enable-experiment=exp1'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + + test('enable-experiment flag for ios', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + }); +} diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart new file mode 100644 index 00000000000..b3504c2358d --- /dev/null +++ b/script/tool/test/common_test.dart @@ -0,0 +1,100 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + RecordingProcessRunner processRunner; + CommandRunner runner; + List plugins; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + plugins = []; + final SamplePluginCommand samplePluginCommand = SamplePluginCommand( + plugins, + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + ); + runner = + CommandRunner('common_command', 'Test for common functionality'); + runner.addCommand(samplePluginCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('all plugins from file system', () async { + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run(['sample']); + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('exclude plugins when plugins flag is specified', () async { + createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run( + ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude plugins when plugins flag isn\'t specified', () async { + createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + await runner.run(['sample', '--exclude=plugin1,plugin2']); + expect(plugins, unorderedEquals([])); + }); + + test('exclude federated plugins when plugins flag is specified', () async { + createFakePlugin('plugin1', parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated/plugin1' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude entire federated plugins when plugins flag is specified', + () async { + createFakePlugin('plugin1', parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); +} + +class SamplePluginCommand extends PluginCommand { + SamplePluginCommand( + this.plugins_, + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + List plugins_; + + @override + final String name = 'sample'; + + @override + final String description = 'sample command'; + + @override + Future run() async { + await for (Directory package in getPlugins()) { + this.plugins_.add(package.path); + } + } +} diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart new file mode 100644 index 00000000000..f4bdd95c166 --- /dev/null +++ b/script/tool/test/drive_examples_command_test.dart @@ -0,0 +1,505 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test drive_example_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final DriveExamplesCommand command = DriveExamplesCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'drive_examples_command', 'Test for drive_example_command'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('driving under folder "test"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver"', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver" when test files are missing"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await expectLater( + () => runCapturingPrint(runner, ['drive-examples']), + throwsA(const TypeMatcher())); + }); + + test( + 'driving under folder "test_driver" when targets are under "integration_test"', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'integration_test.dart'], + ['example', 'integration_test', 'bar_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'ignore_me.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String driverTestPath = p.join('test_driver', 'integration_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'bar_test.dart'), + ], + pluginExampleDirectory.path), + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'foo_test.dart'), + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Linux is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --linux on a non-Linux + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Linux plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'linux', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport macOS is a no-op', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on a macOS plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport windows is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --windows on a non-windows + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Windows plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'windows', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support mobile is no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('enable-experiment flag', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'drive-examples', + '--enable-experiment=exp1', + ]); + + String deviceTestPath = p.join('test', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--enable-experiment=exp1', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + }); +} diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart new file mode 100644 index 00000000000..97b977619d5 --- /dev/null +++ b/script/tool/test/firebase_test_lab_test.dart @@ -0,0 +1,256 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$FirebaseTestLabCommand', () { + final List printedMessages = []; + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final FirebaseTestLabCommand command = FirebaseTestLabCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString())); + + runner = CommandRunner( + 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); + runner.addCommand(command); + }); + + tearDown(() { + printedMessages.clear(); + }); + + test('retries gcloud set', () async { + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockProcess; + createFakePlugin('plugin', withExtraFiles: >[ + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + await expectLater( + () => runCapturingPrint(runner, ['firebase-test-lab']), + throwsA(const TypeMatcher())); + expect( + printedMessages, + contains( + "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.")); + }); + + test('runs e2e tests', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + ]); + + expect( + printedMessages, + orderedEquals([ + '\nRUNNING FIREBASE TEST LAB TESTS for plugin', + '\nFirebase project configured.', + '\n\n', + 'All Firebase Test Lab tests successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ]), + ); + }); + + test('experimental flag', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--test-run-id', + 'testRunId', + '--enable-experiment=exp1', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart new file mode 100644 index 00000000000..49d6ad4d8e2 --- /dev/null +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -0,0 +1,202 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintPodspecsCommand', () { + CommandRunner runner; + MockPlatform mockPlatform; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + List printedMessages; + + setUp(() { + initializeFakePackages(); + + printedMessages = []; + mockPlatform = MockPlatform(); + when(mockPlatform.isMacOS).thenReturn(true); + final LintPodspecsCommand command = LintPodspecsCommand( + mockPackagesDir, + mockFileSystem, + processRunner: processRunner, + platform: mockPlatform, + print: (Object message) => printedMessages.add(message.toString()), + ); + + runner = + CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); + runner.addCommand(command); + final MockProcess mockLintProcess = MockProcess(); + mockLintProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockLintProcess; + processRunner.recordedCalls.clear(); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('only runs on macOS', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + when(mockPlatform.isMacOS).thenReturn(false); + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + equals([]), + ); + }); + + test('runs pod lib lint on a podspec', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['ios', 'plugin1.podspec'], + ['bogus.dart'], // Ignore non-podspecs. + ]); + + processRunner.resultStdout = 'Foo'; + processRunner.resultStderr = 'Bar'; + + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('skips podspecs with known issues', () async { + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'] + ]); + createFakePlugin('plugin2', withExtraFiles: >[ + ['plugin2.podspec'] + ]); + + await runner + .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ]), + ); + }); + + test('skips analyzer for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--no-analyze=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + ], + mockPackagesDir.path), + ]), + ); + + expect(printedMessages, contains('Linting plugin1.podspec')); + }); + + test('allow warnings for podspecs with known warnings', () async { + Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--ignore-warnings=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + '--use-libraries' + ], + mockPackagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--allow-warnings', + '--analyze', + ], + mockPackagesDir.path), + ]), + ); + + expect( + printedMessages, contains('Linting and analyzing plugin1.podspec')); + }); + }); +} + +class MockPlatform extends Mock implements Platform {} diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart new file mode 100644 index 00000000000..478625283dd --- /dev/null +++ b/script/tool/test/list_command_test.dart @@ -0,0 +1,198 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/list_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$ListCommand', () { + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); + + runner = CommandRunner('list_test', 'Test for $ListCommand'); + runner.addCommand(command); + }); + + test('lists plugins', () async { + createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + + final List plugins = + await runCapturingPrint(runner, ['list', '--type=plugin']); + + expect( + plugins, + orderedEquals([ + '/packages/plugin1', + '/packages/plugin2', + ]), + ); + + cleanupPackages(); + }); + + test('lists examples', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=example']); + + expect( + examples, + orderedEquals([ + '/packages/plugin1/example', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + ]), + ); + + cleanupPackages(); + }); + + test('lists packages', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List packages = + await runCapturingPrint(runner, ['list', '--type=package']); + + expect( + packages, + unorderedEquals([ + '/packages/plugin1', + '/packages/plugin1/example', + '/packages/plugin2', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + '/packages/plugin3', + ]), + ); + + cleanupPackages(); + }); + + test('lists files', () async { + createFakePlugin('plugin1', withSingleExample: true); + createFakePlugin('plugin2', + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3'); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=file']); + + expect( + examples, + unorderedEquals([ + '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/example/pubspec.yaml', + '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/example/example1/pubspec.yaml', + '/packages/plugin2/example/example2/pubspec.yaml', + '/packages/plugin3/pubspec.yaml', + ]), + ); + + cleanupPackages(); + }); + + test('lists plugins using federated plugin layout', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + // Test without specifying `--type`. + final List plugins = + await runCapturingPrint(runner, ['list']); + + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + cleanupPackages(); + }); + + test('can filter plugins with the --plugins argument', () async { + createFakePlugin('plugin1'); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory federatedPlugin = + mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory clientLibrary = + federatedPlugin.childDirectory('my_plugin')..createSync(); + createFakePubspec(clientLibrary); + final Directory webLibrary = + federatedPlugin.childDirectory('my_plugin_web')..createSync(); + createFakePubspec(webLibrary); + final Directory macLibrary = + federatedPlugin.childDirectory('my_plugin_macos')..createSync(); + createFakePubspec(macLibrary); + + List plugins = await runCapturingPrint( + runner, ['list', '--plugins=plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin/my_plugin_web']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin_web', + ]), + ); + + plugins = await runCapturingPrint(runner, + ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin_web', + ]), + ); + }); + }); +} diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart new file mode 100644 index 00000000000..3e17ff8efd3 --- /dev/null +++ b/script/tool/test/mocks.dart @@ -0,0 +1,33 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:mockito/mockito.dart'; + +class MockProcess extends Mock implements io.Process { + final Completer exitCodeCompleter = Completer(); + final StreamController> stdoutController = + StreamController>(); + final StreamController> stderrController = + StreamController>(); + final MockIOSink stdinMock = MockIOSink(); + + @override + Future get exitCode => exitCodeCompleter.future; + + @override + Stream> get stdout => stdoutController.stream; + + @override + Stream> get stderr => stderrController.stream; + + @override + IOSink get stdin => stdinMock; +} + +class MockIOSink extends Mock implements IOSink { + List lines = []; + + @override + void writeln([Object obj = ""]) => lines.add(obj); +} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart new file mode 100644 index 00000000000..ada4bf08fd7 --- /dev/null +++ b/script/tool/test/publish_plugin_command_test.dart @@ -0,0 +1,378 @@ +import 'dart:async'; +import 'dart:convert'; +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/publish_plugin_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:git/git.dart'; +import 'package:matcher/matcher.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + const String testPluginName = 'foo'; + final List printedMessages = []; + + Directory parentDir; + Directory pluginDir; + GitDir gitDir; + TestProcessRunner processRunner; + CommandRunner commandRunner; + MockStdin mockStdin; + + setUp(() async { + // This test uses a local file system instead of an in memory one throughout + // so that git actually works. In setup we initialize a mono repo of plugins + // with one package and commit everything to Git. + parentDir = const LocalFileSystem() + .systemTempDirectory + .createTempSync('publish_plugin_command_test-'); + initializeFakePackages(parentDir: parentDir); + pluginDir = createFakePlugin(testPluginName, withSingleExample: false); + assert(pluginDir != null && pluginDir.existsSync()); + createFakePubspec(pluginDir, includeVersion: true); + io.Process.runSync('git', ['init'], + workingDirectory: mockPackagesDir.path); + gitDir = await GitDir.fromExisting(mockPackagesDir.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Initial commit']); + processRunner = TestProcessRunner(); + mockStdin = MockStdin(); + commandRunner = CommandRunner('tester', '') + ..addCommand(PublishPluginCommand( + mockPackagesDir, const LocalFileSystem(), + processRunner: processRunner, + print: (Object message) => printedMessages.add(message.toString()), + stdinput: mockStdin)); + }); + + tearDown(() { + parentDir.deleteSync(recursive: true); + printedMessages.clear(); + }); + + group('Initial validation', () { + test('requires a package flag', () async { + await expectLater(() => commandRunner.run(['publish-plugin']), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, contains("Must specify a package to publish.")); + }); + + test('requires an existing flag', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', 'iamerror']), + throwsA(const TypeMatcher())); + + expect(printedMessages.last, contains('iamerror does not exist')); + }); + + test('refuses to proceed with dirty files', () async { + pluginDir.childFile('tmp').createSync(); + + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect( + printedMessages.last, + contains( + "There are files in the package directory that haven't been saved in git.")); + }); + + test('fails immediately if the remote doesn\'t exist', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + + expect(processRunner.results.last.stderr, contains("No such remote")); + }); + + test("doesn't validate the remote if it's not pushing tags", () async { + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + + expect(printedMessages.last, 'Done!'); + }); + + test('can publish non-flutter package', () async { + createFakePubspec(pluginDir, includeVersion: true, isFlutter: false); + io.Process.runSync('git', ['init'], + workingDirectory: mockPackagesDir.path); + gitDir = await GitDir.fromExisting(mockPackagesDir.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Initial commit']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + expect(printedMessages.last, 'Done!'); + }); + }); + + group('Publishes package', () { + test('while showing all output from pub publish to the user', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + processRunner.mockPublishProcess.stdoutController.add(utf8.encode('Foo')); + processRunner.mockPublishProcess.stderrController.add(utf8.encode('Bar')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('forwards input from the user to `pub publish`', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + mockStdin.controller.add(utf8.encode('user input')); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + await publishCommand; + + expect(processRunner.mockPublishProcess.stdinMock.lines, + contains('user input')); + }); + + test('forwards --pub-publish-flags to pub publish', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + '--pub-publish-flags', + '--dry-run,--server=foo' + ]); + + expect(processRunner.mockPublishArgs.length, 4); + expect(processRunner.mockPublishArgs[0], 'pub'); + expect(processRunner.mockPublishArgs[1], 'publish'); + expect(processRunner.mockPublishArgs[2], '--dry-run'); + expect(processRunner.mockPublishArgs[3], '--server=foo'); + }); + + test('throws if pub publish fails', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + }); + }); + + group('Tags release', () { + test('with the version and name from the pubspec.yaml', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]); + + final String tag = + (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) + .stdout; + expect(tag, isNotEmpty); + }); + + test('only if publishing succeeded', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains("Publish failed. Exiting.")); + final String tag = (await gitDir.runCommand( + ['show-ref', 'fake_package-v0.0.1'], + throwOnError: false)) + .stdout; + expect(tag, isEmpty); + }); + }); + + group('Pushes tags', () { + setUp(() async { + await gitDir.runCommand( + ['remote', 'add', 'upstream', 'http://localhost:8000']); + }); + + test('requires user confirmation', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'help'; + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains('Tag push canceled.')); + }); + + test('to upstream by default', () async { + await gitDir.runCommand(['tag', 'garbage']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('to different remotes based on a flag', () async { + await gitDir.runCommand( + ['remote', 'add', 'origin', 'http://localhost:8001']); + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--remote', + 'origin', + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'origin'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('only if tagging and pushing to remotes are both enabled', () async { + processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-tag-release', + ]); + + expect(processRunner.pushTagsArgs.isEmpty, isTrue); + expect(printedMessages.last, 'Done!'); + }); + }); +} + +class TestProcessRunner extends ProcessRunner { + final List results = []; + final MockProcess mockPublishProcess = MockProcess(); + final List mockPublishArgs = []; + final MockProcessResult mockPushTagsResult = MockProcessResult(); + final List pushTagsArgs = []; + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + // Don't ever really push tags. + if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { + pushTagsArgs.addAll(args); + return mockPushTagsResult; + } + + final io.ProcessResult result = io.Process.runSync(executable, args, + workingDirectory: workingDir?.path); + results.add(result); + if (result.exitCode != 0) { + throw ToolExit(result.exitCode); + } + return result; + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + /// Never actually publish anything. Start is always and only used for this + /// since it returns something we can route stdin through. + assert(executable == 'flutter' && + args.isNotEmpty && + args[0] == 'pub' && + args[1] == 'publish'); + mockPublishArgs.addAll(args); + return mockPublishProcess; + } +} + +class MockStdin extends Mock implements io.Stdin { + final StreamController> controller = StreamController>(); + String readLineOutput; + + @override + Stream transform(StreamTransformer streamTransformer) { + return controller.stream.transform(streamTransformer); + } + + @override + StreamSubscription> listen(void onData(List event), + {Function onError, void onDone(), bool cancelOnError}) { + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + String readLineSync( + {Encoding encoding = io.systemEncoding, + bool retainNewlines = false}) => + readLineOutput; +} + +class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart new file mode 100644 index 00000000000..514e4c27190 --- /dev/null +++ b/script/tool/test/test_command_test.dart @@ -0,0 +1,154 @@ +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/test_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$TestCommand', () { + CommandRunner runner; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + + setUp(() { + initializeFakePackages(); + final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + processRunner.recordedCalls.clear(); + }); + + test('runs flutter test on each plugin', () async { + final Directory plugin1Dir = + createFakePlugin('plugin1', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin1Dir.path), + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('skips testing plugins without test directory', () async { + createFakePlugin('plugin1'); + final Directory plugin2Dir = + createFakePlugin('plugin2', withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs pub run test on non-Flutter packages', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + + test('runs on Chrome for web plugins', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ], + isFlutter: true, + isWebPlugin: true, + ); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', + ['test', '--color', '--platform=chrome'], pluginDir.path), + ]), + ); + }); + + test('enable-experiment flag', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall( + 'pub', + ['run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + + cleanupPackages(); + }); + }); +} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart new file mode 100644 index 00000000000..ec0000d13f3 --- /dev/null +++ b/script/tool/test/util.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:platform/platform.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:quiver/collection.dart'; + +FileSystem mockFileSystem = MemoryFileSystem( + style: LocalPlatform().isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix); +Directory mockPackagesDir; + +/// Creates a mock packages directory in the mock file system. +/// +/// If [parentDir] is set the mock packages dir will be creates as a child of +/// it. If not [mockFileSystem] will be used instead. +void initializeFakePackages({Directory parentDir}) { + mockPackagesDir = + (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); + mockPackagesDir.createSync(); +} + +/// Creates a plugin package with the given [name] in [mockPackagesDir]. +Directory createFakePlugin( + String name, { + bool withSingleExample = false, + List withExamples = const [], + List> withExtraFiles = const >[], + bool isFlutter = true, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, + String parentDirectoryName = '', +}) { + assert(!(withSingleExample && withExamples.isNotEmpty), + 'cannot pass withSingleExample and withExamples simultaneously'); + + final Directory pluginDirectory = (parentDirectoryName != '') + ? mockPackagesDir.childDirectory(parentDirectoryName).childDirectory(name) + : mockPackagesDir.childDirectory(name); + pluginDirectory.createSync(recursive: true); + + createFakePubspec( + pluginDirectory, + name: name, + isFlutter: isFlutter, + isAndroidPlugin: isAndroidPlugin, + isIosPlugin: isIosPlugin, + isWebPlugin: isWebPlugin, + isLinuxPlugin: isLinuxPlugin, + isMacOsPlugin: isMacOsPlugin, + isWindowsPlugin: isWindowsPlugin, + ); + + if (withSingleExample) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + createFakePubspec(exampleDir, + name: "${name}_example", isFlutter: isFlutter); + } else if (withExamples.isNotEmpty) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + for (String example in withExamples) { + final Directory currentExample = exampleDir.childDirectory(example) + ..createSync(); + createFakePubspec(currentExample, name: example, isFlutter: isFlutter); + } + } + + for (List file in withExtraFiles) { + final List newFilePath = [pluginDirectory.path] + ..addAll(file); + final File newFile = + mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); + newFile.createSync(recursive: true); + } + + return pluginDirectory; +} + +/// Creates a `pubspec.yaml` file with a flutter dependency. +void createFakePubspec( + Directory parent, { + String name = 'fake_package', + bool isFlutter = true, + bool includeVersion = false, + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, +}) { + parent.childFile('pubspec.yaml').createSync(); + String yaml = ''' +name: $name +flutter: + plugin: + platforms: +'''; + if (isAndroidPlugin) { + yaml += ''' + android: + package: io.flutter.plugins.fake + pluginClass: FakePlugin +'''; + } + if (isIosPlugin) { + yaml += ''' + ios: + pluginClass: FLTFakePlugin +'''; + } + if (isWebPlugin) { + yaml += ''' + web: + pluginClass: FakePlugin + fileName: ${name}_web.dart +'''; + } + if (isLinuxPlugin) { + yaml += ''' + linux: + pluginClass: FakePlugin +'''; + } + if (isMacOsPlugin) { + yaml += ''' + macos: + pluginClass: FakePlugin +'''; + } + if (isWindowsPlugin) { + yaml += ''' + windows: + pluginClass: FakePlugin +'''; + } + if (isFlutter) { + yaml += ''' +dependencies: + flutter: + sdk: flutter +'''; + } + if (includeVersion) { + yaml += ''' +version: 0.0.1 +publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. +'''; + } + parent.childFile('pubspec.yaml').writeAsStringSync(yaml); +} + +/// Cleans up the mock packages directory, making it an empty directory again. +void cleanupPackages() { + mockPackagesDir.listSync().forEach((FileSystemEntity entity) { + entity.deleteSync(recursive: true); + }); +} + +/// Run the command [runner] with the given [args] and return +/// what was printed. +Future> runCapturingPrint( + CommandRunner runner, List args) async { + final List prints = []; + final ZoneSpecification spec = ZoneSpecification( + print: (_, __, ___, String message) { + prints.add(message); + }, + ); + await Zone.current + .fork(specification: spec) + .run>(() => runner.run(args)); + + return prints; +} + +/// A mock [ProcessRunner] which records process calls. +class RecordingProcessRunner extends ProcessRunner { + io.Process processToReturn; + final List recordedCalls = []; + + /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. + String resultStdout; + + /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. + String resultStderr; + + @override + Future runAndStream( + String executable, + List args, { + Directory workingDir, + bool exitOnError = false, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + return Future.value( + processToReturn == null ? 0 : await processToReturn.exitCode); + } + + /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. + @override + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + io.ProcessResult result; + if (processToReturn != null) { + result = io.ProcessResult( + processToReturn.pid, + await processToReturn.exitCode, + resultStdout ?? processToReturn.stdout, + resultStderr ?? processToReturn.stderr); + } + return Future.value(result); + } + + @override + Future start(String executable, List args, + {Directory workingDirectory}) async { + recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); + return Future.value(processToReturn); + } +} + +/// A recorded process call. +class ProcessCall { + const ProcessCall(this.executable, this.args, this.workingDir); + + /// The executable that was called. + final String executable; + + /// The arguments passed to [executable] in the call. + final List args; + + /// The working directory this process was called from. + final String workingDir; + + @override + bool operator ==(dynamic other) { + if (other is! ProcessCall) { + return false; + } + final ProcessCall otherCall = other; + return executable == otherCall.executable && + listsEqual(args, otherCall.args) && + workingDir == otherCall.workingDir; + } + + @override + int get hashCode => + executable?.hashCode ?? + 0 ^ args?.hashCode ?? + 0 ^ workingDir?.hashCode ?? + 0; + + @override + String toString() { + final List command = [executable]..addAll(args); + return '"${command.join(' ')}" in $workingDir'; + } +} diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart new file mode 100644 index 00000000000..b9ace3811bf --- /dev/null +++ b/script/tool/test/version_check_test.dart @@ -0,0 +1,319 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; +import "package:test/test.dart"; +import "package:flutter_plugin_tools/src/version_check_command.dart"; +import 'package:pub_semver/pub_semver.dart'; +import 'util.dart'; + +void testAllowedVersion( + String masterVersion, + String headVersion, { + bool allowed = true, + NextVersionType nextVersionType, +}) { + final Version master = Version.parse(masterVersion); + final Version head = Version.parse(headVersion); + final Map allowedVersions = + getAllowedNextVersions(master, head); + if (allowed) { + expect(allowedVersions, contains(head)); + if (nextVersionType != null) { + expect(allowedVersions[head], equals(nextVersionType)); + } + } else { + expect(allowedVersions, isNot(contains(head))); + } +} + +class MockGitDir extends Mock implements GitDir {} + +class MockProcessResult extends Mock implements ProcessResult {} + +void main() { + group('$VersionCheckCommand', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + List> gitDirCommands; + String gitDiffResponse; + Map gitShowResponses; + + setUp(() { + gitDirCommands = >[]; + gitDiffResponse = ''; + gitShowResponses = {}; + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0]); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'show') { + final String response = + gitShowResponses[invocation.positionalArguments[0][1]]; + when(mockProcessResult.stdout).thenReturn(response); + } + return Future.value(mockProcessResult); + }); + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final VersionCheckCommand command = VersionCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, gitDir: gitDir); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('allows valid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('denies invalid version', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + }; + final Future> result = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect(gitDirCommands[1].join(' '), + equals('show master:packages/plugin/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin/pubspec.yaml')); + }); + + test('gracefully handles missing pubspec.yaml', () async { + createFakePlugin('plugin'); + gitDiffResponse = "packages/plugin/pubspec.yaml"; + mockFileSystem.currentDirectory + .childDirectory('packages') + .childDirectory('plugin') + .childFile('pubspec.yaml') + .deleteSync(); + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(1)); + expect(gitDirCommands.first.join(' '), + equals('diff --name-only master HEAD')); + }); + + test('allows minor changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.1.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + expect( + output, + orderedEquals([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + + test('disallows breaking changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface'); + gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 2.0.0', + }; + final Future> output = runCapturingPrint( + runner, ['version-check', '--base_sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); + expect( + gitDirCommands[1].join(' '), + equals( + 'show master:packages/plugin_platform_interface/pubspec.yaml')); + expect(gitDirCommands[2].join(' '), + equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + }); + }); + + group("Pre 1.0", () { + test("nextVersion allows patch version", () { + testAllowedVersion("0.12.0", "0.12.0+1", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("0.12.0+4", "0.12.0+5", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow jumping patch", () { + testAllowedVersion("0.12.0", "0.12.0+2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+4", allowed: false); + }); + + test("nextVersion does not allow going back", () { + testAllowedVersion("0.12.0", "0.11.0", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.0+1", allowed: false); + testAllowedVersion("0.12.0+1", "0.12.0", allowed: false); + }); + + test("nextVersion allows minor version", () { + testAllowedVersion("0.12.0", "0.12.1", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("0.12.0+4", "0.12.1", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow jumping minor", () { + testAllowedVersion("0.12.0", "0.12.2", allowed: false); + testAllowedVersion("0.12.0+2", "0.12.3", allowed: false); + }); + }); + + group("Releasing 1.0", () { + test("nextVersion allows releasing 1.0", () { + testAllowedVersion("0.12.0", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("0.12.0+4", "1.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion does not allow jumping major", () { + testAllowedVersion("0.12.0", "2.0.0", allowed: false); + testAllowedVersion("0.12.0+4", "2.0.0", allowed: false); + }); + + test("nextVersion does not allow un-releasing", () { + testAllowedVersion("1.0.0", "0.12.0+4", allowed: false); + testAllowedVersion("1.0.0", "0.12.0", allowed: false); + }); + }); + + group("Post 1.0", () { + test("nextVersion allows patch jumps", () { + testAllowedVersion("1.0.1", "1.0.2", + nextVersionType: NextVersionType.PATCH); + testAllowedVersion("1.0.0", "1.0.1", + nextVersionType: NextVersionType.PATCH); + }); + + test("nextVersion does not allow build jumps", () { + testAllowedVersion("1.0.1", "1.0.1+1", allowed: false); + testAllowedVersion("1.0.0+5", "1.0.0+6", allowed: false); + }); + + test("nextVersion does not allow skipping patches", () { + testAllowedVersion("1.0.1", "1.0.3", allowed: false); + testAllowedVersion("1.0.0", "1.0.6", allowed: false); + }); + + test("nextVersion allows minor version jumps", () { + testAllowedVersion("1.0.1", "1.1.0", + nextVersionType: NextVersionType.MINOR); + testAllowedVersion("1.0.0", "1.1.0", + nextVersionType: NextVersionType.MINOR); + }); + + test("nextVersion does not allow skipping minor versions", () { + testAllowedVersion("1.0.1", "1.2.0", allowed: false); + testAllowedVersion("1.1.0", "1.3.0", allowed: false); + }); + + test("nextVersion allows breaking changes", () { + testAllowedVersion("1.0.1", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion("1.0.0", "2.0.0", + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test("nextVersion allows null safety pre prelease", () { + testAllowedVersion("1.0.1", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "2.0.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety", "1.0.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0-nullsafety.1", "1.0.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.2.0-nullsafety", + nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety", "0.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0-nullsafety.1", "0.1.0-nullsafety.2", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.0.0", "1.1.0-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("1.1.0-nullsafety", "1.1.0-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.0", "0.1.1-nullsafety", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + testAllowedVersion("0.1.1-nullsafety", "0.1.1-nullsafety.1", + nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); + }); + + test("nextVersion does not allow skipping major versions", () { + testAllowedVersion("1.0.1", "3.0.0", allowed: false); + testAllowedVersion("1.1.0", "2.3.0", allowed: false); + }); + }); +} diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart new file mode 100644 index 00000000000..007c2e12188 --- /dev/null +++ b/script/tool/test/xctest_command_test.dart @@ -0,0 +1,358 @@ +// Copyright 2017 The Chromium 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/xctest_command.dart'; +import 'package:test/test.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +final _kDeviceListMap = { + "runtimes": [ + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime", + "buildversion": "17A577", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-0", + "version": "13.0", + "isAvailable": true, + "name": "iOS 13.0" + }, + { + "bundlePath": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime", + "buildversion": "17L255", + "runtimeRoot": + "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-4", + "version": "13.4", + "isAvailable": true, + "name": "iOS 13.4" + }, + { + "bundlePath": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime", + "buildversion": "17T531", + "runtimeRoot": + "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot", + "identifier": "com.apple.CoreSimulator.SimRuntime.watchOS-6-2", + "version": "6.2.1", + "isAvailable": true, + "name": "watchOS 6.2" + } + ], + "devices": { + "com.apple.CoreSimulator.SimRuntime.iOS-13-4": [ + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "udid": "2706BBEB-1E01-403E-A8E9-70E8E5A24774", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8", + "state": "Shutdown", + "name": "iPhone 8" + }, + { + "dataPath": + "/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data", + "logPath": + "/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A", + "udid": "1E76A0FD-38AC-4537-A989-EA639D7D012A", + "isAvailable": true, + "deviceTypeIdentifier": + "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", + "state": "Shutdown", + "name": "iPhone 8 Plus" + } + ] + } +}; + +void main() { + const String _kDestination = '--ios-destination'; + const String _kTarget = '--target'; + const String _kSkip = '--skip'; + + group('test xctest_command', () { + CommandRunner runner; + RecordingProcessRunner processRunner; + + setUp(() { + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + final XCTestCommand command = XCTestCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner('xctest_command', 'Test for xctest_command'); + runner.addCommand(command); + cleanupPackages(); + }); + + test('Not specifying --target throws', () async { + await expectLater( + () => runner.run(['xctest', _kDestination, 'a_destination']), + throwsA(const TypeMatcher())); + }); + + test('skip if ios is not supported', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, contains('iOS is not supported by this plugin.')); + expect(processRunner.recordedCalls, orderedEquals([])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, did not find scheme', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = '{"project":{"targets":["bar_scheme"]}}'; + + await expectLater(() async { + final List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + expect(output, + contains('foo_scheme not configured for plugin, test failed.')); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ], + pluginExampleDirectory.path), + ])); + }, throwsA(const TypeMatcher())); + cleanupPackages(); + }); + + test('running with correct scheme and destination, found scheme', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination' + ]); + + expect(output, contains('Successfully ran xctest for plugin')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + + test('running with correct scheme and destination, skip 1 plugin', + () async { + createFakePlugin('plugin1', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + createFakePlugin('plugin2', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory1 = + mockPackagesDir.childDirectory('plugin1').childDirectory('example'); + createFakePubspec(pluginExampleDirectory1, isFlutter: true); + final Directory pluginExampleDirectory2 = + mockPackagesDir.childDirectory('plugin2').childDirectory('example'); + createFakePubspec(pluginExampleDirectory2, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + List output = await runCapturingPrint(runner, [ + 'xctest', + _kTarget, + 'foo_scheme', + _kDestination, + 'foo_destination', + _kSkip, + 'plugin1' + ]); + + expect(output, contains('plugin1 was skipped with the --skip flag.')); + expect(output, contains('Successfully ran xctest for plugin2')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory2.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'foo_destination', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory2.path), + ])); + + cleanupPackages(); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final Map schemeCommandResult = { + "project": { + "targets": ["bar_scheme", "foo_scheme"] + } + }; + // For simplicity of the test, we combine all the mock results into a single mock result, each internal command + // will get this result and they should still be able to parse them correctly. + processRunner.resultStdout = + jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + await runner.run([ + 'xctest', + _kTarget, + 'foo_scheme', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcodebuild', + ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], + pluginExampleDirectory.path), + ProcessCall( + 'xcodebuild', + [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'foo_scheme', + '-destination', + 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ], + pluginExampleDirectory.path), + ])); + + cleanupPackages(); + }); + }); +} From 3ea78093bf78a22a1f497758d9da06efd0b14c1e Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Tue, 2 Mar 2021 19:27:48 -0800 Subject: [PATCH 008/249] Run static analyzer during xctest (#3667) --- .../tool/lib/src/lint_podspecs_command.dart | 25 +-- script/tool/lib/src/xctest_command.dart | 104 +++++-------- .../tool/test/lint_podspecs_command_test.dart | 43 +----- script/tool/test/xctest_command_test.dart | 145 ++---------------- 4 files changed, 61 insertions(+), 256 deletions(-) diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 68fd4b61dd6..b10031162fe 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -14,8 +14,7 @@ import 'common.dart'; typedef void Print(Object object); -/// Lint the CocoaPod podspecs, run the static analyzer on iOS/macOS plugin -/// platform code, and run unit tests. +/// Lint the CocoaPod podspecs and run unit tests. /// /// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. class LintPodspecsCommand extends PluginCommand { @@ -35,10 +34,6 @@ class LintPodspecsCommand extends PluginCommand { help: 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', valueHelp: 'podspec_file_name'); - argParser.addMultiOption('no-analyze', - help: - 'Do not pass --analyze flag to "pod lib lint" for podspecs with this basename (example: plugins with known analyzer warnings)', - valueHelp: 'podspec_file_name'); } @override @@ -102,25 +97,17 @@ class LintPodspecsCommand extends PluginCommand { Future _lintPodspec(File podspec) async { // Do not run the static analyzer on plugins with known analyzer issues. final String podspecPath = podspec.path; - final bool runAnalyzer = !argResults['no-analyze'] - .contains(p.basenameWithoutExtension(podspecPath)); final String podspecBasename = p.basename(podspecPath); - if (runAnalyzer) { - _print('Linting and analyzing $podspecBasename'); - } else { - _print('Linting $podspecBasename'); - } + _print('Linting $podspecBasename'); // Lint plugin as framework (use_frameworks!). - final ProcessResult frameworkResult = await _runPodLint(podspecPath, - runAnalyzer: runAnalyzer, libraryLint: true); + final ProcessResult frameworkResult = await _runPodLint(podspecPath, libraryLint: true); _print(frameworkResult.stdout); _print(frameworkResult.stderr); // Lint plugin as library. - final ProcessResult libraryResult = await _runPodLint(podspecPath, - runAnalyzer: runAnalyzer, libraryLint: false); + final ProcessResult libraryResult = await _runPodLint(podspecPath, libraryLint: false); _print(libraryResult.stdout); _print(libraryResult.stderr); @@ -128,7 +115,7 @@ class LintPodspecsCommand extends PluginCommand { } Future _runPodLint(String podspecPath, - {bool runAnalyzer, bool libraryLint}) async { + {bool libraryLint}) async { final bool allowWarnings = argResults['ignore-warnings'] .contains(p.basenameWithoutExtension(podspecPath)); final List arguments = [ @@ -136,10 +123,10 @@ class LintPodspecsCommand extends PluginCommand { 'lint', podspecPath, if (allowWarnings) '--allow-warnings', - if (runAnalyzer) '--analyze', if (libraryLint) '--use-libraries' ]; + _print('Running "pod ${arguments.join(' ')}"'); return processRunner.run('pod', arguments, workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); } diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index d90b7a8fbfe..a4d03360b29 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -12,17 +12,15 @@ import 'package:path/path.dart' as p; import 'common.dart'; const String _kiOSDestination = 'ios-destination'; -const String _kTarget = 'target'; const String _kSkip = 'skip'; const String _kXcodeBuildCommand = 'xcodebuild'; const String _kXCRunCommand = 'xcrun'; const String _kFoundNoSimulatorsMessage = 'Cannot find any available simulators, tests failed'; -/// The command to run iOS' XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. -/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcodeproj". -/// The command takes a "-target" argument which has to match the target of the test target. -/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html +/// The command to run iOS XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. +/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcworkspace". +/// The static analyzer is also run. class XCTestCommand extends PluginCommand { XCTestCommand( Directory packagesDir, @@ -36,10 +34,6 @@ class XCTestCommand extends PluginCommand { 'this is passed to the `-destination` argument in xcodebuild command.\n' 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', ); - argParser.addOption(_kTarget, - help: 'The test target.\n' - 'This is the xcode project test target. This is passed to the `-scheme` argument in the xcodebuild command. \n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the scheme'); argParser.addMultiOption(_kSkip, help: 'Plugins to skip while running this command. \n'); } @@ -49,17 +43,10 @@ class XCTestCommand extends PluginCommand { @override final String description = 'Runs the xctests in the iOS example apps.\n\n' - 'This command requires "flutter" to be in your path.'; + 'This command requires "flutter" and "xcrun" to be in your path.'; @override Future run() async { - if (argResults[_kTarget] == null) { - // TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified. - // https://github.com/flutter/flutter/issues/68419 - print('--$_kTarget must be specified'); - throw ToolExit(1); - } - String destination = argResults[_kiOSDestination]; if (destination == null) { String simulatorId = await _findAvailableIphoneSimulator(); @@ -72,7 +59,6 @@ class XCTestCommand extends PluginCommand { checkSharding(); - final String target = argResults[_kTarget]; final List skipped = argResults[_kSkip]; List failingPackages = []; @@ -92,57 +78,14 @@ class XCTestCommand extends PluginCommand { continue; } for (Directory example in getExamplesForPlugin(plugin)) { - // Look for the test scheme in the example app. - print('Look for target named: $_kTarget ...'); - final List findSchemeArgs = [ - '-project', - 'ios/Runner.xcodeproj', - '-list', - '-json' - ]; - final String completeFindSchemeCommand = - '$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}'; - print(completeFindSchemeCommand); - final io.ProcessResult xcodeprojListResult = await processRunner - .run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example); - if (xcodeprojListResult.exitCode != 0) { - print('Error occurred while running "$completeFindSchemeCommand":\n' - '${xcodeprojListResult.stderr}'); - failingPackages.add(packageName); - print('\n\n'); - continue; - } - - final String xcodeprojListOutput = xcodeprojListResult.stdout; - Map xcodeprojListOutputJson = - jsonDecode(xcodeprojListOutput); - if (!xcodeprojListOutputJson['project']['targets'].contains(target)) { - failingPackages.add(packageName); - print('$target not configured for $packageName, test failed.'); - print( - 'Please check the scheme for the test target if it matches the name $target.\n' - 'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.'); - print('\n\n'); - continue; + // Running tests and static analyzer. + print('Running tests and analyzer for $packageName ...'); + int exitCode = await _runTests(true, destination, example); + // 66 = there is no test target (this fails fast). Try again with just the analyzer. + if (exitCode == 66) { + print('Tests not found for $packageName, running analyzer only...'); + exitCode = await _runTests(false, destination, example); } - // Found the scheme, running tests - print('Running XCTests:$target for $packageName ...'); - final List xctestArgs = [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - target, - '-destination', - destination, - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ]; - final String completeTestCommand = - '$_kXcodeBuildCommand ${xctestArgs.join(' ')}'; - print(completeTestCommand); - final int exitCode = await processRunner - .runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example); if (exitCode == 0) { print('Successfully ran xctest for $packageName'); } else { @@ -164,6 +107,31 @@ class XCTestCommand extends PluginCommand { } } + Future _runTests(bool runTests, String destination, Directory example) { + final List xctestArgs = [ + _kXcodeBuildCommand, + if (runTests) + 'test', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + '-destination', + destination, + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ]; + final String completeTestCommand = + '$_kXCRunCommand ${xctestArgs.join(' ')}'; + print(completeTestCommand); + return processRunner + .runAndStream(_kXCRunCommand, xctestArgs, workingDir: example, exitOnError: false); + } + Future _findAvailableIphoneSimulator() async { // Find the first available destination if not specified. final List findSimulatorsArguments = [ diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 49d6ad4d8e2..1014dcd170b 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -81,7 +81,6 @@ void main() { 'lib', 'lint', p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), - '--analyze', '--use-libraries' ], mockPackagesDir.path), @@ -91,14 +90,13 @@ void main() { 'lib', 'lint', p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), - '--analyze', ], mockPackagesDir.path), ]), ); expect( - printedMessages, contains('Linting and analyzing plugin1.podspec')); + printedMessages, contains('Linting plugin1.podspec')); expect(printedMessages, contains('Foo')); expect(printedMessages, contains('Bar')); }); @@ -122,41 +120,6 @@ void main() { ); }); - test('skips analyzer for podspecs with known warnings', () async { - Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); - - await runner.run(['podspecs', '--no-analyze=plugin1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - '--use-libraries' - ], - mockPackagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), - ], - mockPackagesDir.path), - ]), - ); - - expect(printedMessages, contains('Linting plugin1.podspec')); - }); - test('allow warnings for podspecs with known warnings', () async { Directory plugin1Dir = createFakePlugin('plugin1', withExtraFiles: >[ @@ -176,7 +139,6 @@ void main() { 'lint', p.join(plugin1Dir.path, 'plugin1.podspec'), '--allow-warnings', - '--analyze', '--use-libraries' ], mockPackagesDir.path), @@ -187,14 +149,13 @@ void main() { 'lint', p.join(plugin1Dir.path, 'plugin1.podspec'), '--allow-warnings', - '--analyze', ], mockPackagesDir.path), ]), ); expect( - printedMessages, contains('Linting and analyzing plugin1.podspec')); + printedMessages, contains('Linting plugin1.podspec')); }); }); } diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 007c2e12188..2b75ccde421 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -8,7 +8,6 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/xctest_command.dart'; import 'package:test/test.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; import 'mocks.dart'; import 'util.dart'; @@ -81,7 +80,6 @@ final _kDeviceListMap = { void main() { const String _kDestination = '--ios-destination'; - const String _kTarget = '--target'; const String _kSkip = '--skip'; group('test xctest_command', () { @@ -100,12 +98,6 @@ void main() { cleanupPackages(); }); - test('Not specifying --target throws', () async { - await expectLater( - () => runner.run(['xctest', _kDestination, 'a_destination']), - throwsA(const TypeMatcher())); - }); - test('skip if ios is not supported', () async { createFakePlugin('plugin', withExtraFiles: >[ @@ -123,8 +115,6 @@ void main() { processRunner.processToReturn = mockProcess; final List output = await runCapturingPrint(runner, [ 'xctest', - _kTarget, - 'foo_scheme', _kDestination, 'foo_destination' ]); @@ -134,106 +124,7 @@ void main() { cleanupPackages(); }); - test('running with correct scheme and destination, did not find scheme', - () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = '{"project":{"targets":["bar_scheme"]}}'; - - await expectLater(() async { - final List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - expect(output, - contains('foo_scheme not configured for plugin, test failed.')); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('xcrun', ['simctl', 'list', '--json'], null), - ProcessCall( - 'xcodebuild', - [ - '-project', - 'ios/Runner.xcodeproj', - '-list', - '-json' - ], - pluginExampleDirectory.path), - ])); - }, throwsA(const TypeMatcher())); - cleanupPackages(); - }); - - test('running with correct scheme and destination, found scheme', () async { - createFakePlugin('plugin', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; - List output = await runCapturingPrint(runner, [ - 'xctest', - _kTarget, - 'foo_scheme', - _kDestination, - 'foo_destination' - ]); - - expect(output, contains('Successfully ran xctest for plugin')); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory.path), - ProcessCall( - 'xcodebuild', - [ - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'foo_scheme', - '-destination', - 'foo_destination', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' - ], - pluginExampleDirectory.path), - ])); - - cleanupPackages(); - }); - - test('running with correct scheme and destination, skip 1 plugin', + test('running with correct destination, skip 1 plugin', () async { createFakePlugin('plugin1', withExtraFiles: >[ @@ -260,8 +151,6 @@ void main() { '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; List output = await runCapturingPrint(runner, [ 'xctest', - _kTarget, - 'foo_scheme', _kDestination, 'foo_destination', _kSkip, @@ -275,21 +164,22 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory2.path), - ProcessCall( - 'xcodebuild', + 'xcrun', [ + 'xcodebuild', 'test', + 'analyze', '-workspace', 'ios/Runner.xcworkspace', + '-configuration', + 'Debug', '-scheme', - 'foo_scheme', + 'Runner', '-destination', 'foo_destination', 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' + 'CODE_SIGNING_REQUIRED=NO', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory2.path), ])); @@ -324,8 +214,6 @@ void main() { jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); await runner.run([ 'xctest', - _kTarget, - 'foo_scheme', ]); expect( @@ -333,21 +221,22 @@ void main() { orderedEquals([ ProcessCall('xcrun', ['simctl', 'list', '--json'], null), ProcessCall( - 'xcodebuild', - ['-project', 'ios/Runner.xcodeproj', '-list', '-json'], - pluginExampleDirectory.path), - ProcessCall( - 'xcodebuild', + 'xcrun', [ + 'xcodebuild', 'test', + 'analyze', '-workspace', 'ios/Runner.xcworkspace', + '-configuration', + 'Debug', '-scheme', - 'foo_scheme', + 'Runner', '-destination', 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO' + 'CODE_SIGNING_REQUIRED=NO', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), ])); From c042ad3628b411e5ecd48f78eadad19678977302 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Wed, 3 Mar 2021 10:56:12 -0800 Subject: [PATCH 009/249] Adopt Xcode 12 for podspec lints (#3653) --- script/tool/lib/src/lint_podspecs_command.dart | 1 + script/tool/test/lint_podspecs_command_test.dart | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index b10031162fe..749d67ee5f1 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -122,6 +122,7 @@ class LintPodspecsCommand extends PluginCommand { 'lib', 'lint', podspecPath, + '--configuration=Debug', // Release targets unsupported arm64 simulators. Use Debug to only build against targeted x86_64 simulator devices. if (allowWarnings) '--allow-warnings', if (libraryLint) '--use-libraries' ]; diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 1014dcd170b..e0411d5cffd 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -81,6 +81,7 @@ void main() { 'lib', 'lint', p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--configuration=Debug', '--use-libraries' ], mockPackagesDir.path), @@ -90,6 +91,7 @@ void main() { 'lib', 'lint', p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--configuration=Debug', ], mockPackagesDir.path), ]), @@ -138,6 +140,7 @@ void main() { 'lib', 'lint', p.join(plugin1Dir.path, 'plugin1.podspec'), + '--configuration=Debug', '--allow-warnings', '--use-libraries' ], @@ -148,6 +151,7 @@ void main() { 'lib', 'lint', p.join(plugin1Dir.path, 'plugin1.podspec'), + '--configuration=Debug', '--allow-warnings', ], mockPackagesDir.path), From 373cf772f098a417fa24daddf9c073f032baea64 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 4 Mar 2021 13:59:21 -0800 Subject: [PATCH 010/249] Update CI config for Flutter 2 (#3674) Includes cleanup to simplify our setup. Major changes: - Eliminate the NNBD plugin filtering for stable. - Remove the temporarily-added beta branch testing. - Enable Linux, macOS, and web on stable (Windows is LUCI-based) - Combine the two different macOS matrix configurations now that they are the same. - Combine the two different Linux matrix configurations by using a single Dockerfile (which now also includes clang-format) - The web integration smoke test temporarily still uses the old Dockerfile, now renamed, because the driver installer script doesn't support Chrome 89 yet. - Move most of the Linux tasks to lower-CPU machines to allow more tasks to run in parallel without hitting the community limit. - Reorder the tasks slightly and give them comments to identify platform groupings - Enabled web "build all plugins together" and "build all examples" tests --- .../tool/lib/src/build_examples_command.dart | 51 ++++++++----- script/tool/lib/src/format_command.dart | 6 +- .../test/build_examples_command_test.dart | 74 ++++++++++++++++++- 3 files changed, 105 insertions(+), 26 deletions(-) diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 53da9086aba..bb140bd429c 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -19,6 +19,7 @@ class BuildExamplesCommand extends PluginCommand { }) : super(packagesDir, fileSystem, processRunner: processRunner) { argParser.addFlag(kLinux, defaultsTo: false); argParser.addFlag(kMacos, defaultsTo: false); + argParser.addFlag(kWeb, defaultsTo: false); argParser.addFlag(kWindows, defaultsTo: false); argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); argParser.addFlag(kApk); @@ -43,10 +44,10 @@ class BuildExamplesCommand extends PluginCommand { !argResults[kApk] && !argResults[kLinux] && !argResults[kMacos] && + !argResults[kWeb] && !argResults[kWindows]) { - print( - 'None of --linux, --macos, --windows, --apk nor --ipa were specified, ' - 'so not building anything.'); + print('None of --linux, --macos, --web, --windows, --apk, or --ipa were ' + 'specified, so not building anything.'); return; } final String flutterCommand = @@ -84,33 +85,43 @@ class BuildExamplesCommand extends PluginCommand { if (argResults[kMacos]) { print('\nBUILDING macOS for $packageName'); if (isMacOsPlugin(plugin, fileSystem)) { - // TODO(https://github.com/flutter/flutter/issues/46236): - // Builing macos without running flutter pub get first results - // in an error. int exitCode = await processRunner.runAndStream( - flutterCommand, ['pub', 'get'], + flutterCommand, + [ + 'build', + kMacos, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], workingDir: example); if (exitCode != 0) { failingPackages.add('$packageName (macos)'); - } else { - exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kMacos, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (macos)'); - } } } else { print('macOS is not supported by this plugin'); } } + if (argResults[kWeb]) { + print('\nBUILDING web for $packageName'); + if (isWebPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kWeb, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (web)'); + } + } else { + print('Web is not supported by this plugin'); + } + } + if (argResults[kWindows]) { print('\nBUILDING Windows for $packageName'); if (isWindowsPlugin(plugin, fileSystem)) { diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index ec326b96c1f..e1c14e04cfe 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -22,10 +22,10 @@ class FormatCommand extends PluginCommand { FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addFlag('travis', hide: true); + argParser.addFlag('fail-on-change', hide: true); argParser.addOption('clang-format', defaultsTo: 'clang-format', - help: 'Path to executable of clang-format v5.'); + help: 'Path to executable of clang-format.'); } @override @@ -46,7 +46,7 @@ class FormatCommand extends PluginCommand { await _formatJava(googleFormatterPath); await _formatCppAndObjectiveC(); - if (argResults['travis']) { + if (argResults['fail-on-change']) { final bool modified = await _didModifyAnything(); if (modified) { throw ToolExit(1); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index eaf5049dcc0..65417525d71 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -201,7 +201,7 @@ void main() { output, orderedEquals([ '\nBUILDING macOS for $packageName', - '\macOS is not supported by this plugin', + 'macOS is not supported by this plugin', '\n\n', 'All builds successful!', ]), @@ -213,6 +213,7 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); cleanupPackages(); }); + test('building for macos', () async { createFakePlugin('plugin', withExtraFiles: >[ @@ -244,14 +245,81 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, ['pub', 'get'], - pluginExampleDirectory.path), ProcessCall(flutterCommand, ['build', 'macos'], pluginExampleDirectory.path), ])); cleanupPackages(); }); + test('building for web with no implementation results in no-op', () async { + createFakePlugin('plugin', withExtraFiles: >[ + ['example', 'test'], + ]); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--web']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING web for $packageName', + 'Web is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + cleanupPackages(); + }); + + test('building for web', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test'], + ['example', 'web', 'index.html'], + ], + isWebPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--web']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING web for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, ['build', 'web'], + pluginExampleDirectory.path), + ])); + cleanupPackages(); + }); + test( 'building for Windows when plugin is not set up for Windows results in no-op', () async { From 01a4eca6e70c9402686e146c45e212d2f7f71542 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Mon, 8 Mar 2021 18:05:00 -0800 Subject: [PATCH 011/249] Skip pod lint tests (#3692) --- script/tool/lib/src/lint_podspecs_command.dart | 1 + script/tool/test/lint_podspecs_command_test.dart | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 749d67ee5f1..13de64415e9 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -123,6 +123,7 @@ class LintPodspecsCommand extends PluginCommand { 'lint', podspecPath, '--configuration=Debug', // Release targets unsupported arm64 simulators. Use Debug to only build against targeted x86_64 simulator devices. + '--skip-tests', if (allowWarnings) '--allow-warnings', if (libraryLint) '--use-libraries' ]; diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index e0411d5cffd..1c59d2d7e55 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -82,6 +82,7 @@ void main() { 'lint', p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), '--configuration=Debug', + '--skip-tests', '--use-libraries' ], mockPackagesDir.path), @@ -92,6 +93,7 @@ void main() { 'lint', p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), '--configuration=Debug', + '--skip-tests', ], mockPackagesDir.path), ]), @@ -141,6 +143,7 @@ void main() { 'lint', p.join(plugin1Dir.path, 'plugin1.podspec'), '--configuration=Debug', + '--skip-tests', '--allow-warnings', '--use-libraries' ], @@ -152,6 +155,7 @@ void main() { 'lint', p.join(plugin1Dir.path, 'plugin1.podspec'), '--configuration=Debug', + '--skip-tests', '--allow-warnings', ], mockPackagesDir.path), From c415eebb5bce1df3ab8a7cf853751348bdac88e1 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 10 Mar 2021 14:15:04 -0800 Subject: [PATCH 012/249] [tool] Improve check version ci so that it enforces the version in CHANGELOG and pubspec matches. (#3678) --- script/tool/lib/src/common.dart | 143 ++++++++++- .../tool/lib/src/version_check_command.dart | 139 ++++++----- script/tool/test/common_test.dart | 227 +++++++++++++++++- script/tool/test/util.dart | 19 +- script/tool/test/version_check_test.dart | 160 +++++++++++- 5 files changed, 611 insertions(+), 77 deletions(-) diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index 08622df281b..ce4f37873b0 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -7,8 +7,12 @@ 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:meta/meta.dart'; import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; import 'package:yaml/yaml.dart'; typedef void Print(Object object); @@ -140,6 +144,13 @@ bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { return pluginSupportsPlatform(kLinux, entity, fileSystem); } +/// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red. +void printErrorAndExit({@required String errorMessage, int exitCode = 1}) { + final Colorize redError = Colorize(errorMessage)..red(); + print(redError); + throw ToolExit(exitCode); +} + /// Error thrown when a command needs to exit with a non-zero exit code. class ToolExit extends Error { ToolExit(this.exitCode); @@ -152,6 +163,7 @@ abstract class PluginCommand extends Command { this.packagesDir, this.fileSystem, { this.processRunner = const ProcessRunner(), + this.gitDir, }) { argParser.addMultiOption( _pluginsArg, @@ -179,12 +191,23 @@ abstract class PluginCommand extends Command { 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' + '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; @@ -199,6 +222,11 @@ abstract class PluginCommand extends Command { /// 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; @@ -273,9 +301,13 @@ abstract class PluginCommand extends Command { /// "client library" package, which declares the API for the plugin, as /// well as one or more platform-specific implementations. Stream _getAllPlugins() async* { - final Set plugins = Set.from(argResults[_pluginsArg]); + Set plugins = Set.from(argResults[_pluginsArg]); final Set excludedPlugins = Set.from(argResults[_excludeArg]); + final bool runOnChangedPackages = argResults[_runOnChangedPackagesArg]; + if (plugins.isEmpty && runOnChangedPackages) { + plugins = await _getChangedPackages(); + } await for (FileSystemEntity entity in packagesDir.list(followLinks: false)) { @@ -363,6 +395,50 @@ abstract class PluginCommand extends Command { (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) .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; + String baseSha = argResults[_kBaseSha]; + + GitDir baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + printErrorAndExit( + errorMessage: '$rootDir is not a valid Git repository.', + exitCode: 2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + return gitVersionFinder; + } + + Future> _getChangedPackages() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + + final List allChangedFiles = + await gitVersionFinder.getChangedFiles(); + final Set packages = {}; + allChangedFiles.forEach((String path) { + final List pathComponents = path.split('/'); + final int packagesIndex = + pathComponents.indexWhere((String element) => element == 'packages'); + if (packagesIndex != -1) { + packages.add(pathComponents[packagesIndex + 1]); + } + }); + if (packages.isNotEmpty) { + final String changedPackages = packages.join(','); + print(changedPackages); + } + print('No changed packages.'); + return packages; + } } /// A class used to run processes. @@ -466,3 +542,68 @@ class ProcessRunner { return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; } } + +/// 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((element) => element.isEmpty); + return changedFiles.toList(); + } + + /// Get the package version specified in the pubspec file in `pubspecPath` and at the revision of `gitRef`. + Future getPackageVersion(String pubspecPath, String gitRef) async { + final io.ProcessResult gitShow = + await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); + final String fileContent = gitShow.stdout; + final String versionString = loadYaml(fileContent)['version']; + 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 == null || + 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/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 2c6b92bbcb7..111239f0399 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -6,43 +6,14 @@ import 'dart:async'; import 'dart:io' as io; import 'package:meta/meta.dart'; -import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:yaml/yaml.dart'; import 'common.dart'; -const String _kBaseSha = 'base_sha'; - -class GitVersionFinder { - GitVersionFinder(this.baseGitDir, this.baseSha); - - final GitDir baseGitDir; - final String baseSha; - - static bool isPubspec(String file) { - return file.trim().endsWith('pubspec.yaml'); - } - - Future> getChangedPubSpecs() async { - final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); - final List changedFiles = - changedFilesCommand.stdout.toString().split('\n'); - return changedFiles.where(isPubspec).toList(); - } - - Future getPackageVersion(String pubspecPath, String gitRef) async { - final io.ProcessResult gitShow = - await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); - final String fileContent = gitShow.stdout; - final String versionString = loadYaml(fileContent)['version']; - return versionString == null ? null : Version.parse(versionString); - } -} +const String _kBaseSha = 'base-sha'; enum NextVersionType { BREAKING_MAJOR, @@ -128,46 +99,28 @@ class VersionCheckCommand extends PluginCommand { Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), - this.gitDir, - }) : super(packagesDir, fileSystem, processRunner: processRunner) { - argParser.addOption(_kBaseSha); - } - - /// The git directory to use. By default it uses the parent directory. - /// - /// This can be mocked for testing. - final GitDir gitDir; + GitDir gitDir, + }) : super(packagesDir, fileSystem, + processRunner: processRunner, gitDir: gitDir); @override final String name = 'version-check'; @override final String description = - 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' + 'Checks if the versions of the plugins have been incremented per pub specification.\n' + 'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n' 'This command requires "pub" and "flutter" to be in your path.'; @override Future run() async { checkSharding(); - - final String rootDir = packagesDir.parent.absolute.path; - final String baseSha = argResults[_kBaseSha]; - - GitDir baseGitDir = gitDir; - if (baseGitDir == null) { - if (!await GitDir.isGitDir(rootDir)) { - print('$rootDir is not a valid Git repository.'); - throw ToolExit(2); - } - baseGitDir = await GitDir.fromExisting(rootDir); - } - - final GitVersionFinder gitVersionFinder = - GitVersionFinder(baseGitDir, baseSha); + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); + final String baseSha = argResults[_kBaseSha]; for (final String pubspecPath in changedPubspecs) { try { final File pubspecFile = fileSystem.file(pubspecPath); @@ -194,9 +147,7 @@ class VersionCheckCommand extends PluginCommand { final String error = '$pubspecPath incorrectly updated version.\n' 'HEAD: $headVersion, master: $masterVersion.\n' 'Allowed versions: $allowedNextVersions'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); + printErrorAndExit(errorMessage: error); } bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); @@ -205,9 +156,7 @@ class VersionCheckCommand extends PluginCommand { NextVersionType.BREAKING_MAJOR) { final String error = '$pubspecPath breaking change detected.\n' 'Breaking changes to platform interfaces are strongly discouraged.\n'; - final Colorize redError = Colorize(error)..red(); - print(redError); - throw ToolExit(1); + printErrorAndExit(errorMessage: error); } } on io.ProcessException { print('Unable to find pubspec in master for $pubspecPath.' @@ -215,6 +164,74 @@ class VersionCheckCommand extends PluginCommand { } } + await for (Directory plugin in getPlugins()) { + await _checkVersionsMatch(plugin); + } + print('No version check errors found!'); } + + Future _checkVersionsMatch(Directory plugin) async { + // get version from pubspec + final String packageName = plugin.basename; + print('-----------------------------------------'); + print( + 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for $packageName.'); + + final Pubspec pubspec = _tryParsePubspec(plugin); + if (pubspec == null) { + final String error = 'Cannot parse version from pubspec.yaml'; + printErrorAndExit(errorMessage: error); + } + final Version fromPubspec = pubspec.version; + + // get first version from CHANGELOG + final File changelog = plugin.childFile('CHANGELOG.md'); + final List lines = changelog.readAsLinesSync(); + String firstLineWithText; + final Iterator iterator = lines.iterator; + while (iterator.moveNext()) { + if ((iterator.current as String).trim().isNotEmpty) { + firstLineWithText = iterator.current; + break; + } + } + // Remove all leading mark down syntax from the version line. + final String versionString = firstLineWithText.split(' ').last; + Version fromChangeLog = Version.parse(versionString); + if (fromChangeLog == null) { + final String error = + 'Cannot find version on the first line of ${plugin.path}/CHANGELOG.md'; + printErrorAndExit(errorMessage: error); + } + + if (fromPubspec != fromChangeLog) { + final String error = ''' +versions for $packageName in CHANGELOG.md and pubspec.yaml do not match. +The version in pubspec.yaml is $fromPubspec. +The first version listed in CHANGELOG.md is $fromChangeLog. +'''; + printErrorAndExit(errorMessage: error); + } + print('${packageName} passed version check'); + } + + Pubspec _tryParsePubspec(Directory package) { + final File pubspecFile = package.childFile('pubspec.yaml'); + + try { + Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec == null) { + final String error = + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}'; + printErrorAndExit(errorMessage: error); + } + return pubspec; + } on Exception catch (exception) { + final String error = + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}'; + printErrorAndExit(errorMessage: error); + } + return null; + } } diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index b3504c2358d..0fb3ce74c37 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -1,6 +1,10 @@ +import 'dart:io'; + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import 'util.dart'; @@ -9,8 +13,21 @@ void main() { RecordingProcessRunner processRunner; CommandRunner runner; List plugins; + List> gitDirCommands; + String gitDiffResponse; setUp(() { + gitDirCommands = >[]; + gitDiffResponse = ''; + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0]); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + } + return Future.value(mockProcessResult); + }); initializeFakePackages(); processRunner = RecordingProcessRunner(); plugins = []; @@ -19,6 +36,7 @@ void main() { mockPackagesDir, mockFileSystem, processRunner: processRunner, + gitDir: gitDir, ); runner = CommandRunner('common_command', 'Test for common functionality'); @@ -73,6 +91,207 @@ void main() { ]); expect(plugins, unorderedEquals([plugin2.path])); }); + + group('test run-on-changed-packages', () { + test('all plugins should be tested if there are no changes.', () async { + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if there are no plugin related changes.', + () async { + gitDiffResponse = ".cirrus"; + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('Only changed plugin should be tested.', () async { + gitDiffResponse = "packages/plugin1/plugin1.dart"; + final Directory plugin1 = createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + + test('multiple files in one plugin should also test the plugin', () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin1/ios/plugin1.m +'''; + final Directory plugin1 = createFakePlugin('plugin1'); + createFakePlugin('plugin2'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + + test('multiple plugins changed should test all the changed plugins', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +'''; + final Directory plugin1 = createFakePlugin('plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + createFakePlugin('plugin3'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test( + 'multiple plugins inside the same plugin group changed should output the plugin group name', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1/plugin1.dart +packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart +packages/plugin1/plugin1_web/plugin1_web.dart +'''; + final Directory plugin1 = + createFakePlugin('plugin1', parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2'); + createFakePlugin('plugin3'); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + + test('--plugins flag overrides the behavior of --run-on-changed-packages', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +packages/plugin3/plugin3.dart +'''; + final Directory plugin1 = + createFakePlugin('plugin1', parentDirectoryName: 'plugin1'); + final Directory plugin2 = createFakePlugin('plugin2'); + createFakePlugin('plugin3'); + await runner.run([ + 'sample', + '--plugins=plugin1,plugin2', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('--exclude flag works with --run-on-changed-packages', () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +packages/plugin3/plugin3.dart +'''; + final Directory plugin1 = + createFakePlugin('plugin1', parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2'); + createFakePlugin('plugin3'); + await runner.run([ + 'sample', + '--exclude=plugin2,plugin3', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + }); + + group('$GitVersionFinder', () { + List> gitDirCommands; + String gitDiffResponse; + String mergeBaseResponse; + MockGitDir gitDir; + + setUp(() { + gitDirCommands = >[]; + gitDiffResponse = ''; + gitDir = MockGitDir(); + when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0]); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'merge-base') { + when(mockProcessResult.stdout).thenReturn(mergeBaseResponse); + } + return Future.value(mockProcessResult); + }); + initializeFakePackages(); + processRunner = RecordingProcessRunner(); + }); + + tearDown(() { + cleanupPackages(); + }); + + test('No git diff should result no files changed', () async { + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + 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'); + 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'); + 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 { + final 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 SamplePluginCommand extends PluginCommand { @@ -81,7 +300,9 @@ class SamplePluginCommand extends PluginCommand { Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); + GitDir gitDir, + }) : super(packagesDir, fileSystem, + processRunner: processRunner, gitDir: gitDir); List plugins_; @@ -98,3 +319,7 @@ class SamplePluginCommand extends PluginCommand { } } } + +class MockGitDir extends Mock implements GitDir {} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index ec0000d13f3..1538d9b554e 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -37,6 +37,8 @@ Directory createFakePlugin( bool isLinuxPlugin = false, bool isMacOsPlugin = false, bool isWindowsPlugin = false, + bool includeChangeLog = false, + bool includeVersion = false, String parentDirectoryName = '', }) { assert(!(withSingleExample && withExamples.isNotEmpty), @@ -57,7 +59,14 @@ Directory createFakePlugin( isLinuxPlugin: isLinuxPlugin, isMacOsPlugin: isMacOsPlugin, isWindowsPlugin: isWindowsPlugin, + includeVersion: includeVersion, ); + if (includeChangeLog) { + createFakeCHANGELOG(pluginDirectory, ''' +## 0.0.1 + * Some changes. + '''); + } if (withSingleExample) { final Directory exampleDir = pluginDirectory.childDirectory('example') @@ -85,6 +94,11 @@ Directory createFakePlugin( return pluginDirectory; } +void createFakeCHANGELOG(Directory parent, String texts) { + parent.childFile('CHANGELOG.md').createSync(); + parent.childFile('CHANGELOG.md').writeAsStringSync(texts); +} + /// Creates a `pubspec.yaml` file with a flutter dependency. void createFakePubspec( Directory parent, { @@ -97,6 +111,7 @@ void createFakePubspec( bool isLinuxPlugin = false, bool isMacOsPlugin = false, bool isWindowsPlugin = false, + String version = '0.0.1', }) { parent.childFile('pubspec.yaml').createSync(); String yaml = ''' @@ -152,8 +167,8 @@ dependencies: } if (includeVersion) { yaml += ''' -version: 0.0.1 -publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test. +version: $version +publish_to: http://no_pub_server.com # Hardcoded safeguard to prevent this from somehow being published by a broken test. '''; } parent.childFile('pubspec.yaml').writeAsStringSync(yaml); diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index b9ace3811bf..ac0d378c2a2 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:args/command_runner.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; import "package:test/test.dart"; @@ -74,18 +75,18 @@ void main() { }); test('allows valid version', () async { - createFakePlugin('plugin'); + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin/pubspec.yaml"; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); expect( output, - orderedEquals([ + containsAllInOrder([ 'No version check errors found!', ]), ); @@ -99,14 +100,14 @@ void main() { }); test('denies invalid version', () async { - createFakePlugin('plugin'); + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin/pubspec.yaml"; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', }; final Future> result = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); await expectLater( result, @@ -122,7 +123,7 @@ void main() { }); test('gracefully handles missing pubspec.yaml', () async { - createFakePlugin('plugin'); + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin/pubspec.yaml"; mockFileSystem.currentDirectory .childDirectory('packages') @@ -130,11 +131,12 @@ void main() { .childFile('pubspec.yaml') .deleteSync(); final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); expect( output, orderedEquals([ + 'Determine diff with base sha: master', 'No version check errors found!', ]), ); @@ -144,7 +146,8 @@ void main() { }); test('allows minor changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); + createFakePlugin('plugin_platform_interface', + includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': @@ -153,10 +156,10 @@ void main() { 'version: 1.1.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); expect( output, - orderedEquals([ + containsAllInOrder([ 'No version check errors found!', ]), ); @@ -172,7 +175,8 @@ void main() { }); test('disallows breaking changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface'); + createFakePlugin('plugin_platform_interface', + includeChangeLog: true, includeVersion: true); gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': @@ -181,7 +185,7 @@ void main() { 'version: 2.0.0', }; final Future> output = runCapturingPrint( - runner, ['version-check', '--base_sha=master']); + runner, ['version-check', '--base-sha=master']); await expectLater( output, throwsA(const TypeMatcher()), @@ -196,6 +200,138 @@ void main() { expect(gitDirCommands[2].join(' '), equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); }); + + test('Allow empty lines in front of the first version in CHANGELOG', + () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.1'); + String changelog = ''' + + + +## 1.0.1 + +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expect( + output, + containsAllInOrder([ + 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.', + 'plugin passed version check', + 'No version check errors found!' + ]), + ); + }); + + test('Throws if versions in changelog and pubspec do not match', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.1'); + String changelog = ''' +## 1.0.2 + +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final Future> output = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + try { + List outputValue = await output; + await expectLater( + outputValue, + containsAllInOrder([ + ''' + versions for plugin in CHANGELOG.md and pubspec.yaml do not match. + The version in pubspec.yaml is 1.0.1. + The first version listed in CHANGELOG.md is 1.0.2. + ''', + ]), + ); + } on ToolExit catch (_) {} + }); + + test('Success if CHANGELOG and pubspec versions match', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.1'); + String changelog = ''' +## 1.0.1 + +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expect( + output, + containsAllInOrder([ + 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.', + 'plugin passed version check', + 'No version check errors found!' + ]), + ); + }); + + test( + 'Fail if pubspec version only matches an older version listed in CHANGELOG', + () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.0'); + String changelog = ''' +## 1.0.1 + +* Some changes. + +## 1.0.0 + +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + Future> output = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + try { + List outputValue = await output; + await expectLater( + outputValue, + containsAllInOrder([ + ''' + versions for plugin in CHANGELOG.md and pubspec.yaml do not match. + The version in pubspec.yaml is 1.0.0. + The first version listed in CHANGELOG.md is 1.0.1. + ''', + ]), + ); + } on ToolExit catch (_) {} + }); }); group("Pre 1.0", () { From 6141051fa27c383c604929aa80b8fff6871dbe0b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 16 Mar 2021 17:50:51 -0400 Subject: [PATCH 013/249] Add missing licenses, and add a check (#3720) Adds a new CI check that all code files have a copyright+license block (and that it's one we are expecting to see). Fixes the ~350 files (!) that did not have them. This includes all of the files in the .../example/ directories, following the example of flutter/flutter. (This does mean some manual intervention will be needed when generating new example directories in the future, but it's one-time per example.) Also standardized some variants that used different line breaks than most of the rest of the repo (likely added since I standardized them all a while ago, but didn't add a check for at the time to enforce going forward), to simplify the checks. Fixes flutter/flutter#77114 --- .../tool/lib/src/license_check_command.dart | 209 ++++++++++++ script/tool/lib/src/main.dart | 2 + .../tool/lib/src/publish_plugin_command.dart | 4 + script/tool/test/analyze_command_test.dart | 4 + .../test/build_examples_command_test.dart | 4 + script/tool/test/common_test.dart | 4 + .../test/drive_examples_command_test.dart | 4 + script/tool/test/firebase_test_lab_test.dart | 4 + .../tool/test/license_check_command_test.dart | 306 ++++++++++++++++++ .../tool/test/lint_podspecs_command_test.dart | 4 + script/tool/test/list_command_test.dart | 4 + script/tool/test/mocks.dart | 4 + .../test/publish_plugin_command_test.dart | 4 + script/tool/test/test_command_test.dart | 4 + script/tool/test/util.dart | 4 + script/tool/test/version_check_test.dart | 4 + 16 files changed, 569 insertions(+) create mode 100644 script/tool/lib/src/license_check_command.dart create mode 100644 script/tool/test/license_check_command_test.dart diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart new file mode 100644 index 00000000000..4e0e5931d3e --- /dev/null +++ b/script/tool/lib/src/license_check_command.dart @@ -0,0 +1,209 @@ +// Copyright 2017 The Chromium 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +const Set _codeFileExtensions = { + '.c', + '.cc', + '.cpp', + '.dart', + '.h', + '.html', + '.java', + '.m', + '.mm', + '.swift', + '.sh', +}; + +// Basenames without extensions of files to ignore. +const Set _ignoreBasenameList = { + 'flutter_export_environment', + 'GeneratedPluginRegistrant', + 'generated_plugin_registrant', +}; + +// File suffixes that otherwise match _codeFileExtensions to ignore. +const Set _ignoreSuffixList = { + '.g.dart', // Generated API code. + '.mocks.dart', // Generated by Mockito. +}; + +// Full basenames of files to ignore. +const Set _ignoredFullBasenameList = { + 'resource.h', // Generated by VS. +}; + +// Copyright and license regexes. +// +// These are intentionally very simple, since almost all source in this +// repository should be using the same license text, comment style, etc., so +// they shouldn't need to be very flexible. Complexity can be added as-needed +// on a case-by-case basis. +final RegExp _copyrightRegex = + RegExp(r'^(?://|#|'), + }; + + for (final File file in codeFiles) { + _print('Checking ${file.path}'); + final String content = await file.readAsString(); + + final RegExpMatch copyright = _copyrightRegex.firstMatch(content); + if (copyright == null) { + filesWithoutDetectedCopyright.add(file); + continue; + } + final String author = copyright.group(1); + if (!_firstPartyAuthors.contains(author) && + !p.split(file.path).contains('third_party')) { + misplacedThirdPartyFiles.add(file); + } + + final String bsdLicense = + bsdLicenseBlockByExtension[p.extension(file.path)] ?? + defaultBsdLicenseBlock; + if (!content.contains(bsdLicense) && + !_workivaLicenseRegex.hasMatch(content)) { + filesWithoutDetectedLicense.add(file); + } + } + _print('\n\n'); + + // Sort by path for more usable output. + final pathCompare = (File a, File b) => a.path.compareTo(b.path); + filesWithoutDetectedCopyright.sort(pathCompare); + filesWithoutDetectedLicense.sort(pathCompare); + misplacedThirdPartyFiles.sort(pathCompare); + + if (filesWithoutDetectedCopyright.isNotEmpty) { + _print('No copyright line was found for the following files:'); + for (final File file in filesWithoutDetectedCopyright) { + _print(' ${file.path}'); + } + _print('Please check that they have a copyright and license block. ' + 'If they do, the license check may need to be updated to recognize its ' + 'format.\n'); + } + + if (filesWithoutDetectedLicense.isNotEmpty) { + _print('No recognized license was found for the following files:'); + for (final File file in filesWithoutDetectedLicense) { + _print(' ${file.path}'); + } + _print('Please check that they have a license at the top of the file. ' + 'If they do, the license check may need to be updated to recognize ' + 'either the license or the specific format of the license ' + 'block.\n'); + } + + if (misplacedThirdPartyFiles.isNotEmpty) { + _print('The following files do not have a recognized first-party author ' + 'but are not in a "third_party/" directory:'); + for (final File file in misplacedThirdPartyFiles) { + _print(' ${file.path}'); + } + _print('Please move these files to "third_party/".\n'); + } + + bool succeeded = filesWithoutDetectedCopyright.isEmpty && + filesWithoutDetectedLicense.isEmpty && + misplacedThirdPartyFiles.isEmpty; + if (succeeded) { + _print('All files passed validation!'); + } + return succeeded; + } + + bool _shouldIgnoreFile(File file) { + final String path = file.path; + return _ignoreBasenameList.contains(p.basenameWithoutExtension(path)) || + _ignoreSuffixList.any((String suffix) => + path.endsWith(suffix) || + _ignoredFullBasenameList.contains(p.basename(path))); + } + + Future> _getAllFiles() => packagesDir.parent + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .map((FileSystemEntity file) => file as File) + .toList(); +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index fa81597237d..32911293125 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -19,6 +19,7 @@ import 'drive_examples_command.dart'; import 'firebase_test_lab_command.dart'; import 'format_command.dart'; import 'java_test_command.dart'; +import 'license_check_command.dart'; import 'lint_podspecs_command.dart'; import 'list_command.dart'; import 'test_command.dart'; @@ -50,6 +51,7 @@ void main(List args) { ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) ..addCommand(FormatCommand(packagesDir, fileSystem)) ..addCommand(JavaTestCommand(packagesDir, fileSystem)) + ..addCommand(LicenseCheckCommand(packagesDir, fileSystem)) ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) ..addCommand(ListCommand(packagesDir, fileSystem)) ..addCommand(PublishCheckCommand(packagesDir, fileSystem)) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index f7e3b5deeec..f61a76947c9 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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'; diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 9e7a42bbb68..63afb51c859 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/analyze_command.dart'; diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 65417525d71..e0213893db3 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/build_examples_command.dart'; diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index 0fb3ce74c37..a8deacc8e48 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:args/command_runner.dart'; diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index f4bdd95c166..7a8e9f3e9f9 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common.dart'; diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index 97b977619d5..d1162467152 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:args/command_runner.dart'; diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart new file mode 100644 index 00000000000..524e7271236 --- /dev/null +++ b/script/tool/test/license_check_command_test.dart @@ -0,0 +1,306 @@ +// Copyright 2019 The Chromium 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: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/license_check_command.dart'; +import 'package:test/test.dart'; + +void main() { + group('$LicenseCheckCommand', () { + CommandRunner runner; + FileSystem fileSystem; + List printedMessages; + Directory root; + + setUp(() { + fileSystem = MemoryFileSystem(); + final Directory packagesDir = + fileSystem.currentDirectory.childDirectory('packages'); + root = packagesDir.parent; + + printedMessages = []; + final LicenseCheckCommand command = LicenseCheckCommand( + packagesDir, + fileSystem, + print: (Object message) => printedMessages.add(message.toString()), + ); + runner = + CommandRunner('license_test', 'Test for $LicenseCheckCommand'); + runner.addCommand(command); + }); + + /// Writes a copyright+license block to [file], defaulting to a standard + /// block for this repository. + /// + /// [commentString] is added to the start of each line. + /// [prefix] is added to the start of the entire block. + /// [suffix] is added to the end of the entire block. + void _writeLicense( + File file, { + String comment = '// ', + String prefix = '', + String suffix = '', + String copyright = + 'Copyright 2019 The Chromium Authors. All rights reserved.', + List license = const [ + 'Use of this source code is governed by a BSD-style license that can be', + 'found in the LICENSE file.', + ], + }) { + List lines = ['$prefix$comment$copyright']; + for (String line in license) { + lines.add('$comment$line'); + } + file.writeAsStringSync(lines.join('\n') + suffix + '\n'); + } + + test('looks at only expected extensions', () async { + Map extensions = { + 'c': true, + 'cc': true, + 'cpp': true, + 'dart': true, + 'h': true, + 'html': true, + 'java': true, + 'json': false, + 'm': true, + 'md': false, + 'mm': true, + 'png': false, + 'swift': true, + 'sh': true, + 'yaml': false, + }; + + const String filenameBase = 'a_file'; + for (final String fileExtension in extensions.keys) { + root.childFile('$filenameBase.$fileExtension').createSync(); + } + + try { + await runner.run(['license-check']); + } on ToolExit { + // Ignore failure; the files are empty so the check is expected to fail, + // but this test isn't for that behavior. + } + + extensions.forEach((String fileExtension, bool shouldCheck) { + final Matcher logLineMatcher = + contains('Checking $filenameBase.$fileExtension'); + expect(printedMessages, + shouldCheck ? logLineMatcher : isNot(logLineMatcher)); + }); + }); + + test('ignore list overrides extension matches', () async { + List ignoredFiles = [ + // Ignored base names. + 'flutter_export_environment.sh', + 'GeneratedPluginRegistrant.java', + 'GeneratedPluginRegistrant.m', + 'generated_plugin_registrant.cc', + 'generated_plugin_registrant.cpp', + // Ignored path suffixes. + 'foo.g.dart', + 'foo.mocks.dart', + // Ignored files. + 'resource.h', + ]; + + for (final String name in ignoredFiles) { + root.childFile(name).createSync(); + } + + await runner.run(['license-check']); + + for (final String name in ignoredFiles) { + expect(printedMessages, isNot(contains('Checking $name'))); + } + }); + + test('passes if all checked files have license blocks', () async { + File checked = root.childFile('checked.cc'); + checked.createSync(); + _writeLicense(checked); + File not_checked = root.childFile('not_checked.md'); + not_checked.createSync(); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check a file. + expect(printedMessages, contains('Checking checked.cc')); + expect(printedMessages, contains('All files passed validation!')); + }); + + test('handles the comment styles for all supported languages', () async { + File file_a = root.childFile('file_a.cc'); + file_a.createSync(); + _writeLicense(file_a, comment: '// '); + File file_b = root.childFile('file_b.sh'); + file_b.createSync(); + _writeLicense(file_b, comment: '# '); + File file_c = root.childFile('file_c.html'); + file_c.createSync(); + _writeLicense(file_c, comment: '', prefix: ''); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check the files. + expect(printedMessages, contains('Checking file_a.cc')); + expect(printedMessages, contains('Checking file_b.sh')); + expect(printedMessages, contains('Checking file_c.html')); + expect(printedMessages, contains('All files passed validation!')); + }); + + test('fails if any checked files are missing license blocks', () async { + File good_a = root.childFile('good.cc'); + good_a.createSync(); + _writeLicense(good_a); + File good_b = root.childFile('good.h'); + good_b.createSync(); + _writeLicense(good_b); + root.childFile('bad.cc').createSync(); + root.childFile('bad.h').createSync(); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect(printedMessages, + contains('No copyright line was found for the following files:')); + expect(printedMessages, contains(' bad.cc')); + expect(printedMessages, contains(' bad.h')); + // Failure shouldn't print the success message. + expect(printedMessages, isNot(contains('All files passed validation!'))); + }); + + test('fails if any checked files are missing just the copyright', () async { + File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + File bad = root.childFile('bad.cc'); + bad.createSync(); + _writeLicense(bad, copyright: ''); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect(printedMessages, + contains('No copyright line was found for the following files:')); + expect(printedMessages, contains(' bad.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, isNot(contains('All files passed validation!'))); + }); + + test('fails if any checked files are missing just the license', () async { + File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + File bad = root.childFile('bad.cc'); + bad.createSync(); + _writeLicense(bad, license: []); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect(printedMessages, + contains('No recognized license was found for the following files:')); + expect(printedMessages, contains(' bad.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, isNot(contains('All files passed validation!'))); + }); + + test('fails if any third-party code is not in a third_party directory', + () async { + File thirdPartyFile = root.childFile('third_party.cc'); + thirdPartyFile.createSync(); + _writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect( + printedMessages, + contains( + 'The following files do not have a recognized first-party author ' + 'but are not in a "third_party/" directory:')); + expect(printedMessages, contains(' third_party.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, isNot(contains('All files passed validation!'))); + }); + + test('succeeds for third-party code in a third_party directory', () async { + File thirdPartyFile = root + .childDirectory('a_plugin') + .childDirectory('lib') + .childDirectory('src') + .childDirectory('third_party') + .childFile('file.cc'); + thirdPartyFile.createSync(recursive: true); + _writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check the file. + expect(printedMessages, + contains('Checking a_plugin/lib/src/third_party/file.cc')); + expect(printedMessages, contains('All files passed validation!')); + }); + + test('fails for licenses that the tool does not expect', () async { + File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + File bad = root.childDirectory('third_party').childFile('bad.cc'); + bad.createSync(recursive: true); + _writeLicense(bad, license: [ + 'This program is free software: you can redistribute it and/or modify', + 'it under the terms of the GNU General Public License', + ]); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect(printedMessages, + contains('No recognized license was found for the following files:')); + expect(printedMessages, contains(' third_party/bad.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, isNot(contains('All files passed validation!'))); + }); + + test('Apache is not recognized for new authors without validation changes', + () async { + File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + File bad = root.childDirectory('third_party').childFile('bad.cc'); + bad.createSync(recursive: true); + _writeLicense( + bad, + copyright: 'Copyright 2017 Some New Authors', + license: [ + 'Licensed under the Apache License, Version 2.0', + ], + ); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect(printedMessages, + contains('No recognized license was found for the following files:')); + expect(printedMessages, contains(' third_party/bad.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, isNot(contains('All files passed validation!'))); + }); + }); +} diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 1c59d2d7e55..2a3e60853d0 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:args/command_runner.dart'; diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index 478625283dd..d06f6d2ca46 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/list_command.dart'; diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 3e17ff8efd3..35a4eb7cbac 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:io' as io; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index ada4bf08fd7..4f770b2054f 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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; diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 514e4c27190..520f4c316f5 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/test_command.dart'; diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 1538d9b554e..63cd5defbb2 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:io' as io; diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index ac0d378c2a2..400d4263357 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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:io'; From 85e7e4391bf9e38fbd2c9de52ec7023d0d4e3659 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 17 Mar 2021 16:25:28 -0400 Subject: [PATCH 014/249] Prep for alignment with Flutter analysis options (#3703) Renames the old analysis_options.yaml to analysis_options_legacy.yaml, replacing it with a slightly modified copy of flutter/flutter's analysis options. Each plugins has a temporary local analysis_options.yaml that points to the legacy version. This allows for inceremental conversion on a per-plugin basis, which should make the problem more tractable. Since this hasn't yet been enabled for any packages, it's likely that as it is we'll find a few local modification we need to make to the root analysis_options (e.g., things that conflict with 'dart format'). Part of https://github.com/flutter/flutter/issues/76229 --- script/tool/analysis_options.yaml | 1 + 1 file changed, 1 insertion(+) create mode 100644 script/tool/analysis_options.yaml diff --git a/script/tool/analysis_options.yaml b/script/tool/analysis_options.yaml new file mode 100644 index 00000000000..cda4f6e153e --- /dev/null +++ b/script/tool/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml From c7b35882b47068eada1e5a779eb0ff8cb422bb76 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 18 Mar 2021 13:28:44 -0700 Subject: [PATCH 015/249] Fix cosmetic variations in copyrights and license files (#3730) - Replaces "the Flutter project authors" with the repo-standard version "The Flutter Authors" - Updates the license check not to allow "the Flutter project authors" in the future - Fixes a few minor cosmetic variations that had crept back into LICENSE files since my mass-standardization of those files. - Updates the license check to validate those to prevent such drift in the future. --- .../tool/lib/src/license_check_command.dart | 86 ++++++++++-- .../tool/test/license_check_command_test.dart | 123 ++++++++++++++++-- 2 files changed, 191 insertions(+), 18 deletions(-) diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 4e0e5931d3e..e70e60b154d 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -66,9 +66,40 @@ final List _firstPartyAuthors = [ 'The Chromium Authors', 'the Chromium project authors', 'The Flutter Authors', - 'the Flutter project authors', ]; +// The exact format of the BSD license that our license files should contain. +// Slight variants are not accepted because they may prevent consolidation in +// tools that assemble all licenses used in distributed applications. +// +// TODO(stuartmorgan): Add the copyright string here once that's completely +// standardized. +final String _fullBsdLicenseText = ''' +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + /// Validates that code files have copyright and license blocks. class LicenseCheckCommand extends PluginCommand { /// Creates a new license check command for [packagesDir]. @@ -90,13 +121,19 @@ class LicenseCheckCommand extends PluginCommand { @override Future run() async { - Iterable codeFiles = (await _getAllFiles()).where((File file) => + final Iterable codeFiles = (await _getAllFiles()).where((File file) => _codeFileExtensions.contains(p.extension(file.path)) && !_shouldIgnoreFile(file)); + final Iterable firstPartyLicenseFiles = (await _getAllFiles()).where( + (File file) => + p.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); - bool succeeded = await _checkLicenses(codeFiles); + final bool copyrightCheckSucceeded = await _checkCodeLicenses(codeFiles); + print('\n=======================================\n'); + final bool licenseCheckSucceeded = + await _checkLicenseFiles(firstPartyLicenseFiles); - if (!succeeded) { + if (!copyrightCheckSucceeded || !licenseCheckSucceeded) { throw ToolExit(1); } } @@ -110,7 +147,7 @@ class LicenseCheckCommand extends PluginCommand { // Checks all license blocks for [codeFiles], returning false if any of them // fail validation. - Future _checkLicenses(Iterable codeFiles) async { + Future _checkCodeLicenses(Iterable codeFiles) async { final List filesWithoutDetectedCopyright = []; final List filesWithoutDetectedLicense = []; final List misplacedThirdPartyFiles = []; @@ -133,8 +170,7 @@ class LicenseCheckCommand extends PluginCommand { continue; } final String author = copyright.group(1); - if (!_firstPartyAuthors.contains(author) && - !p.split(file.path).contains('third_party')) { + if (!_firstPartyAuthors.contains(author) && !_isThirdParty(file)) { misplacedThirdPartyFiles.add(file); } @@ -146,7 +182,7 @@ class LicenseCheckCommand extends PluginCommand { filesWithoutDetectedLicense.add(file); } } - _print('\n\n'); + _print('\n'); // Sort by path for more usable output. final pathCompare = (File a, File b) => a.path.compareTo(b.path); @@ -188,7 +224,35 @@ class LicenseCheckCommand extends PluginCommand { filesWithoutDetectedLicense.isEmpty && misplacedThirdPartyFiles.isEmpty; if (succeeded) { - _print('All files passed validation!'); + _print('All source files passed validation!'); + } + return succeeded; + } + + // Checks all provide LICENSE files, returning false if any of them + // fail validation. + Future _checkLicenseFiles(Iterable files) async { + final List incorrectLicenseFiles = []; + + for (final File file in files) { + _print('Checking ${file.path}'); + if (!file.readAsStringSync().contains(_fullBsdLicenseText)) { + incorrectLicenseFiles.add(file); + } + } + _print('\n'); + + if (incorrectLicenseFiles.isNotEmpty) { + _print('The following LICENSE files do not follow the expected format:'); + for (final File file in incorrectLicenseFiles) { + _print(' ${file.path}'); + } + _print('Please ensure that they use the exact format used in this repository".\n'); + } + + bool succeeded = incorrectLicenseFiles.isEmpty; + if (succeeded) { + _print('All LICENSE files passed validation!'); } return succeeded; } @@ -201,6 +265,10 @@ class LicenseCheckCommand extends PluginCommand { _ignoredFullBasenameList.contains(p.basename(path))); } + bool _isThirdParty(File file) { + return p.split(file.path).contains('third_party'); + } + Future> _getAllFiles() => packagesDir.parent .list(recursive: true, followLinks: false) .where((FileSystemEntity entity) => entity is File) diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 524e7271236..8ae956740d7 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -134,7 +134,7 @@ void main() { // Sanity check that the test did actually check a file. expect(printedMessages, contains('Checking checked.cc')); - expect(printedMessages, contains('All files passed validation!')); + expect(printedMessages, contains('All source files passed validation!')); }); test('handles the comment styles for all supported languages', () async { @@ -154,7 +154,7 @@ void main() { expect(printedMessages, contains('Checking file_a.cc')); expect(printedMessages, contains('Checking file_b.sh')); expect(printedMessages, contains('Checking file_c.html')); - expect(printedMessages, contains('All files passed validation!')); + expect(printedMessages, contains('All source files passed validation!')); }); test('fails if any checked files are missing license blocks', () async { @@ -176,7 +176,8 @@ void main() { expect(printedMessages, contains(' bad.cc')); expect(printedMessages, contains(' bad.h')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('fails if any checked files are missing just the copyright', () async { @@ -195,7 +196,8 @@ void main() { contains('No copyright line was found for the following files:')); expect(printedMessages, contains(' bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('fails if any checked files are missing just the license', () async { @@ -214,7 +216,8 @@ void main() { contains('No recognized license was found for the following files:')); expect(printedMessages, contains(' bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('fails if any third-party code is not in a third_party directory', @@ -234,7 +237,8 @@ void main() { 'but are not in a "third_party/" directory:')); expect(printedMessages, contains(' third_party.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('succeeds for third-party code in a third_party directory', () async { @@ -252,7 +256,7 @@ void main() { // Sanity check that the test did actually check the file. expect(printedMessages, contains('Checking a_plugin/lib/src/third_party/file.cc')); - expect(printedMessages, contains('All files passed validation!')); + expect(printedMessages, contains('All source files passed validation!')); }); test('fails for licenses that the tool does not expect', () async { @@ -274,7 +278,8 @@ void main() { contains('No recognized license was found for the following files:')); expect(printedMessages, contains(' third_party/bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('Apache is not recognized for new authors without validation changes', @@ -300,7 +305,107 @@ void main() { contains('No recognized license was found for the following files:')); expect(printedMessages, contains(' third_party/bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); + }); + + test('passes if all first-party LICENSE files are correctly formatted', + () async { + File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_correctLicenseFileText); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check the file. + expect(printedMessages, contains('Checking LICENSE')); + expect(printedMessages, contains('All LICENSE files passed validation!')); + }); + + test('fails if any first-party LICENSE files are incorrectly formatted', + () async { + File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_incorrectLicenseFileText); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + expect(printedMessages, + isNot(contains('All LICENSE files passed validation!'))); + }); + + test('ignores third-party LICENSE format', + () async { + File license = root.childDirectory('third_party').childFile('LICENSE'); + license.createSync(recursive: true); + license.writeAsStringSync(_incorrectLicenseFileText); + + await runner.run(['license-check']); + + // The file shouldn't be checked. + expect(printedMessages, isNot(contains('Checking third_party/LICENSE'))); + expect(printedMessages, contains('All LICENSE files passed validation!')); }); }); } + +const String _correctLicenseFileText = + '''Copyright 2017 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + +// A common incorrect version created by copying text intended for a code file, +// with comment markers. +const String _incorrectLicenseFileText = + '''// Copyright 2017 The Flutter Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; From c956cb327046eb045dc599f2476a7977778e3323 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 18 Mar 2021 16:05:33 -0700 Subject: [PATCH 016/249] Standardize Copyrights: Chromium->Flutter (#2996) In all copyright messages (and in the Xcode project organization name) standardize on "The Flutter Authors", adding "The Chromium Authors" to the Flutter AUTHORS list. This reduces inconsistency in the copyright lines in this repository, moving closer to a single consistent copyright+license (as in flutter/engine and flutter/flutter) Updates the validation script to no longer accept "The Chromium Authors" or "the Chromium project authors" in first-party code. --- script/tool/lib/src/analyze_command.dart | 2 +- script/tool/lib/src/build_examples_command.dart | 2 +- script/tool/lib/src/common.dart | 2 +- .../lib/src/create_all_plugins_app_command.dart | 2 +- script/tool/lib/src/drive_examples_command.dart | 2 +- script/tool/lib/src/firebase_test_lab_command.dart | 2 +- script/tool/lib/src/format_command.dart | 2 +- script/tool/lib/src/java_test_command.dart | 2 +- script/tool/lib/src/license_check_command.dart | 14 ++++---------- script/tool/lib/src/lint_podspecs_command.dart | 2 +- script/tool/lib/src/list_command.dart | 2 +- script/tool/lib/src/main.dart | 2 +- script/tool/lib/src/publish_check_command.dart | 2 +- script/tool/lib/src/publish_plugin_command.dart | 2 +- script/tool/lib/src/test_command.dart | 2 +- script/tool/lib/src/version_check_command.dart | 2 +- script/tool/lib/src/xctest_command.dart | 2 +- script/tool/test/analyze_command_test.dart | 2 +- script/tool/test/build_examples_command_test.dart | 2 +- script/tool/test/common_test.dart | 2 +- script/tool/test/drive_examples_command_test.dart | 2 +- script/tool/test/firebase_test_lab_test.dart | 2 +- script/tool/test/license_check_command_test.dart | 4 ++-- script/tool/test/lint_podspecs_command_test.dart | 2 +- script/tool/test/list_command_test.dart | 2 +- script/tool/test/mocks.dart | 2 +- script/tool/test/publish_plugin_command_test.dart | 2 +- script/tool/test/test_command_test.dart | 2 +- script/tool/test/util.dart | 2 +- script/tool/test/version_check_test.dart | 4 ++-- script/tool/test/xctest_command_test.dart | 2 +- 31 files changed, 36 insertions(+), 42 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 8cd57fa0b33..7a0b47261a9 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index bb140bd429c..22029791f32 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index ce4f37873b0..1228aef5b0f 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. 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 0f1431c5aee..ec2fb7ed698 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 0bd531a20f8..4b5b33ff902 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 0b4b2a471db..38216122698 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2018 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. diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index e1c14e04cfe..54895cbae09 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index cf605bfc5ce..07800349a37 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2018 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. diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index e70e60b154d..2df217ef78f 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. @@ -60,13 +60,7 @@ final RegExp _workivaLicenseRegex = RegExp( multiLine: true, dotAll: true); -// TODO(stuartmorgan): Replace this with a single string once all the copyrights -// are standardized. -final List _firstPartyAuthors = [ - 'The Chromium Authors', - 'the Chromium project authors', - 'The Flutter Authors', -]; +const String _firstPartyAuthors = 'The Flutter Authors'; // The exact format of the BSD license that our license files should contain. // Slight variants are not accepted because they may prevent consolidation in @@ -129,7 +123,7 @@ class LicenseCheckCommand extends PluginCommand { p.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); final bool copyrightCheckSucceeded = await _checkCodeLicenses(codeFiles); - print('\n=======================================\n'); + _print('\n=======================================\n'); final bool licenseCheckSucceeded = await _checkLicenseFiles(firstPartyLicenseFiles); @@ -170,7 +164,7 @@ class LicenseCheckCommand extends PluginCommand { continue; } final String author = copyright.group(1); - if (!_firstPartyAuthors.contains(author) && !_isThirdParty(file)) { + if (author != _firstPartyAuthors && !_isThirdParty(file)) { misplacedThirdPartyFiles.add(file); } diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 13de64415e9..1e75eb62e4e 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 7f94daac709..53c1ba2f3ea 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2018 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. diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 32911293125..bb8af5d5ad5 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 670fedaf2fa..53002b57897 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index f61a76947c9..d48ed5b7fe7 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index e938168cfa8..47f7ad89cb8 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 111239f0399..36105ad300a 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index a4d03360b29..cdc02594a19 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 63afb51c859..6474d02f3d2 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index e0213893db3..ef959a6e251 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index a8deacc8e48..8ee82ca2e95 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 7a8e9f3e9f9..63a3e69adcd 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index d1162467152..6db4461a23a 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 8ae956740d7..69879bff886 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. @@ -45,7 +45,7 @@ void main() { String prefix = '', String suffix = '', String copyright = - 'Copyright 2019 The Chromium Authors. All rights reserved.', + 'Copyright 2019 The Flutter Authors. All rights reserved.', List license = const [ 'Use of this source code is governed by a BSD-style license that can be', 'found in the LICENSE file.', diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 2a3e60853d0..44e94ee873e 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index d06f6d2ca46..e9b68254fb5 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 35a4eb7cbac..d5cfe4ec4f7 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 4f770b2054f..b8ab1d25e53 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 520f4c316f5..eb9f3a9b0cf 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 63cd5defbb2..e463b88a1ab 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index 400d4263357..9e610d8a7a2 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2019 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. @@ -215,7 +215,7 @@ void main() { createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.1'); String changelog = ''' - + ## 1.0.1 diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 2b75ccde421..3b76fa6ffa1 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2017 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. From aef6008b7b0263ea52b9e55e088d6181203fad3e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 19 Mar 2021 13:18:00 -0700 Subject: [PATCH 017/249] Standardize copyright year (#3737) Standardizes all first-party copyrights on a single year, as is done in flutter/flutter and flutter/engine. All code now uses 2013, which is the earliest year that was in any existing copyright notice. The script checks now enforce the exact format of first-party licenses and copyrights. Fixes flutter/flutter#78448 --- script/tool/lib/src/analyze_command.dart | 2 +- .../tool/lib/src/build_examples_command.dart | 2 +- script/tool/lib/src/common.dart | 2 +- .../src/create_all_plugins_app_command.dart | 2 +- .../tool/lib/src/drive_examples_command.dart | 2 +- .../lib/src/firebase_test_lab_command.dart | 2 +- script/tool/lib/src/format_command.dart | 2 +- script/tool/lib/src/java_test_command.dart | 2 +- .../tool/lib/src/license_check_command.dart | 140 ++++++++---------- .../tool/lib/src/lint_podspecs_command.dart | 2 +- script/tool/lib/src/list_command.dart | 2 +- script/tool/lib/src/main.dart | 2 +- .../tool/lib/src/publish_check_command.dart | 2 +- .../tool/lib/src/publish_plugin_command.dart | 2 +- script/tool/lib/src/test_command.dart | 2 +- .../tool/lib/src/version_check_command.dart | 2 +- script/tool/lib/src/xctest_command.dart | 2 +- script/tool/test/analyze_command_test.dart | 2 +- .../test/build_examples_command_test.dart | 2 +- script/tool/test/common_test.dart | 2 +- .../test/drive_examples_command_test.dart | 2 +- script/tool/test/firebase_test_lab_test.dart | 2 +- .../tool/test/license_check_command_test.dart | 56 ++++--- .../tool/test/lint_podspecs_command_test.dart | 2 +- script/tool/test/list_command_test.dart | 2 +- script/tool/test/mocks.dart | 2 +- .../test/publish_plugin_command_test.dart | 2 +- script/tool/test/test_command_test.dart | 2 +- script/tool/test/util.dart | 2 +- script/tool/test/version_check_test.dart | 2 +- script/tool/test/xctest_command_test.dart | 2 +- 31 files changed, 126 insertions(+), 128 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 7a0b47261a9..b2586c4b0fc 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 22029791f32..966fdc9b99d 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index 1228aef5b0f..7d3063a20f5 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. 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 ec2fb7ed698..f1f11d15ca9 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 4b5b33ff902..529268ab4eb 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 38216122698..8e0abd50746 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -1,4 +1,4 @@ -// Copyright 2018 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 54895cbae09..d849dc9aa9d 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index 07800349a37..45042c3c700 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -1,4 +1,4 @@ -// Copyright 2018 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 2df217ef78f..4f78e4e9663 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. @@ -41,34 +41,30 @@ const Set _ignoredFullBasenameList = { 'resource.h', // Generated by VS. }; -// Copyright and license regexes. +// Copyright and license regexes for third-party code. // -// These are intentionally very simple, since almost all source in this -// repository should be using the same license text, comment style, etc., so -// they shouldn't need to be very flexible. Complexity can be added as-needed -// on a case-by-case basis. -final RegExp _copyrightRegex = - RegExp(r'^(?://|#|'), + final Map firstPartyLicenseBlockByExtension = + { + '.sh': _generateLicenseBlock('# '), + '.html': _generateLicenseBlock('', prefix: ''), }; for (final File file in codeFiles) { _print('Checking ${file.path}'); final String content = await file.readAsString(); - final RegExpMatch copyright = _copyrightRegex.firstMatch(content); - if (copyright == null) { - filesWithoutDetectedCopyright.add(file); - continue; - } - final String author = copyright.group(1); - if (author != _firstPartyAuthors && !_isThirdParty(file)) { - misplacedThirdPartyFiles.add(file); - } - - final String bsdLicense = - bsdLicenseBlockByExtension[p.extension(file.path)] ?? - defaultBsdLicenseBlock; - if (!content.contains(bsdLicense) && - !_workivaLicenseRegex.hasMatch(content)) { - filesWithoutDetectedLicense.add(file); + if (_isThirdParty(file)) { + if (!_thirdPartyLicenseBlockRegexes + .any((regex) => regex.hasMatch(content))) { + unrecognizedThirdPartyFiles.add(file); + } + } else { + final String license = + firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? + defaultFirstParyLicenseBlock; + if (!content.contains(license)) { + incorrectFirstPartyFiles.add(file); + } } } _print('\n'); // Sort by path for more usable output. final pathCompare = (File a, File b) => a.path.compareTo(b.path); - filesWithoutDetectedCopyright.sort(pathCompare); - filesWithoutDetectedLicense.sort(pathCompare); - misplacedThirdPartyFiles.sort(pathCompare); + incorrectFirstPartyFiles.sort(pathCompare); + unrecognizedThirdPartyFiles.sort(pathCompare); - if (filesWithoutDetectedCopyright.isNotEmpty) { - _print('No copyright line was found for the following files:'); - for (final File file in filesWithoutDetectedCopyright) { + if (incorrectFirstPartyFiles.isNotEmpty) { + _print('The license block for these files is missing or incorrect:'); + for (final File file in incorrectFirstPartyFiles) { _print(' ${file.path}'); } - _print('Please check that they have a copyright and license block. ' - 'If they do, the license check may need to be updated to recognize its ' - 'format.\n'); + _print('If this third-party code, move it to a "third_party/" directory, ' + 'otherwise ensure that you are using the exact copyright and license ' + 'text used by all first-party files in this repository.\n'); } - if (filesWithoutDetectedLicense.isNotEmpty) { - _print('No recognized license was found for the following files:'); - for (final File file in filesWithoutDetectedLicense) { + if (unrecognizedThirdPartyFiles.isNotEmpty) { + _print( + 'No recognized license was found for the following third-party files:'); + for (final File file in unrecognizedThirdPartyFiles) { _print(' ${file.path}'); } _print('Please check that they have a license at the top of the file. ' - 'If they do, the license check may need to be updated to recognize ' - 'either the license or the specific format of the license ' - 'block.\n'); - } - - if (misplacedThirdPartyFiles.isNotEmpty) { - _print('The following files do not have a recognized first-party author ' - 'but are not in a "third_party/" directory:'); - for (final File file in misplacedThirdPartyFiles) { - _print(' ${file.path}'); - } - _print('Please move these files to "third_party/".\n'); + 'If they do, the license check needs to be updated to recognize ' + 'the new third-party license block.\n'); } - bool succeeded = filesWithoutDetectedCopyright.isEmpty && - filesWithoutDetectedLicense.isEmpty && - misplacedThirdPartyFiles.isEmpty; + bool succeeded = + incorrectFirstPartyFiles.isEmpty && unrecognizedThirdPartyFiles.isEmpty; if (succeeded) { _print('All source files passed validation!'); } @@ -241,7 +226,8 @@ class LicenseCheckCommand extends PluginCommand { for (final File file in incorrectLicenseFiles) { _print(' ${file.path}'); } - _print('Please ensure that they use the exact format used in this repository".\n'); + _print( + 'Please ensure that they use the exact format used in this repository".\n'); } bool succeeded = incorrectLicenseFiles.isEmpty; diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 1e75eb62e4e..3261072d71a 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 53c1ba2f3ea..3571786ab7f 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -1,4 +1,4 @@ -// Copyright 2018 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index bb8af5d5ad5..2e29aac1e78 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 53002b57897..dcd28d9a89f 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index d48ed5b7fe7..eb59091db34 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 47f7ad89cb8..10ded4621ab 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 36105ad300a..fb939a71e38 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index cdc02594a19..41974713f99 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 6474d02f3d2..7b4ef81e0fc 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index ef959a6e251..f9ee6dcf25d 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index 8ee82ca2e95..6f51ade64e8 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 63a3e69adcd..2b20f23d7da 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index 6db4461a23a..f1141ae19d8 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 69879bff886..f8646da5d83 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. @@ -45,7 +45,7 @@ void main() { String prefix = '', String suffix = '', String copyright = - 'Copyright 2019 The Flutter Authors. All rights reserved.', + 'Copyright 2013 The Flutter Authors. All rights reserved.', List license = const [ 'Use of this source code is governed by a BSD-style license that can be', 'found in the LICENSE file.', @@ -171,8 +171,10 @@ void main() { throwsA(const TypeMatcher())); // Failure should give information about the problematic files. - expect(printedMessages, - contains('No copyright line was found for the following files:')); + expect( + printedMessages, + contains( + 'The license block for these files is missing or incorrect:')); expect(printedMessages, contains(' bad.cc')); expect(printedMessages, contains(' bad.h')); // Failure shouldn't print the success message. @@ -192,8 +194,10 @@ void main() { throwsA(const TypeMatcher())); // Failure should give information about the problematic files. - expect(printedMessages, - contains('No copyright line was found for the following files:')); + expect( + printedMessages, + contains( + 'The license block for these files is missing or incorrect:')); expect(printedMessages, contains(' bad.cc')); // Failure shouldn't print the success message. expect(printedMessages, @@ -212,8 +216,10 @@ void main() { throwsA(const TypeMatcher())); // Failure should give information about the problematic files. - expect(printedMessages, - contains('No recognized license was found for the following files:')); + expect( + printedMessages, + contains( + 'The license block for these files is missing or incorrect:')); expect(printedMessages, contains(' bad.cc')); // Failure shouldn't print the success message. expect(printedMessages, @@ -233,8 +239,7 @@ void main() { expect( printedMessages, contains( - 'The following files do not have a recognized first-party author ' - 'but are not in a "third_party/" directory:')); + 'The license block for these files is missing or incorrect:')); expect(printedMessages, contains(' third_party.cc')); // Failure shouldn't print the success message. expect(printedMessages, @@ -249,7 +254,12 @@ void main() { .childDirectory('third_party') .childFile('file.cc'); thirdPartyFile.createSync(recursive: true); - _writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); + _writeLicense(thirdPartyFile, + copyright: 'Copyright 2017 Workiva Inc.', + license: [ + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the License.' + ]); await runner.run(['license-check']); @@ -274,8 +284,10 @@ void main() { throwsA(const TypeMatcher())); // Failure should give information about the problematic files. - expect(printedMessages, - contains('No recognized license was found for the following files:')); + expect( + printedMessages, + contains( + 'No recognized license was found for the following third-party files:')); expect(printedMessages, contains(' third_party/bad.cc')); // Failure shouldn't print the success message. expect(printedMessages, @@ -291,10 +303,11 @@ void main() { bad.createSync(recursive: true); _writeLicense( bad, - copyright: 'Copyright 2017 Some New Authors', - license: [ - 'Licensed under the Apache License, Version 2.0', - ], + copyright: 'Copyright 2017 Some New Authors.', + license: [ + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the License.' + ], ); await expectLater(() => runner.run(['license-check']), @@ -302,7 +315,7 @@ void main() { // Failure should give information about the problematic files. expect(printedMessages, - contains('No recognized license was found for the following files:')); + contains('No recognized license was found for the following third-party files:')); expect(printedMessages, contains(' third_party/bad.cc')); // Failure shouldn't print the success message. expect(printedMessages, @@ -335,8 +348,7 @@ void main() { isNot(contains('All LICENSE files passed validation!'))); }); - test('ignores third-party LICENSE format', - () async { + test('ignores third-party LICENSE format', () async { File license = root.childDirectory('third_party').childFile('LICENSE'); license.createSync(recursive: true); license.writeAsStringSync(_incorrectLicenseFileText); @@ -351,7 +363,7 @@ void main() { } const String _correctLicenseFileText = - '''Copyright 2017 The Flutter Authors. All rights reserved. + '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -381,7 +393,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // A common incorrect version created by copying text intended for a code file, // with comment markers. const String _incorrectLicenseFileText = - '''// Copyright 2017 The Flutter Authors. All rights reserved. + '''// Copyright 2013 The Flutter Authors. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 44e94ee873e..5475641cba9 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index e9b68254fb5..19e9df0dd38 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index d5cfe4ec4f7..2ef9d72e363 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index b8ab1d25e53..9a0f1d6b6e6 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index eb9f3a9b0cf..66471263f66 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index e463b88a1ab..908b67b3cf7 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index 9e610d8a7a2..dc36c6c3228 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// 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. diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 3b76fa6ffa1..aa71c25835c 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Flutter Authors. All rights reserved. +// 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. From 514457de82ed5c8b62151f44f01244f0f89595fb Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 19 Mar 2021 14:11:06 -0700 Subject: [PATCH 018/249] Enable web integration tests in CI (#3738) --- .../tool/lib/src/drive_examples_command.dart | 38 +++++--- .../test/drive_examples_command_test.dart | 88 ++++++++++++++++++- 2 files changed, 110 insertions(+), 16 deletions(-) diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 529268ab4eb..de5dc9ebfc6 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -14,16 +14,18 @@ class DriveExamplesCommand extends PluginCommand { FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kAndroid, + help: 'Runs the Android implementation of the examples'); + argParser.addFlag(kIos, + help: 'Runs the iOS implementation of the examples'); argParser.addFlag(kLinux, help: 'Runs the Linux implementation of the examples'); argParser.addFlag(kMacos, help: 'Runs the macOS implementation of the examples'); + argParser.addFlag(kWeb, + help: 'Runs the web implementation of the examples'); argParser.addFlag(kWindows, help: 'Runs the Windows implementation of the examples'); - argParser.addFlag(kIos, - help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(kAndroid, - help: 'Runs the Android implementation of the examples'); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -51,6 +53,7 @@ class DriveExamplesCommand extends PluginCommand { final List failingTests = []; final bool isLinux = argResults[kLinux]; final bool isMacos = argResults[kMacos]; + final bool isWeb = argResults[kWeb]; final bool isWindows = argResults[kWindows]; await for (Directory plugin in getPlugins()) { final String flutterCommand = @@ -139,6 +142,13 @@ Tried searching for the following: 'macos', ]); } + if (isWeb && isWebPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'web-server', + '--browser-name=chrome', + ]); + } if (isWindows && isWindowsPlugin(plugin, fileSystem)) { driveArgs.addAll([ '-d', @@ -180,26 +190,30 @@ Tried searching for the following: Future pluginSupportedOnCurrentPlatform( FileSystemEntity plugin, FileSystem fileSystem) async { + final bool isAndroid = argResults[kAndroid]; + final bool isIOS = argResults[kIos]; final bool isLinux = argResults[kLinux]; final bool isMacos = argResults[kMacos]; + final bool isWeb = argResults[kWeb]; final bool isWindows = argResults[kWindows]; - final bool isIOS = argResults[kIos]; - final bool isAndroid = argResults[kAndroid]; + if (isAndroid) { + return (isAndroidPlugin(plugin, fileSystem)); + } + if (isIOS) { + return isIosPlugin(plugin, fileSystem); + } if (isLinux) { return isLinuxPlugin(plugin, fileSystem); } if (isMacos) { return isMacOsPlugin(plugin, fileSystem); } + if (isWeb) { + return isWebPlugin(plugin, fileSystem); + } if (isWindows) { return isWindowsPlugin(plugin, fileSystem); } - if (isIOS) { - return isIosPlugin(plugin, fileSystem); - } - if (isAndroid) { - return (isAndroidPlugin(plugin, fileSystem)); - } // When we are here, no flags are specified. Only return true if the plugin // supports Android for legacy command support. TODO(cyanglaz): Make Android // flag also required like other platforms (breaking change). diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 2b20f23d7da..65bdf99c165 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -357,13 +357,13 @@ void main() { ])); }); - test('driving when plugin does not suppport windows is a no-op', () async { + test('driving when plugin does not suppport web is a no-op', () async { createFakePlugin('plugin', withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], ], - isMacOsPlugin: false); + isWebPlugin: false); final Directory pluginExampleDirectory = mockPackagesDir.childDirectory('plugin').childDirectory('example'); @@ -372,7 +372,7 @@ void main() { final List output = await runCapturingPrint(runner, [ 'drive-examples', - '--windows', + '--web', ]); expect( @@ -384,11 +384,91 @@ void main() { ); print(processRunner.recordedCalls); - // Output should be empty since running drive-examples --windows on a non-windows + // Output should be empty since running drive-examples --web on a non-web // plugin is a no-op. expect(processRunner.recordedCalls, []); }); + test('driving a web plugin', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWebPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + String deviceTestPath = p.join('test_driver', 'plugin.dart'); + String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'web-server', + '--browser-name=chrome', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport Windows is a no-op', () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWindowsPlugin: false); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --windows on a + // non-Windows plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on a Windows plugin', () async { createFakePlugin('plugin', withExtraFiles: >[ From 116a0e2900815337d4d9c08f53a569b6ef438b31 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Fri, 19 Mar 2021 16:34:20 -0700 Subject: [PATCH 019/249] [ci] Run more web tests (#3739) This change enables the integration_tests of the following packages to run in Cirrus CI: * google_sign_in_web * connectivity_for_web * google_maps_flutter_web * url_launcher_web --- script/tool/lib/src/drive_examples_command.dart | 1 + script/tool/test/drive_examples_command_test.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index de5dc9ebfc6..54e0eee2642 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -146,6 +146,7 @@ Tried searching for the following: driveArgs.addAll([ '-d', 'web-server', + '--web-port=7357', '--browser-name=chrome', ]); } diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 65bdf99c165..58b8fb0d087 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -427,6 +427,7 @@ void main() { 'drive', '-d', 'web-server', + '--web-port=7357', '--browser-name=chrome', '--driver', driverTestPath, From 8143dc2673b42cde7cb866aee2ed2b0ae8c927b4 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 24 Mar 2021 20:20:04 +0100 Subject: [PATCH 020/249] [flutter_plugin_tools] Also look for Java tests in plugin path (#3742) --- script/tool/lib/src/java_test_command.dart | 9 +- script/tool/test/java_test_command_test.dart | 86 ++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 script/tool/test/java_test_command_test.dart diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index 45042c3c700..4b6a561b9e6 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -32,9 +32,12 @@ class JavaTestCommand extends PluginCommand { final Stream examplesWithTests = getExamples().where( (Directory d) => isFlutterPackage(d, fileSystem) && - fileSystem - .directory(p.join(d.path, 'android', 'app', 'src', 'test')) - .existsSync()); + (fileSystem + .directory(p.join(d.path, 'android', 'app', 'src', 'test')) + .existsSync() || + fileSystem + .directory(p.join(d.path, '..', 'android', 'src', 'test')) + .existsSync())); final List failingPackages = []; final List missingFlutterBuild = []; diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart new file mode 100644 index 00000000000..b036d7ec0c6 --- /dev/null +++ b/script/tool/test/java_test_command_test.dart @@ -0,0 +1,86 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/java_test_command.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$JavaTestCommand', () { + CommandRunner runner; + final RecordingProcessRunner processRunner = RecordingProcessRunner(); + + setUp(() { + initializeFakePackages(); + final JavaTestCommand command = JavaTestCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = + CommandRunner('java_test_test', 'Test for $JavaTestCommand'); + runner.addCommand(command); + }); + + tearDown(() { + cleanupPackages(); + processRunner.recordedCalls.clear(); + }); + + test('Should run Java tests in Android implementation folder', () async { + final Directory plugin = createFakePlugin( + 'plugin1', + isAndroidPlugin: true, + isFlutter: true, + withSingleExample: true, + withExtraFiles: >[ + ['example/android', 'gradlew'], + ['android/src/test', 'example_test.java'], + ], + ); + + await runner.run(['java-test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + p.join(plugin.path, 'example/android/gradlew'), + ['testDebugUnitTest', '--info'], + p.join(plugin.path, 'example/android'), + ), + ]), + ); + }); + + test('Should run Java tests in example folder', () async { + final Directory plugin = createFakePlugin( + 'plugin1', + isAndroidPlugin: true, + isFlutter: true, + withSingleExample: true, + withExtraFiles: >[ + ['example/android', 'gradlew'], + ['example/android/app/src/test', 'example_test.java'], + ], + ); + + await runner.run(['java-test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + p.join(plugin.path, 'example/android/gradlew'), + ['testDebugUnitTest', '--info'], + p.join(plugin.path, 'example/android'), + ), + ]), + ); + }); + }); +} From e8f4aea2986b8d9d084337da201efdca3149ccbd Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Wed, 24 Mar 2021 15:56:40 -0700 Subject: [PATCH 021/249] [ci] Do not use empty exclude directories in analyze_command. (#3748) * Add CUSTOM_ANALYSIS_PLUGINS back with all the plugins that now have a custom analysis_options.yaml --- script/tool/lib/src/analyze_command.dart | 6 ++++-- script/tool/test/analyze_command_test.dart | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index b2586c4b0fc..9489303f566 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -42,10 +42,12 @@ class AnalyzeCommand extends PluginCommand { continue; } - final bool whitelisted = argResults[_customAnalysisFlag].any( + final bool allowed = argResults[_customAnalysisFlag].any( (String directory) => + directory != null && + directory.isNotEmpty && p.isWithin(p.join(packagesDir.path, directory), file.path)); - if (whitelisted) { + if (allowed) { continue; } diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 7b4ef81e0fc..636d4794a48 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -93,5 +93,20 @@ void main() { pluginDir.path), ])); }); + + // See: https://github.com/flutter/flutter/issues/78994 + test('takes an empty allow list', () async { + await createFakePlugin('foo', withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + + await expectLater( + () => runner.run(['analyze', '--custom-analysis', '']), + throwsA(const TypeMatcher())); + }); }); } From b98ea9175ba167c5e64d59b739f03d9fd58a1d73 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 29 Mar 2021 07:27:40 -1000 Subject: [PATCH 022/249] Add tests for publish check tool command (#3760) --- .../tool/lib/src/publish_check_command.dart | 4 +- .../tool/test/publish_check_command_test.dart | 114 ++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 script/tool/test/publish_check_command_test.dart diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index dcd28d9a89f..fb57dfcd6dc 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -65,10 +65,10 @@ class PublishCheckCommand extends PluginCommand { } Future hasValidPublishCheckRun(Directory package) async { - final io.Process process = await io.Process.start( + final io.Process process = await processRunner.start( 'flutter', ['pub', 'publish', '--', '--dry-run'], - workingDirectory: package.path, + workingDirectory: package, ); final StringBuffer outputBuffer = StringBuffer(); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart new file mode 100644 index 00000000000..dbe6e2cfe54 --- /dev/null +++ b/script/tool/test/publish_check_command_test.dart @@ -0,0 +1,114 @@ +// 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:collection'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/publish_check_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$PublishCheckProcessRunner tests', () { + PublishCheckProcessRunner processRunner; + CommandRunner runner; + + setUp(() { + initializeFakePackages(); + processRunner = PublishCheckProcessRunner(); + final PublishCheckCommand publishCheckCommand = PublishCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(publishCheckCommand); + }); + + tearDown(() { + mockPackagesDir.deleteSync(recursive: true); + }); + + test('publish check all packages', () async { + final Directory plugin1Dir = await createFakePlugin('a'); + final Directory plugin2Dir = await createFakePlugin('b'); + + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + await runner.run(['publish-check']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', + ['pub', 'publish', '--', '--dry-run'], plugin1Dir.path), + ProcessCall('flutter', + ['pub', 'publish', '--', '--dry-run'], plugin2Dir.path), + ])); + }); + + test('fail on negative test', () async { + await createFakePlugin('a'); + + final MockProcess process = MockProcess(); + process.stdoutController.close(); // ignore: unawaited_futures + process.stderrController.close(); // ignore: unawaited_futures + process.exitCodeCompleter.complete(1); + + processRunner.processesToReturn.add(process); + + expect( + () => runner.run(['publish-check']), + throwsA(isA()), + ); + }); + + test('fail on bad pubspec', () async { + final Directory dir = await createFakePlugin('c'); + await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + + final MockProcess process = MockProcess(); + processRunner.processesToReturn.add(process); + + expect(() => runner.run(['publish-check']), + throwsA(isA())); + }); + + test('pass on prerelease', () async { + await createFakePlugin('d'); + + final String preReleaseOutput = 'Package has 1 warning.' + 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; + + final MockProcess process = MockProcess(); + process.stdoutController.add(preReleaseOutput.codeUnits); + process.stdoutController.close(); // ignore: unawaited_futures + process.stderrController.close(); // ignore: unawaited_futures + + process.exitCodeCompleter.complete(1); + + processRunner.processesToReturn.add(process); + + expect(runner.run(['publish-check']), completes); + }); + }); +} + +class PublishCheckProcessRunner extends RecordingProcessRunner { + final Queue processesToReturn = Queue(); + + @override + io.Process get processToReturn => processesToReturn.removeFirst(); +} From b8b7ef5cd5cf788d814cf7e4dbf3e7ae8deb6926 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 30 Mar 2021 13:34:20 -0700 Subject: [PATCH 023/249] Update build-all test for null-safe template (#3773) Flutter master now creates NNBD code when running 'flutter create', so the generated pubspec for build_all needs to use a compatible SDK version. This updates from 2.0.0 to 2.12.0. Also includes a test for this, which involved setting up tests for the file, and doing some refactoring to make the command testable. As a result, this fixes https://github.com/flutter/flutter/issues/61049 (although more test backfill is needed). --- .../src/create_all_plugins_app_command.dart | 53 ++++++----- .../create_all_plugins_app_command_test.dart | 91 +++++++++++++++++++ script/tool/test/util.dart | 15 ++- 3 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 script/tool/test/create_all_plugins_app_command_test.dart 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 f1f11d15ca9..aaa7f7fb966 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -12,11 +12,21 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common.dart'; -// TODO(cyanglaz): Add tests for this command. -// https://github.com/flutter/flutter/issues/61049 class CreateAllPluginsAppCommand extends PluginCommand { - CreateAllPluginsAppCommand(Directory packagesDir, FileSystem fileSystem) - : super(packagesDir, fileSystem); + CreateAllPluginsAppCommand( + Directory packagesDir, + FileSystem fileSystem, { + this.pluginsRoot, + }) : super(packagesDir, fileSystem) { + pluginsRoot ??= fileSystem.currentDirectory; + appDirectory = pluginsRoot.childDirectory('all_plugins'); + } + + /// The root directory of the plugin repository. + Directory pluginsRoot; + + /// The location of the synthesized app project. + Directory appDirectory; @override String get description => @@ -27,7 +37,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { @override Future run() async { - final int exitCode = await _createPlugin(); + final int exitCode = await _createApp(); if (exitCode != 0) { throw ToolExit(exitCode); } @@ -39,7 +49,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { ]); } - Future _createPlugin() async { + Future _createApp() async { final io.ProcessResult result = io.Process.runSync( 'flutter', [ @@ -47,7 +57,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { '--template=app', '--project-name=all_plugins', '--android-language=java', - './all_plugins', + appDirectory.path, ], ); @@ -57,12 +67,10 @@ class CreateAllPluginsAppCommand extends PluginCommand { } Future _updateAppGradle() async { - final File gradleFile = fileSystem.file(p.join( - 'all_plugins', - 'android', - 'app', - 'build.gradle', - )); + final File gradleFile = appDirectory + .childDirectory('android') + .childDirectory('app') + .childFile('build.gradle'); if (!gradleFile.existsSync()) { throw ToolExit(64); } @@ -86,14 +94,12 @@ class CreateAllPluginsAppCommand extends PluginCommand { } Future _updateManifest() async { - final File manifestFile = fileSystem.file(p.join( - 'all_plugins', - 'android', - 'app', - 'src', - 'main', - 'AndroidManifest.xml', - )); + final File manifestFile = appDirectory + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childFile('AndroidManifest.xml'); if (!manifestFile.existsSync()) { throw ToolExit(64); } @@ -124,7 +130,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { version: Version.parse('1.0.0+1'), environment: { 'sdk': VersionConstraint.compatibleWith( - Version.parse('2.0.0'), + Version.parse('2.12.0'), ), }, dependencies: { @@ -135,8 +141,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { }, dependencyOverrides: pluginDeps, ); - final File pubspecFile = - fileSystem.file(p.join('all_plugins', 'pubspec.yaml')); + final File pubspecFile = appDirectory.childFile('pubspec.yaml'); pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); } diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart new file mode 100644 index 00000000000..58f24c9a3de --- /dev/null +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -0,0 +1,91 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/create_all_plugins_app_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$CreateAllPluginsAppCommand', () { + CommandRunner runner; + FileSystem fileSystem; + Directory testRoot; + Directory packagesDir; + Directory appDir; + + setUp(() { + // Since the core of this command is a call to 'flutter create', the test + // has to use the real filesystem. Put everything possible in a unique + // temporary to minimize affect on the host system. + fileSystem = LocalFileSystem(); + testRoot = fileSystem.systemTempDirectory.createTempSync(); + packagesDir = testRoot.childDirectory('packages'); + + final CreateAllPluginsAppCommand command = CreateAllPluginsAppCommand( + packagesDir, + fileSystem, + pluginsRoot: testRoot, + ); + appDir = command.appDirectory; + runner = CommandRunner( + 'create_all_test', 'Test for $CreateAllPluginsAppCommand'); + runner.addCommand(command); + }); + + tearDown(() { + testRoot.deleteSync(recursive: true); + }); + + test('pubspec includes all plugins', () async { + createFakePlugin('plugina', packagesDirectory: packagesDir); + createFakePlugin('pluginb', packagesDirectory: packagesDir); + createFakePlugin('pluginc', packagesDirectory: packagesDir); + + await runner.run(['all-plugins-app']); + final List pubspec = + appDir.childFile('pubspec.yaml').readAsLinesSync(); + + expect( + pubspec, + containsAll([ + contains(RegExp('path: .*/packages/plugina')), + contains(RegExp('path: .*/packages/pluginb')), + contains(RegExp('path: .*/packages/pluginc')), + ])); + }); + + test('pubspec has overrides for all plugins', () async { + createFakePlugin('plugina', packagesDirectory: packagesDir); + createFakePlugin('pluginb', packagesDirectory: packagesDir); + createFakePlugin('pluginc', packagesDirectory: packagesDir); + + await runner.run(['all-plugins-app']); + final List pubspec = + appDir.childFile('pubspec.yaml').readAsLinesSync(); + + expect( + pubspec, + containsAllInOrder([ + contains('dependency_overrides:'), + contains(RegExp('path: .*/packages/plugina')), + contains(RegExp('path: .*/packages/pluginb')), + contains(RegExp('path: .*/packages/pluginc')), + ])); + }); + + test('pubspec is compatible with null-safe app code', () async { + createFakePlugin('plugina', packagesDirectory: packagesDir); + + await runner.run(['all-plugins-app']); + final String pubspec = + appDir.childFile('pubspec.yaml').readAsStringSync(); + + expect(pubspec, contains(RegExp('sdk:\\s*(?:["\']>=|[^])2\\.12\\.'))); + }); + }); +} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 908b67b3cf7..d02b756ee9c 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -12,6 +12,9 @@ import 'package:platform/platform.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:quiver/collection.dart'; +// TODO(stuartmorgan): Eliminate this in favor of setting up a clean filesystem +// for each test, to eliminate the chance of files from one test interfering +// with another test. FileSystem mockFileSystem = MemoryFileSystem( style: LocalPlatform().isWindows ? FileSystemStyle.windows @@ -28,7 +31,8 @@ void initializeFakePackages({Directory parentDir}) { mockPackagesDir.createSync(); } -/// Creates a plugin package with the given [name] in [mockPackagesDir]. +/// Creates a plugin package with the given [name] in [packagesDirectory], +/// defaulting to [mockPackagesDir]. Directory createFakePlugin( String name, { bool withSingleExample = false, @@ -44,13 +48,16 @@ Directory createFakePlugin( bool includeChangeLog = false, bool includeVersion = false, String parentDirectoryName = '', + Directory packagesDirectory, }) { assert(!(withSingleExample && withExamples.isNotEmpty), 'cannot pass withSingleExample and withExamples simultaneously'); - final Directory pluginDirectory = (parentDirectoryName != '') - ? mockPackagesDir.childDirectory(parentDirectoryName).childDirectory(name) - : mockPackagesDir.childDirectory(name); + Directory parentDirectory = packagesDirectory ?? mockPackagesDir; + if (parentDirectoryName != '') { + parentDirectory = parentDirectory.childDirectory(parentDirectoryName); + } + final Directory pluginDirectory = parentDirectory.childDirectory(name); pluginDirectory.createSync(recursive: true); createFakePubspec( From 5ea4d5fdfc4036363df5739e787e66accfd66ca3 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 5 Apr 2021 13:54:05 -0700 Subject: [PATCH 024/249] [tool] refactor publish plugin command (#3779) --- .../tool/lib/src/publish_plugin_command.dart | 92 +++++++++++-------- .../test/publish_plugin_command_test.dart | 8 +- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index eb59091db34..d00400294ab 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -85,15 +85,24 @@ class PublishPluginCommand extends PluginCommand { final Print _print; final Stdin _stdin; // The directory of the actual package that we are publishing. - Directory _packageDir; StreamSubscription _stdinSubscription; @override Future run() async { checkSharding(); + final String package = argResults[_packageOption]; + if (package == null) { + _print( + 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); + throw ToolExit(1); + } + _print('Checking local repo...'); - _packageDir = _checkPackageDir(); - await _checkGitStatus(); + if (!await GitDir.isGitDir(packagesDir.path)) { + _print('$packagesDir is not a valid Git repository.'); + throw ToolExit(1); + } + final bool shouldPushTag = argResults[_pushTagsOption]; final String remote = argResults[_remoteOption]; String remoteUrl; @@ -102,23 +111,39 @@ class PublishPluginCommand extends PluginCommand { } _print('Local repo is ready!'); - await _publish(); - _print('Package published!'); - if (!argResults[_tagReleaseOption]) { - return await _finishSuccesfully(); + final Directory packageDir = _getPackageDir(package); + await _publishPlugin(packageDir: packageDir); + if (argResults[_tagReleaseOption] as bool) { + await _tagRelease( + packageDir: packageDir, + remote: remote, + remoteUrl: remoteUrl, + shouldPushTag: shouldPushTag); } + await _finishSuccesfully(); + } - _print('Tagging release...'); - final String tag = _getTag(); + Future _publishPlugin({@required Directory packageDir}) async { + await _checkGitStatus(packageDir); + await _publish(packageDir); + _print('Package published!'); + } + + Future _tagRelease( + {@required Directory packageDir, + @required String remote, + @required String remoteUrl, + @required bool shouldPushTag}) async { + final String tag = _getTag(packageDir); + _print('Tagging release $tag...'); await processRunner.runAndExitOnError('git', ['tag', tag], - workingDir: _packageDir); + workingDir: packageDir); if (!shouldPushTag) { - return await _finishSuccesfully(); + return; } _print('Pushing tag to $remote...'); await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); - await _finishSuccesfully(); } Future _finishSuccesfully() async { @@ -126,36 +151,28 @@ class PublishPluginCommand extends PluginCommand { _print('Done!'); } - Directory _checkPackageDir() { - final String package = argResults[_packageOption]; - if (package == null) { - _print( - 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); - throw ToolExit(1); - } - final Directory _packageDir = packagesDir.childDirectory(package); - if (!_packageDir.existsSync()) { - _print('${_packageDir.absolute.path} does not exist.'); + // Returns the packageDirectory based on the package name. + // Throws ToolExit if the `package` doesn't exist. + Directory _getPackageDir(String package) { + final Directory packageDir = packagesDir.childDirectory(package); + if (!packageDir.existsSync()) { + _print('${packageDir.absolute.path} does not exist.'); throw ToolExit(1); } - return _packageDir; + return packageDir; } - Future _checkGitStatus() async { - if (!await GitDir.isGitDir(packagesDir.path)) { - _print('$packagesDir is not a valid Git repository.'); - throw ToolExit(1); - } - + Future _checkGitStatus(Directory packageDir) async { final ProcessResult statusResult = await processRunner.runAndExitOnError( 'git', [ 'status', '--porcelain', '--ignored', - _packageDir.absolute.path + packageDir.absolute.path ], - workingDir: _packageDir); + workingDir: packageDir); + final String statusOutput = statusResult.stdout; if (statusOutput.isNotEmpty) { _print( @@ -169,17 +186,17 @@ class PublishPluginCommand extends PluginCommand { Future _verifyRemote(String remote) async { final ProcessResult remoteInfo = await processRunner.runAndExitOnError( 'git', ['remote', 'get-url', remote], - workingDir: _packageDir); + workingDir: packagesDir); return remoteInfo.stdout; } - Future _publish() async { + Future _publish(Directory packageDir) async { final List publishFlags = argResults[_pubFlagsOption]; _print( - 'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n'); + 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); final Process publish = await processRunner.start( 'flutter', ['pub', 'publish'] + publishFlags, - workingDirectory: _packageDir); + workingDirectory: packageDir); publish.stdout .transform(utf8.decoder) .listen((String data) => _print(data)); @@ -196,9 +213,9 @@ class PublishPluginCommand extends PluginCommand { } } - String _getTag() { + String _getTag(Directory packageDir) { final File pubspecFile = - fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml')); + fileSystem.file(p.join(packageDir.path, 'pubspec.yaml')); final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); final String name = pubspecYaml['name']; final String version = pubspecYaml['version']; @@ -220,7 +237,6 @@ class PublishPluginCommand extends PluginCommand { _print('Tag push canceled.'); throw ToolExit(1); } - await processRunner.runAndExitOnError('git', ['push', remote, tag], workingDir: packagesDir); } diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 9a0f1d6b6e6..cfa40b9dc0a 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -50,7 +50,7 @@ void main() { mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand( - mockPackagesDir, const LocalFileSystem(), + mockPackagesDir, mockPackagesDir.fileSystem, processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString()), stdinput: mockStdin)); @@ -65,7 +65,6 @@ void main() { test('requires a package flag', () async { await expectLater(() => commandRunner.run(['publish-plugin']), throwsA(const TypeMatcher())); - expect( printedMessages.last, contains("Must specify a package to publish.")); }); @@ -73,7 +72,7 @@ void main() { test('requires an existing flag', () async { await expectLater( () => commandRunner - .run(['publish-plugin', '--package', 'iamerror']), + .run(['publish-plugin', '--package', 'iamerror', '--no-push-tags']), throwsA(const TypeMatcher())); expect(printedMessages.last, contains('iamerror does not exist')); @@ -84,7 +83,7 @@ void main() { await expectLater( () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), + .run(['publish-plugin', '--package', testPluginName, '--no-push-tags']), throwsA(const TypeMatcher())); expect( @@ -98,7 +97,6 @@ void main() { () => commandRunner .run(['publish-plugin', '--package', testPluginName]), throwsA(const TypeMatcher())); - expect(processRunner.results.last.stderr, contains("No such remote")); }); From 3d9e5232180d6fc71567675e23fa8a4c91976243 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 6 Apr 2021 13:04:25 -0700 Subject: [PATCH 025/249] Switch script/tools over to the new analysis options (#3777) Removes the legacy analysis options override and fixes all resulting issues. This is a combination of dart fix and manual changes (mostly mechanical, but some small restructuring to address warnings more cleanly, such as creating typed structs from args when they are used repeatedly to avoid repeated casting, or making things that were unnecessarily public private). One small opportunistic extra cleanup is that the handling of null-safety prerelease versions is removed, as any new plugin would be written null-safe from the start, so we no longer need to allow those versions. Part of flutter/flutter#76229 --- script/tool/analysis_options.yaml | 1 - script/tool/lib/src/analyze_command.dart | 18 +- .../tool/lib/src/build_examples_command.dart | 59 +++--- script/tool/lib/src/common.dart | 83 ++++---- .../src/create_all_plugins_app_command.dart | 16 +- .../tool/lib/src/drive_examples_command.dart | 56 +++--- .../lib/src/firebase_test_lab_command.dart | 39 ++-- script/tool/lib/src/format_command.dart | 43 +++-- script/tool/lib/src/java_test_command.dart | 11 +- .../tool/lib/src/license_check_command.dart | 15 +- .../tool/lib/src/lint_podspecs_command.dart | 33 ++-- script/tool/lib/src/list_command.dart | 16 +- script/tool/lib/src/main.dart | 6 +- .../tool/lib/src/publish_check_command.dart | 27 +-- .../tool/lib/src/publish_plugin_command.dart | 24 +-- script/tool/lib/src/test_command.dart | 13 +- .../tool/lib/src/version_check_command.dart | 77 +++----- script/tool/lib/src/xctest_command.dart | 51 ++--- script/tool/test/analyze_command_test.dart | 37 ++-- .../test/build_examples_command_test.dart | 22 +-- script/tool/test/common_test.dart | 48 ++--- .../create_all_plugins_app_command_test.dart | 6 +- .../test/drive_examples_command_test.dart | 37 ++-- script/tool/test/firebase_test_lab_test.dart | 8 +- script/tool/test/java_test_command_test.dart | 8 +- .../tool/test/license_check_command_test.dart | 97 +++++----- .../tool/test/lint_podspecs_command_test.dart | 22 +-- script/tool/test/list_command_test.dart | 4 +- script/tool/test/mocks.dart | 2 +- .../tool/test/publish_check_command_test.dart | 28 +-- .../test/publish_plugin_command_test.dart | 34 ++-- script/tool/test/test_command_test.dart | 31 +-- script/tool/test/util.dart | 33 ++-- script/tool/test/version_check_test.dart | 181 ++++++++---------- script/tool/test/xctest_command_test.dart | 141 +++++++------- 35 files changed, 675 insertions(+), 652 deletions(-) delete mode 100644 script/tool/analysis_options.yaml diff --git a/script/tool/analysis_options.yaml b/script/tool/analysis_options.yaml deleted file mode 100644 index cda4f6e153e..00000000000 --- a/script/tool/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 9489303f566..e22bb0b17ba 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -9,7 +9,9 @@ import 'package:path/path.dart' as p; import 'common.dart'; +/// A command to run Dart analysis on packages. class AnalyzeCommand extends PluginCommand { + /// Creates a analysis command instance. AnalyzeCommand( Directory packagesDir, FileSystem fileSystem, { @@ -31,9 +33,7 @@ class AnalyzeCommand extends PluginCommand { 'This command requires "pub" and "flutter" to be in your path.'; @override - Future run() async { - checkSharding(); - + Future run() async { print('Verifying analysis settings...'); final List files = packagesDir.listSync(recursive: true); for (final FileSystemEntity file in files) { @@ -42,8 +42,8 @@ class AnalyzeCommand extends PluginCommand { continue; } - final bool allowed = argResults[_customAnalysisFlag].any( - (String directory) => + final bool allowed = (argResults[_customAnalysisFlag] as List) + .any((String directory) => directory != null && directory.isNotEmpty && p.isWithin(p.join(packagesDir.path, directory), file.path)); @@ -62,7 +62,7 @@ class AnalyzeCommand extends PluginCommand { 'pub', ['global', 'activate', 'tuneup'], workingDir: packagesDir, exitOnError: true); - await for (Directory package in getPackages()) { + await for (final Directory package in getPackages()) { if (isFlutterPackage(package, fileSystem)) { await processRunner.runAndStream('flutter', ['packages', 'get'], workingDir: package, exitOnError: true); @@ -73,7 +73,7 @@ class AnalyzeCommand extends PluginCommand { } final List failingPackages = []; - await for (Directory package in getPlugins()) { + await for (final Directory package in getPlugins()) { final int exitCode = await processRunner.runAndStream( 'pub', ['global', 'run', 'tuneup', 'check'], workingDir: package); @@ -85,9 +85,9 @@ class AnalyzeCommand extends PluginCommand { print('\n\n'); if (failingPackages.isNotEmpty) { print('The following packages have analyzer errors (see above):'); - failingPackages.forEach((String package) { + for (final String package in failingPackages) { print(' * $package'); - }); + } throw ToolExit(1); } diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 966fdc9b99d..0069c058681 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -11,7 +11,9 @@ import 'package:platform/platform.dart'; import 'common.dart'; +/// A command to build the example applications for packages. class BuildExamplesCommand extends PluginCommand { + /// Creates an instance of the build command. BuildExamplesCommand( Directory packagesDir, FileSystem fileSystem, { @@ -39,33 +41,40 @@ class BuildExamplesCommand extends PluginCommand { 'This command requires "flutter" to be in your path.'; @override - Future run() async { - if (!argResults[kIpa] && - !argResults[kApk] && - !argResults[kLinux] && - !argResults[kMacos] && - !argResults[kWeb] && - !argResults[kWindows]) { - print('None of --linux, --macos, --web, --windows, --apk, or --ipa were ' - 'specified, so not building anything.'); + Future run() async { + final List platformSwitches = [ + kApk, + kIpa, + kLinux, + kMacos, + kWeb, + kWindows, + ]; + final Map platforms = { + for (final String platform in platformSwitches) + platform: argResults[platform] as bool + }; + if (!platforms.values.any((bool enabled) => enabled)) { + print( + 'None of ${platformSwitches.map((String platform) => '--$platform').join(', ')} ' + 'were specified, so not building anything.'); return; } final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - final String enableExperiment = argResults[kEnableExperiment]; + final String enableExperiment = argResults[kEnableExperiment] as String; - checkSharding(); final List failingPackages = []; - await for (Directory plugin in getPlugins()) { - for (Directory example in getExamplesForPlugin(plugin)) { + await for (final Directory plugin in getPlugins()) { + for (final Directory example in getExamplesForPlugin(plugin)) { final String packageName = p.relative(example.path, from: packagesDir.path); - if (argResults[kLinux]) { + if (platforms[kLinux]) { print('\nBUILDING Linux for $packageName'); if (isLinuxPlugin(plugin, fileSystem)) { - int buildExitCode = await processRunner.runAndStream( + final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ 'build', @@ -82,10 +91,10 @@ class BuildExamplesCommand extends PluginCommand { } } - if (argResults[kMacos]) { + if (platforms[kMacos]) { print('\nBUILDING macOS for $packageName'); if (isMacOsPlugin(plugin, fileSystem)) { - int exitCode = await processRunner.runAndStream( + final int exitCode = await processRunner.runAndStream( flutterCommand, [ 'build', @@ -102,10 +111,10 @@ class BuildExamplesCommand extends PluginCommand { } } - if (argResults[kWeb]) { + if (platforms[kWeb]) { print('\nBUILDING web for $packageName'); if (isWebPlugin(plugin, fileSystem)) { - int buildExitCode = await processRunner.runAndStream( + final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ 'build', @@ -122,10 +131,10 @@ class BuildExamplesCommand extends PluginCommand { } } - if (argResults[kWindows]) { + if (platforms[kWindows]) { print('\nBUILDING Windows for $packageName'); if (isWindowsPlugin(plugin, fileSystem)) { - int buildExitCode = await processRunner.runAndStream( + final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ 'build', @@ -142,7 +151,7 @@ class BuildExamplesCommand extends PluginCommand { } } - if (argResults[kIpa]) { + if (platforms[kIpa]) { print('\nBUILDING IPA for $packageName'); if (isIosPlugin(plugin, fileSystem)) { final int exitCode = await processRunner.runAndStream( @@ -163,7 +172,7 @@ class BuildExamplesCommand extends PluginCommand { } } - if (argResults[kApk]) { + if (platforms[kApk]) { print('\nBUILDING APK for $packageName'); if (isAndroidPlugin(plugin, fileSystem)) { final int exitCode = await processRunner.runAndStream( @@ -188,7 +197,7 @@ class BuildExamplesCommand extends PluginCommand { if (failingPackages.isNotEmpty) { print('The following build are failing (see above for details):'); - for (String package in failingPackages) { + for (final String package in failingPackages) { print(' * $package'); } throw ToolExit(1); diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index 7d3063a20f5..fc1fdca6a1c 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; import 'dart:math'; @@ -15,7 +16,9 @@ import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:yaml/yaml.dart'; -typedef void Print(Object object); +/// 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 kWindows = 'windows'; @@ -53,8 +56,9 @@ bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { try { final File pubspecFile = fileSystem.file(p.join(entity.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final YamlMap dependencies = pubspecYaml['dependencies']; + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap dependencies = pubspecYaml['dependencies'] as YamlMap; if (dependencies == null) { return false; } @@ -89,16 +93,17 @@ bool pluginSupportsPlatform( try { final File pubspecFile = fileSystem.file(p.join(entity.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final YamlMap flutterSection = pubspecYaml['flutter']; + 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']; + final YamlMap pluginSection = flutterSection['plugin'] as YamlMap; if (pluginSection == null) { return false; } - final YamlMap platforms = pluginSection['platforms']; + final YamlMap platforms = pluginSection['platforms'] as YamlMap; if (platforms == null) { // Legacy plugin specs are assumed to support iOS and Android. if (!pluginSection.containsKey('platforms')) { @@ -153,12 +158,16 @@ void printErrorAndExit({@required String errorMessage, int exitCode = 1}) { /// 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; } -abstract class PluginCommand extends Command { +/// 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.fileSystem, { @@ -230,23 +239,25 @@ abstract class PluginCommand extends Command { int _shardIndex; int _shardCount; + /// The shard of the overall command execution that this instance should run. int get shardIndex { if (_shardIndex == null) { - checkSharding(); + _checkSharding(); } return _shardIndex; } + /// The number of shards this command is divided into. int get shardCount { if (_shardCount == null) { - checkSharding(); + _checkSharding(); } return _shardCount; } - void checkSharding() { - final int shardIndex = int.tryParse(argResults[_shardIndexArg]); - final int shardCount = int.tryParse(argResults[_shardCountArg]); + void _checkSharding() { + final int shardIndex = int.tryParse(argResults[_shardIndexArg] as String); + final int shardCount = int.tryParse(argResults[_shardCountArg] as String); if (shardIndex == null) { usageException('$_shardIndexArg must be an integer'); } @@ -281,7 +292,7 @@ abstract class PluginCommand extends Command { final int start = min(shardIndex * shardSize, allPlugins.length); final int end = min(start + shardSize, allPlugins.length); - for (Directory plugin in allPlugins.sublist(start, end)) { + for (final Directory plugin in allPlugins.sublist(start, end)) { yield plugin; } } @@ -301,25 +312,28 @@ abstract class PluginCommand extends Command { /// "client library" package, which declares the API for the plugin, as /// well as one or more platform-specific implementations. Stream _getAllPlugins() async* { - Set plugins = Set.from(argResults[_pluginsArg]); + Set plugins = + Set.from(argResults[_pluginsArg] as List); final Set excludedPlugins = - Set.from(argResults[_excludeArg]); - final bool runOnChangedPackages = argResults[_runOnChangedPackagesArg]; + Set.from(argResults[_excludeArg] as List); + final bool runOnChangedPackages = + argResults[_runOnChangedPackagesArg] as bool; if (plugins.isEmpty && runOnChangedPackages) { plugins = await _getChangedPackages(); } - await for (FileSystemEntity entity + await for (final FileSystemEntity entity in packagesDir.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; + yield entity as Directory; } } else if (entity is Directory) { // Look for Dart packages under this top-level directory. - await for (FileSystemEntity subdir in entity.list(followLinks: false)) { + 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 @@ -334,7 +348,7 @@ abstract class PluginCommand extends Command { (plugins.isEmpty || plugins.contains(relativePath) || plugins.contains(basenamePath))) { - yield subdir; + yield subdir as Directory; } } } @@ -350,7 +364,7 @@ abstract class PluginCommand extends Command { /// Returns all Dart package folders (typically, plugin + example) of the /// plugins involved in this command execution. Stream getPackages() async* { - await for (Directory plugin in getPlugins()) { + await for (final Directory plugin in getPlugins()) { yield plugin; yield* plugin .list(recursive: true, followLinks: false) @@ -401,7 +415,7 @@ abstract class PluginCommand extends Command { /// Throws tool exit if [gitDir] nor root directory is a git directory. Future retrieveVersionFinder() async { final String rootDir = packagesDir.parent.absolute.path; - String baseSha = argResults[_kBaseSha]; + final String baseSha = argResults[_kBaseSha] as String; GitDir baseGitDir = gitDir; if (baseGitDir == null) { @@ -424,14 +438,14 @@ abstract class PluginCommand extends Command { final List allChangedFiles = await gitVersionFinder.getChangedFiles(); final Set packages = {}; - allChangedFiles.forEach((String path) { + 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.isNotEmpty) { final String changedPackages = packages.join(','); print(changedPackages); @@ -446,6 +460,7 @@ abstract class PluginCommand extends Command { /// 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. @@ -490,8 +505,8 @@ class ProcessRunner { Future run(String executable, List args, {Directory workingDir, bool exitOnError = false, - stdoutEncoding = io.systemEncoding, - stderrEncoding = io.systemEncoding}) async { + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding}) async { return io.Process.run(executable, args, workingDirectory: workingDir?.path, stdoutEncoding: stdoutEncoding, @@ -569,15 +584,15 @@ class GitVersionFinder { Future> getChangedFiles() async { final String baseSha = await _getBaseSha(); final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); + .runCommand(['diff', '--name-only', baseSha, 'HEAD']); print('Determine diff with base sha: $baseSha'); - final String changedFilesStdout = changedFilesCommand.stdout.toString() ?? ''; + final String changedFilesStdout = + changedFilesCommand.stdout.toString() ?? ''; if (changedFilesStdout.isEmpty) { return []; } - final List changedFiles = changedFilesStdout - .split('\n') - ..removeWhere((element) => element.isEmpty); + final List changedFiles = changedFilesStdout.split('\n') + ..removeWhere((String element) => element.isEmpty); return changedFiles.toList(); } @@ -585,8 +600,8 @@ class GitVersionFinder { Future getPackageVersion(String pubspecPath, String gitRef) async { final io.ProcessResult gitShow = await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); - final String fileContent = gitShow.stdout; - final String versionString = loadYaml(fileContent)['version']; + final String fileContent = gitShow.stdout as String; + final String versionString = loadYaml(fileContent)['version'] as String; return versionString == null ? null : Version.parse(versionString); } 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 aaa7f7fb966..4b5b4c6b8df 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -12,7 +12,9 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common.dart'; +/// A command to create an application that builds all in a single application. class CreateAllPluginsAppCommand extends PluginCommand { + /// Creates an instance of the builder command. CreateAllPluginsAppCommand( Directory packagesDir, FileSystem fileSystem, { @@ -36,7 +38,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { String get name => 'all-plugins-app'; @override - Future run() async { + Future run() async { final int exitCode = await _createApp(); if (exitCode != 0) { throw ToolExit(exitCode); @@ -76,7 +78,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { } final StringBuffer newGradle = StringBuffer(); - for (String line in gradleFile.readAsLinesSync()) { + for (final String line in gradleFile.readAsLinesSync()) { newGradle.writeln(line); if (line.contains('defaultConfig {')) { newGradle.writeln(' multiDexEnabled true'); @@ -105,7 +107,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { } final StringBuffer newManifest = StringBuffer(); - for (String line in manifestFile.readAsLinesSync()) { + for (final String line in manifestFile.readAsLinesSync()) { if (line.contains('package="com.example.all_plugins"')) { newManifest ..writeln('package="com.example.all_plugins"') @@ -149,7 +151,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { final Map pathDependencies = {}; - await for (Directory package in getPlugins()) { + await for (final Directory package in getPlugins()) { final String pluginName = package.path.split('/').last; final File pubspecFile = fileSystem.file(p.join(package.path, 'pubspec.yaml')); @@ -183,15 +185,15 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} String _pubspecMapString(Map values) { final StringBuffer buffer = StringBuffer(); - for (MapEntry entry in values.entries) { + for (final MapEntry entry in values.entries) { buffer.writeln(); if (entry.value is VersionConstraint) { buffer.write(' ${entry.key}: ${entry.value}'); } else if (entry.value is SdkDependency) { - final SdkDependency dep = entry.value; + final SdkDependency dep = entry.value as SdkDependency; buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); } else if (entry.value is PathDependency) { - final PathDependency dep = entry.value; + final PathDependency dep = entry.value as PathDependency; buffer.write(' ${entry.key}: \n path: ${dep.path}'); } else { throw UnimplementedError( diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 54e0eee2642..e52052a49ae 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -8,7 +8,9 @@ import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'common.dart'; +/// A command to run the example applications for packages via Flutter driver. class DriveExamplesCommand extends PluginCommand { + /// Creates an instance of the drive command. DriveExamplesCommand( Directory packagesDir, FileSystem fileSystem, { @@ -48,20 +50,19 @@ class DriveExamplesCommand extends PluginCommand { 'integration_test/*_test.dart.'; @override - Future run() async { - checkSharding(); + Future run() async { final List failingTests = []; - final bool isLinux = argResults[kLinux]; - final bool isMacos = argResults[kMacos]; - final bool isWeb = argResults[kWeb]; - final bool isWindows = argResults[kWindows]; - await for (Directory plugin in getPlugins()) { + final bool isLinux = argResults[kLinux] == true; + final bool isMacos = argResults[kMacos] == true; + final bool isWeb = argResults[kWeb] == true; + final bool isWindows = argResults[kWindows] == true; + await for (final Directory plugin in getPlugins()) { final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - for (Directory example in getExamplesForPlugin(plugin)) { + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + for (final Directory example in getExamplesForPlugin(plugin)) { final String packageName = p.relative(example.path, from: packagesDir.path); - if (!(await pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { + if (!(await _pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { continue; } final Directory driverTests = @@ -71,7 +72,7 @@ class DriveExamplesCommand extends PluginCommand { continue; } // Look for driver tests ending in _test.dart in test_driver/ - await for (FileSystemEntity test in driverTests.list()) { + await for (final FileSystemEntity test in driverTests.list()) { final String driverTestName = p.relative(test.path, from: driverTests.path); if (!driverTestName.endsWith('_test.dart')) { @@ -100,7 +101,7 @@ class DriveExamplesCommand extends PluginCommand { fileSystem.directory(p.join(example.path, 'integration_test')); if (await integrationTests.exists()) { - await for (FileSystemEntity integration_test + await for (final FileSystemEntity integration_test in integrationTests.list()) { if (!integration_test.basename.endsWith('_test.dart')) { continue; @@ -125,7 +126,8 @@ Tried searching for the following: final List driveArgs = ['drive']; - final String enableExperiment = argResults[kEnableExperiment]; + final String enableExperiment = + argResults[kEnableExperiment] as String; if (enableExperiment.isNotEmpty) { driveArgs.add('--enable-experiment=$enableExperiment'); } @@ -157,10 +159,10 @@ Tried searching for the following: ]); } - for (final targetPath in targetPaths) { + for (final String targetPath in targetPaths) { final int exitCode = await processRunner.runAndStream( flutterCommand, - [ + [ ...driveArgs, '--driver', p.join('test_driver', driverTestName), @@ -180,7 +182,7 @@ Tried searching for the following: if (failingTests.isNotEmpty) { print('The following driver tests are failing (see above for details):'); - for (String test in failingTests) { + for (final String test in failingTests) { print(' * $test'); } throw ToolExit(1); @@ -189,16 +191,16 @@ Tried searching for the following: print('All driver tests successful!'); } - Future pluginSupportedOnCurrentPlatform( + Future _pluginSupportedOnCurrentPlatform( FileSystemEntity plugin, FileSystem fileSystem) async { - final bool isAndroid = argResults[kAndroid]; - final bool isIOS = argResults[kIos]; - final bool isLinux = argResults[kLinux]; - final bool isMacos = argResults[kMacos]; - final bool isWeb = argResults[kWeb]; - final bool isWindows = argResults[kWindows]; + final bool isAndroid = argResults[kAndroid] == true; + final bool isIOS = argResults[kIos] == true; + final bool isLinux = argResults[kLinux] == true; + final bool isMacos = argResults[kMacos] == true; + final bool isWeb = argResults[kWeb] == true; + final bool isWindows = argResults[kWindows] == true; if (isAndroid) { - return (isAndroidPlugin(plugin, fileSystem)); + return isAndroidPlugin(plugin, fileSystem); } if (isIOS) { return isIosPlugin(plugin, fileSystem); @@ -216,9 +218,9 @@ Tried searching for the following: return isWindowsPlugin(plugin, fileSystem); } // When we are here, no flags are specified. Only return true if the plugin - // supports Android for legacy command support. TODO(cyanglaz): Make Android - // flag also required like other platforms (breaking change). - // https://github.com/flutter/flutter/issues/58285 + // supports Android for legacy command support. + // TODO(cyanglaz): Make Android flag also required like other platforms + // (breaking change). https://github.com/flutter/flutter/issues/58285 return isAndroidPlugin(plugin, fileSystem); } } diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 8e0abd50746..2998522da03 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -11,7 +11,9 @@ import 'package:uuid/uuid.dart'; import 'common.dart'; +/// A command to run tests via Firebase test lab. class FirebaseTestLabCommand extends PluginCommand { + /// Creates an instance of the test runner command. FirebaseTestLabCommand( Directory packagesDir, FileSystem fileSystem, { @@ -76,11 +78,11 @@ class FirebaseTestLabCommand extends PluginCommand { 'activate-service-account', '--key-file=${argResults['service-key']}', ]); - int exitCode = await processRunner.runAndStream('gcloud', [ + final int exitCode = await processRunner.runAndStream('gcloud', [ 'config', 'set', 'project', - argResults['project'], + argResults['project'] as String, ]); if (exitCode == 0) { _print('\nFirebase project configured.'); @@ -93,8 +95,7 @@ class FirebaseTestLabCommand extends PluginCommand { } @override - Future run() async { - checkSharding(); + Future run() async { final Stream packagesWithTests = getPackages().where( (Directory d) => isFlutterPackage(d, fileSystem) && @@ -107,7 +108,7 @@ class FirebaseTestLabCommand extends PluginCommand { final List missingFlutterBuild = []; int resultsCounter = 0; // We use a unique GCS bucket for each Firebase Test Lab run - await for (Directory package in packagesWithTests) { + await for (final Directory package in packagesWithTests) { // See https://github.com/flutter/flutter/issues/38983 final Directory exampleDirectory = @@ -119,7 +120,7 @@ class FirebaseTestLabCommand extends PluginCommand { final Directory androidDirectory = fileSystem.directory(p.join(exampleDirectory.path, 'android')); - final String enableExperiment = argResults[kEnableExperiment]; + final String enableExperiment = argResults[kEnableExperiment] as String; final String encodedEnableExperiment = Uri.encodeComponent('--enable-experiment=$enableExperiment'); @@ -166,16 +167,18 @@ class FirebaseTestLabCommand extends PluginCommand { // Look for tests recursively in folders that start with 'test' and that // live in the root or example folders. bool isTestDir(FileSystemEntity dir) { - return p.basename(dir.path).startsWith('test') || - p.basename(dir.path) == 'integration_test'; + return dir is Directory && + (p.basename(dir.path).startsWith('test') || + p.basename(dir.path) == 'integration_test'); } - final List testDirs = - package.listSync().where(isTestDir).toList(); + final List testDirs = + package.listSync().where(isTestDir).cast().toList(); final Directory example = fileSystem.directory(p.join(package.path, 'example')); - testDirs.addAll(example.listSync().where(isTestDir).toList()); - for (Directory testDir in testDirs) { + testDirs.addAll( + example.listSync().where(isTestDir).cast().toList()); + for (final Directory testDir in testDirs) { bool isE2ETest(FileSystemEntity file) { return file.path.endsWith('_e2e.dart') || (file.parent.basename == 'integration_test' && @@ -186,7 +189,7 @@ class FirebaseTestLabCommand extends PluginCommand { .listSync(recursive: true, followLinks: true) .where(isE2ETest) .toList(); - for (FileSystemEntity test in testFiles) { + for (final FileSystemEntity test in testFiles) { exitCode = await processRunner.runAndStream( p.join(androidDirectory.path, _gradleWrapper), [ @@ -205,7 +208,7 @@ class FirebaseTestLabCommand extends PluginCommand { continue; } final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; - final String testRunId = argResults['test-run-id']; + final String testRunId = argResults['test-run-id'] as String; final String resultsDir = 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; final List args = [ @@ -222,9 +225,9 @@ class FirebaseTestLabCommand extends PluginCommand { '--timeout', '5m', '--results-bucket=${argResults['results-bucket']}', - '--results-dir=${resultsDir}', + '--results-dir=$resultsDir', ]; - for (String device in argResults['device']) { + for (final String device in argResults['device'] as List) { args.addAll(['--device', device]); } exitCode = await processRunner.runAndStream('gcloud', args, @@ -243,14 +246,14 @@ class FirebaseTestLabCommand extends PluginCommand { _print( 'The instrumentation tests for the following packages are failing (see above for' 'details):'); - for (String package in failingPackages) { + for (final String package in failingPackages) { _print(' * $package'); } } if (missingFlutterBuild.isNotEmpty) { _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' 'the following packages before executing tests again:'); - for (String package in missingFlutterBuild) { + for (final String package in missingFlutterBuild) { _print(' * $package'); } } diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index d849dc9aa9d..19b6004d244 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -16,7 +16,9 @@ import 'common.dart'; const String _googleFormatterUrl = 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; +/// A command to format all package code. class FormatCommand extends PluginCommand { + /// Creates an instance of the format command. FormatCommand( Directory packagesDir, FileSystem fileSystem, { @@ -38,15 +40,14 @@ class FormatCommand extends PluginCommand { 'your path.'; @override - Future run() async { - checkSharding(); + Future run() async { final String googleFormatterPath = await _getGoogleFormatterPath(); await _formatDart(); await _formatJava(googleFormatterPath); await _formatCppAndObjectiveC(); - if (argResults['fail-on-change']) { + if (argResults['fail-on-change'] == true) { final bool modified = await _didModifyAnything(); if (modified) { throw ToolExit(1); @@ -61,15 +62,14 @@ class FormatCommand extends PluginCommand { print('\n\n'); - if (modifiedFiles.stdout.isEmpty) { + final String stdout = modifiedFiles.stdout as String; + if (stdout.isEmpty) { print('All files formatted correctly.'); return false; } print('These files are not formatted correctly (see diff below):'); - LineSplitter.split(modifiedFiles.stdout) - .map((String line) => ' $line') - .forEach(print); + LineSplitter.split(stdout).map((String line) => ' $line').forEach(print); print('\nTo fix run "pub global activate flutter_plugin_tools && ' 'pub global run flutter_plugin_tools format" or copy-paste ' @@ -83,33 +83,34 @@ class FormatCommand extends PluginCommand { return true; } - Future _formatCppAndObjectiveC() async { + Future _formatCppAndObjectiveC() async { print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); - final Iterable allFiles = [] - ..addAll(await _getFilesWithExtension('.h')) - ..addAll(await _getFilesWithExtension('.m')) - ..addAll(await _getFilesWithExtension('.mm')) - ..addAll(await _getFilesWithExtension('.cc')) - ..addAll(await _getFilesWithExtension('.cpp')); + final Iterable allFiles = [ + ...await _getFilesWithExtension('.h'), + ...await _getFilesWithExtension('.m'), + ...await _getFilesWithExtension('.mm'), + ...await _getFilesWithExtension('.cc'), + ...await _getFilesWithExtension('.cpp'), + ]; // Split this into multiple invocations to avoid a // 'ProcessException: Argument list too long'. final Iterable> batches = partition(allFiles, 100); - for (List batch in batches) { - await processRunner.runAndStream(argResults['clang-format'], - ['-i', '--style=Google']..addAll(batch), + for (final List batch in batches) { + await processRunner.runAndStream(argResults['clang-format'] as String, + ['-i', '--style=Google', ...batch], workingDir: packagesDir, exitOnError: true); } } - Future _formatJava(String googleFormatterPath) async { + Future _formatJava(String googleFormatterPath) async { print('Formatting all .java files...'); final Iterable javaFiles = await _getFilesWithExtension('.java'); await processRunner.runAndStream('java', - ['-jar', googleFormatterPath, '--replace']..addAll(javaFiles), + ['-jar', googleFormatterPath, '--replace', ...javaFiles], workingDir: packagesDir, exitOnError: true); } - Future _formatDart() async { + Future _formatDart() async { // This actually should be fine for non-Flutter Dart projects, no need to // specifically shell out to dartfmt -w in that case. print('Formatting all .dart files...'); @@ -119,7 +120,7 @@ class FormatCommand extends PluginCommand { 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); } else { await processRunner.runAndStream( - 'flutter', ['format']..addAll(dartFiles), + 'flutter', ['format', ...dartFiles], workingDir: packagesDir, exitOnError: true); } } diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index 4b6a561b9e6..5df97627cec 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -9,7 +9,9 @@ import 'package:path/path.dart' as p; import 'common.dart'; +/// A command to run the Java tests of Android plugins. class JavaTestCommand extends PluginCommand { + /// Creates an instance of the test runner. JavaTestCommand( Directory packagesDir, FileSystem fileSystem, { @@ -27,8 +29,7 @@ class JavaTestCommand extends PluginCommand { static const String _gradleWrapper = 'gradlew'; @override - Future run() async { - checkSharding(); + Future run() async { final Stream examplesWithTests = getExamples().where( (Directory d) => isFlutterPackage(d, fileSystem) && @@ -41,7 +42,7 @@ class JavaTestCommand extends PluginCommand { final List failingPackages = []; final List missingFlutterBuild = []; - await for (Directory example in examplesWithTests) { + await for (final Directory example in examplesWithTests) { final String packageName = p.relative(example.path, from: packagesDir.path); print('\nRUNNING JAVA TESTS for $packageName'); @@ -71,14 +72,14 @@ class JavaTestCommand extends PluginCommand { print( 'The Java tests for the following packages are failing (see above for' 'details):'); - for (String package in failingPackages) { + for (final String package in failingPackages) { print(' * $package'); } } if (missingFlutterBuild.isNotEmpty) { print('Run "pub global run flutter_plugin_tools build-examples --apk" on' 'the following packages before executing tests again:'); - for (String package in missingFlutterBuild) { + for (final String package in missingFlutterBuild) { print(' * $package'); } } diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 4f78e4e9663..adb2f931529 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -62,8 +62,8 @@ final List _thirdPartyLicenseBlockRegexes = [ // Slight variants are not accepted because they may prevent consolidation in // tools that assemble all licenses used in distributed applications. // standardized. -final String _fullBsdLicenseText = - '''Copyright 2013 The Flutter Authors. All rights reserved. +const String _fullBsdLicenseText = ''' +Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -110,7 +110,7 @@ class LicenseCheckCommand extends PluginCommand { 'Ensures that all code files have copyright/license blocks.'; @override - Future run() async { + Future run() async { final Iterable codeFiles = (await _getAllFiles()).where((File file) => _codeFileExtensions.contains(p.extension(file.path)) && !_shouldIgnoreFile(file)); @@ -160,7 +160,7 @@ class LicenseCheckCommand extends PluginCommand { if (_isThirdParty(file)) { if (!_thirdPartyLicenseBlockRegexes - .any((regex) => regex.hasMatch(content))) { + .any((RegExp regex) => regex.hasMatch(content))) { unrecognizedThirdPartyFiles.add(file); } } else { @@ -175,7 +175,8 @@ class LicenseCheckCommand extends PluginCommand { _print('\n'); // Sort by path for more usable output. - final pathCompare = (File a, File b) => a.path.compareTo(b.path); + final int Function(File, File) pathCompare = + (File a, File b) => a.path.compareTo(b.path); incorrectFirstPartyFiles.sort(pathCompare); unrecognizedThirdPartyFiles.sort(pathCompare); @@ -200,7 +201,7 @@ class LicenseCheckCommand extends PluginCommand { 'the new third-party license block.\n'); } - bool succeeded = + final bool succeeded = incorrectFirstPartyFiles.isEmpty && unrecognizedThirdPartyFiles.isEmpty; if (succeeded) { _print('All source files passed validation!'); @@ -230,7 +231,7 @@ class LicenseCheckCommand extends PluginCommand { 'Please ensure that they use the exact format used in this repository".\n'); } - bool succeeded = incorrectLicenseFiles.isEmpty; + final bool succeeded = incorrectLicenseFiles.isEmpty; if (succeeded) { _print('All LICENSE files passed validation!'); } diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 3261072d71a..1fe6b71cf11 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -12,19 +12,19 @@ import 'package:platform/platform.dart'; import 'common.dart'; -typedef void Print(Object object); - /// Lint the CocoaPod podspecs and run unit tests. /// /// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. class LintPodspecsCommand extends PluginCommand { + /// Creates an instance of the linter command. LintPodspecsCommand( Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), - this.platform = const LocalPlatform(), + Platform platform = const LocalPlatform(), Print print = print, - }) : _print = print, + }) : _platform = platform, + _print = print, super(packagesDir, fileSystem, processRunner: processRunner) { argParser.addMultiOption('skip', help: @@ -47,26 +47,24 @@ class LintPodspecsCommand extends PluginCommand { 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; - final Platform platform; + final Platform _platform; final Print _print; @override - Future run() async { - if (!platform.isMacOS) { + Future run() async { + if (!_platform.isMacOS) { _print('Detected platform is not macOS, skipping podspec lint'); return; } - checkSharding(); - await processRunner.runAndExitOnError('which', ['pod'], workingDir: packagesDir); _print('Starting podspec lint test'); final List failingPlugins = []; - for (File podspec in await _podspecsToLint()) { + for (final File podspec in await _podspecsToLint()) { if (!await _lintPodspec(podspec)) { failingPlugins.add(p.basenameWithoutExtension(podspec.path)); } @@ -75,9 +73,9 @@ class LintPodspecsCommand extends PluginCommand { _print('\n\n'); if (failingPlugins.isNotEmpty) { _print('The following plugins have podspec errors (see above):'); - failingPlugins.forEach((String plugin) { + for (final String plugin in failingPlugins) { _print(' * $plugin'); - }); + } throw ToolExit(1); } } @@ -86,7 +84,8 @@ class LintPodspecsCommand extends PluginCommand { final List podspecs = await getFiles().where((File entity) { final String filePath = entity.path; return p.extension(filePath) == '.podspec' && - !argResults['skip'].contains(p.basenameWithoutExtension(filePath)); + !(argResults['skip'] as List) + .contains(p.basenameWithoutExtension(filePath)); }).toList(); podspecs.sort( @@ -102,12 +101,14 @@ class LintPodspecsCommand extends PluginCommand { _print('Linting $podspecBasename'); // Lint plugin as framework (use_frameworks!). - final ProcessResult frameworkResult = await _runPodLint(podspecPath, libraryLint: true); + final ProcessResult frameworkResult = + await _runPodLint(podspecPath, libraryLint: true); _print(frameworkResult.stdout); _print(frameworkResult.stderr); // Lint plugin as library. - final ProcessResult libraryResult = await _runPodLint(podspecPath, libraryLint: false); + final ProcessResult libraryResult = + await _runPodLint(podspecPath, libraryLint: false); _print(libraryResult.stdout); _print(libraryResult.stderr); @@ -116,7 +117,7 @@ class LintPodspecsCommand extends PluginCommand { Future _runPodLint(String podspecPath, {bool libraryLint}) async { - final bool allowWarnings = argResults['ignore-warnings'] + final bool allowWarnings = (argResults['ignore-warnings'] as List) .contains(p.basenameWithoutExtension(podspecPath)); final List arguments = [ 'lib', diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 3571786ab7f..49302a91ad2 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -8,7 +8,10 @@ import 'package:file/file.dart'; import 'common.dart'; +/// A command to list different types of repository content. class ListCommand extends PluginCommand { + /// Creates an instance of the list command, whose behavior depends on the + /// 'type' argument it provides. ListCommand(Directory packagesDir, FileSystem fileSystem) : super(packagesDir, fileSystem) { argParser.addOption( @@ -32,26 +35,25 @@ class ListCommand extends PluginCommand { final String description = 'Lists packages or files'; @override - Future run() async { - checkSharding(); - switch (argResults[_type]) { + Future run() async { + switch (argResults[_type] as String) { case _plugin: - await for (Directory package in getPlugins()) { + await for (final Directory package in getPlugins()) { print(package.path); } break; case _example: - await for (Directory package in getExamples()) { + await for (final Directory package in getExamples()) { print(package.path); } break; case _package: - await for (Directory package in getPackages()) { + await for (final Directory package in getPackages()) { print(package.path); } break; case _file: - await for (File file in getFiles()) { + await for (final File file in getFiles()) { print(file.path); } break; diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 2e29aac1e78..6fba3b34b95 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -27,7 +27,7 @@ import 'version_check_command.dart'; import 'xctest_command.dart'; void main(List args) { - final FileSystem fileSystem = const LocalFileSystem(); + const FileSystem fileSystem = LocalFileSystem(); Directory packagesDir = fileSystem .directory(p.join(fileSystem.currentDirectory.path, 'packages')); @@ -41,7 +41,7 @@ void main(List args) { } } - final CommandRunner commandRunner = CommandRunner( + final CommandRunner commandRunner = CommandRunner( 'pub global run flutter_plugin_tools', 'Productivity utils for hosting multiple plugins within one repository.') ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) @@ -61,7 +61,7 @@ void main(List args) { ..addCommand(XCTestCommand(packagesDir, fileSystem)); commandRunner.run(args).catchError((Object e) { - final ToolExit toolExit = e; + final ToolExit toolExit = e as ToolExit; io.exit(toolExit.exitCode); }, test: (Object e) => e is ToolExit); } diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index fb57dfcd6dc..07bf0aa8667 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -11,7 +11,9 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common.dart'; +/// A command to check that packages are publishable via 'dart publish'. class PublishCheckCommand extends PluginCommand { + /// Creates an instance of the publish command. PublishCheckCommand( Directory packagesDir, FileSystem fileSystem, { @@ -26,12 +28,13 @@ class PublishCheckCommand extends PluginCommand { 'Checks to make sure that a plugin *could* be published.'; @override - Future run() async { - checkSharding(); + Future run() async { final List failedPackages = []; - await for (Directory plugin in getPlugins()) { - if (!(await passesPublishCheck(plugin))) failedPackages.add(plugin); + await for (final Directory plugin in getPlugins()) { + if (!(await _passesPublishCheck(plugin))) { + failedPackages.add(plugin); + } } if (failedPackages.isNotEmpty) { @@ -51,7 +54,7 @@ class PublishCheckCommand extends PluginCommand { print(passedMessage); } - Pubspec tryParsePubspec(Directory package) { + Pubspec _tryParsePubspec(Directory package) { final File pubspecFile = package.childFile('pubspec.yaml'); try { @@ -64,7 +67,7 @@ class PublishCheckCommand extends PluginCommand { } } - Future hasValidPublishCheckRun(Directory package) async { + Future _hasValidPublishCheckRun(Directory package) async { final io.Process process = await processRunner.start( 'flutter', ['pub', 'publish', '--', '--dry-run'], @@ -91,7 +94,9 @@ class PublishCheckCommand extends PluginCommand { onDone: () => stdInCompleter.complete(), ); - if (await process.exitCode == 0) return true; + if (await process.exitCode == 0) { + return true; + } await stdOutCompleter.future; await stdInCompleter.future; @@ -102,11 +107,11 @@ class PublishCheckCommand extends PluginCommand { 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); } - Future passesPublishCheck(Directory package) async { + Future _passesPublishCheck(Directory package) async { final String packageName = package.basename; print('Checking that $packageName can be published.'); - final Pubspec pubspec = tryParsePubspec(package); + final Pubspec pubspec = _tryParsePubspec(package); if (pubspec == null) { return false; } else if (pubspec.publishTo == 'none') { @@ -114,8 +119,8 @@ class PublishCheckCommand extends PluginCommand { return true; } - if (await hasValidPublishCheckRun(package)) { - print("Package $packageName is able to be published."); + if (await _hasValidPublishCheckRun(package)) { + print('Package $packageName is able to be published.'); return true; } else { print('Unable to publish $packageName'); diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index d00400294ab..0dae3a502be 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -26,6 +26,7 @@ import 'common.dart'; /// /// [processRunner], [print], and [stdin] can be overriden for easier testing. class PublishPluginCommand extends PluginCommand { + /// Creates an instance of the publish command. PublishPluginCommand( Directory packagesDir, FileSystem fileSystem, { @@ -88,9 +89,8 @@ class PublishPluginCommand extends PluginCommand { StreamSubscription _stdinSubscription; @override - Future run() async { - checkSharding(); - final String package = argResults[_packageOption]; + Future run() async { + final String package = argResults[_packageOption] as String; if (package == null) { _print( 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); @@ -103,8 +103,8 @@ class PublishPluginCommand extends PluginCommand { throw ToolExit(1); } - final bool shouldPushTag = argResults[_pushTagsOption]; - final String remote = argResults[_remoteOption]; + final bool shouldPushTag = argResults[_pushTagsOption] == true; + final String remote = argResults[_remoteOption] as String; String remoteUrl; if (shouldPushTag) { remoteUrl = await _verifyRemote(remote); @@ -173,7 +173,7 @@ class PublishPluginCommand extends PluginCommand { ], workingDir: packageDir); - final String statusOutput = statusResult.stdout; + final String statusOutput = statusResult.stdout as String; if (statusOutput.isNotEmpty) { _print( "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" @@ -187,11 +187,12 @@ class PublishPluginCommand extends PluginCommand { final ProcessResult remoteInfo = await processRunner.runAndExitOnError( 'git', ['remote', 'get-url', remote], workingDir: packagesDir); - return remoteInfo.stdout; + return remoteInfo.stdout as String; } Future _publish(Directory packageDir) async { - final List publishFlags = argResults[_pubFlagsOption]; + final List publishFlags = + argResults[_pubFlagsOption] as List; _print( 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); final Process publish = await processRunner.start( @@ -216,9 +217,10 @@ class PublishPluginCommand extends PluginCommand { String _getTag(Directory packageDir) { final File pubspecFile = fileSystem.file(p.join(packageDir.path, 'pubspec.yaml')); - final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); - final String name = pubspecYaml['name']; - final String version = pubspecYaml['version']; + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final String name = pubspecYaml['name'] as String; + final String version = pubspecYaml['version'] as String; // We should have failed to publish if these were unset. assert(name.isNotEmpty && version.isNotEmpty); return _tagFormat diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 10ded4621ab..bb4f9c12a77 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -9,7 +9,9 @@ import 'package:path/path.dart' as p; import 'common.dart'; +/// A command to run Dart unit tests for packages. class TestCommand extends PluginCommand { + /// Creates an instance of the test command. TestCommand( Directory packagesDir, FileSystem fileSystem, { @@ -30,10 +32,9 @@ class TestCommand extends PluginCommand { 'This command requires "flutter" to be in your path.'; @override - Future run() async { - checkSharding(); + Future run() async { final List failingPackages = []; - await for (Directory packageDir in getPackages()) { + await for (final Directory packageDir in getPackages()) { final String packageName = p.relative(packageDir.path, from: packagesDir.path); if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { @@ -43,7 +44,7 @@ class TestCommand extends PluginCommand { print('RUNNING $packageName tests...'); - final String enableExperiment = argResults[kEnableExperiment]; + final String enableExperiment = argResults[kEnableExperiment] as String; // `flutter test` automatically gets packages. `pub run test` does not. :( int exitCode = 0; @@ -90,9 +91,9 @@ class TestCommand extends PluginCommand { print('\n\n'); if (failingPackages.isNotEmpty) { print('Tests for the following packages are failing (see above):'); - failingPackages.forEach((String package) { + for (final String package in failingPackages) { print(' * $package'); - }); + } throw ToolExit(1); } diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index fb939a71e38..086fa548880 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -15,47 +15,32 @@ import 'common.dart'; const String _kBaseSha = 'base-sha'; +/// Categories of version change types. enum NextVersionType { + /// A breaking change. BREAKING_MAJOR, - MAJOR_NULLSAFETY_PRE_RELEASE, - MINOR_NULLSAFETY_PRE_RELEASE, + + /// A minor change (e.g., added feature). MINOR, + + /// A bugfix change. PATCH, - RELEASE, -} -Version getNextNullSafetyPreRelease(Version current, Version next) { - String nextNullsafetyPrerelease = 'nullsafety'; - if (current.isPreRelease && - current.preRelease.first is String && - current.preRelease.first == 'nullsafety') { - if (current.preRelease.length == 1) { - nextNullsafetyPrerelease = 'nullsafety.1'; - } else if (current.preRelease.length == 2 && - current.preRelease.last is int) { - nextNullsafetyPrerelease = 'nullsafety.${current.preRelease.last + 1}'; - } - } - return Version( - next.major, - next.minor, - next.patch, - pre: nextNullsafetyPrerelease, - ); + /// The release of an existing prerelease version. + RELEASE, } +/// Returns the set of allowed next versions, with their change type, for +/// [masterVersion]. +/// +/// [headVerison] is used to check whether this is a pre-1.0 version bump, as +/// those have different semver rules. @visibleForTesting Map getAllowedNextVersions( Version masterVersion, Version headVersion) { - final Version nextNullSafetyMajor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMajor); - final Version nextNullSafetyMinor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); final Map allowedNextVersions = { masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, - nextNullSafetyMajor: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE, - nextNullSafetyMinor: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE, masterVersion.nextMinor: NextVersionType.MINOR, masterVersion.nextPatch: NextVersionType.PATCH, }; @@ -65,7 +50,7 @@ Map getAllowedNextVersions( if (masterVersion.build.isEmpty) { nextBuildNumber = 1; } else { - final int currentBuildNumber = masterVersion.build.first; + final int currentBuildNumber = masterVersion.build.first as int; nextBuildNumber = currentBuildNumber + 1; } final Version preReleaseVersion = Version( @@ -80,21 +65,13 @@ Map getAllowedNextVersions( NextVersionType.BREAKING_MAJOR; allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; - - final Version nextNullSafetyMajor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); - final Version nextNullSafetyMinor = - getNextNullSafetyPreRelease(masterVersion, masterVersion.nextPatch); - - allowedNextVersions[nextNullSafetyMajor] = - NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE; - allowedNextVersions[nextNullSafetyMinor] = - NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE; } return allowedNextVersions; } +/// A command to validate version changes to packages. class VersionCheckCommand extends PluginCommand { + /// Creates an instance of the version check command. VersionCheckCommand( Directory packagesDir, FileSystem fileSystem, { @@ -113,14 +90,13 @@ class VersionCheckCommand extends PluginCommand { 'This command requires "pub" and "flutter" to be in your path.'; @override - Future run() async { - checkSharding(); + Future run() async { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); - final String baseSha = argResults[_kBaseSha]; + final String baseSha = argResults[_kBaseSha] as String; for (final String pubspecPath in changedPubspecs) { try { final File pubspecFile = fileSystem.file(pubspecPath); @@ -150,7 +126,8 @@ class VersionCheckCommand extends PluginCommand { printErrorAndExit(errorMessage: error); } - bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); + final bool isPlatformInterface = + pubspec.name.endsWith('_platform_interface'); if (isPlatformInterface && allowedNextVersions[headVersion] == NextVersionType.BREAKING_MAJOR) { @@ -164,7 +141,7 @@ class VersionCheckCommand extends PluginCommand { } } - await for (Directory plugin in getPlugins()) { + await for (final Directory plugin in getPlugins()) { await _checkVersionsMatch(plugin); } @@ -180,7 +157,7 @@ class VersionCheckCommand extends PluginCommand { final Pubspec pubspec = _tryParsePubspec(plugin); if (pubspec == null) { - final String error = 'Cannot parse version from pubspec.yaml'; + const String error = 'Cannot parse version from pubspec.yaml'; printErrorAndExit(errorMessage: error); } final Version fromPubspec = pubspec.version; @@ -189,16 +166,16 @@ class VersionCheckCommand extends PluginCommand { final File changelog = plugin.childFile('CHANGELOG.md'); final List lines = changelog.readAsLinesSync(); String firstLineWithText; - final Iterator iterator = lines.iterator; + final Iterator iterator = lines.iterator; while (iterator.moveNext()) { - if ((iterator.current as String).trim().isNotEmpty) { + if (iterator.current.trim().isNotEmpty) { firstLineWithText = iterator.current; break; } } // Remove all leading mark down syntax from the version line. final String versionString = firstLineWithText.split(' ').last; - Version fromChangeLog = Version.parse(versionString); + final Version fromChangeLog = Version.parse(versionString); if (fromChangeLog == null) { final String error = 'Cannot find version on the first line of ${plugin.path}/CHANGELOG.md'; @@ -213,14 +190,14 @@ The first version listed in CHANGELOG.md is $fromChangeLog. '''; printErrorAndExit(errorMessage: error); } - print('${packageName} passed version check'); + print('$packageName passed version check'); } Pubspec _tryParsePubspec(Directory package) { final File pubspecFile = package.childFile('pubspec.yaml'); try { - Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec == null) { final String error = 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}'; diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 41974713f99..64f85577dbc 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -22,6 +22,7 @@ const String _kFoundNoSimulatorsMessage = /// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcworkspace". /// The static analyzer is also run. class XCTestCommand extends PluginCommand { + /// Creates an instance of the test command. XCTestCommand( Directory packagesDir, FileSystem fileSystem, { @@ -46,10 +47,10 @@ class XCTestCommand extends PluginCommand { 'This command requires "flutter" and "xcrun" to be in your path.'; @override - Future run() async { - String destination = argResults[_kiOSDestination]; + Future run() async { + String destination = argResults[_kiOSDestination] as String; if (destination == null) { - String simulatorId = await _findAvailableIphoneSimulator(); + final String simulatorId = await _findAvailableIphoneSimulator(); if (simulatorId == null) { print(_kFoundNoSimulatorsMessage); throw ToolExit(1); @@ -57,12 +58,10 @@ class XCTestCommand extends PluginCommand { destination = 'id=$simulatorId'; } - checkSharding(); + final List skipped = argResults[_kSkip] as List; - final List skipped = argResults[_kSkip]; - - List failingPackages = []; - await for (Directory plugin in getPlugins()) { + final List failingPackages = []; + await for (final Directory plugin in getPlugins()) { // Start running for package. final String packageName = p.relative(plugin.path, from: packagesDir.path); @@ -77,7 +76,7 @@ class XCTestCommand extends PluginCommand { print('\n\n'); continue; } - for (Directory example in getExamplesForPlugin(plugin)) { + for (final Directory example in getExamplesForPlugin(plugin)) { // Running tests and static analyzer. print('Running tests and analyzer for $packageName ...'); int exitCode = await _runTests(true, destination, example); @@ -96,11 +95,11 @@ class XCTestCommand extends PluginCommand { // Command end, print reports. if (failingPackages.isEmpty) { - print("All XCTests have passed!"); + print('All XCTests have passed!'); } else { print( 'The following packages are failing XCTests (see above for details):'); - for (String package in failingPackages) { + for (final String package in failingPackages) { print(' * $package'); } throw ToolExit(1); @@ -110,8 +109,7 @@ class XCTestCommand extends PluginCommand { Future _runTests(bool runTests, String destination, Directory example) { final List xctestArgs = [ _kXcodeBuildCommand, - if (runTests) - 'test', + if (runTests) 'test', 'analyze', '-workspace', 'ios/Runner.xcworkspace', @@ -128,8 +126,8 @@ class XCTestCommand extends PluginCommand { final String completeTestCommand = '$_kXCRunCommand ${xctestArgs.join(' ')}'; print(completeTestCommand); - return processRunner - .runAndStream(_kXCRunCommand, xctestArgs, workingDir: example, exitOnError: false); + return processRunner.runAndStream(_kXCRunCommand, xctestArgs, + workingDir: example, exitOnError: false); } Future _findAvailableIphoneSimulator() async { @@ -151,30 +149,35 @@ class XCTestCommand extends PluginCommand { throw ToolExit(1); } final Map simulatorListJson = - jsonDecode(findSimulatorsResult.stdout); - final List runtimes = simulatorListJson['runtimes']; - final Map devices = simulatorListJson['devices']; + jsonDecode(findSimulatorsResult.stdout as String) + as Map; + final List> runtimes = + (simulatorListJson['runtimes'] as List) + .cast>(); + final Map devices = + simulatorListJson['devices'] as Map; if (runtimes.isEmpty || devices.isEmpty) { return null; } String id; // Looking for runtimes, trying to find one with highest OS version. - for (Map runtimeMap in runtimes.reversed) { - if (!runtimeMap['name'].contains('iOS')) { + for (final Map runtimeMap in runtimes.reversed) { + if (!(runtimeMap['name'] as String).contains('iOS')) { continue; } - final String runtimeID = runtimeMap['identifier']; - final List devicesForRuntime = devices[runtimeID]; + final String runtimeID = runtimeMap['identifier'] as String; + final List> devicesForRuntime = + (devices[runtimeID] as List).cast>(); if (devicesForRuntime.isEmpty) { continue; } // Looking for runtimes, trying to find latest version of device. - for (Map device in devicesForRuntime.reversed) { + for (final Map device in devicesForRuntime.reversed) { if (device['availabilityError'] != null || (device['isAvailable'] as bool == false)) { continue; } - id = device['udid']; + id = device['udid'] as String; print('device selected: $device'); return id; } diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 636d4794a48..7b23ea9778e 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -13,7 +13,7 @@ import 'util.dart'; void main() { RecordingProcessRunner processRunner; - CommandRunner runner; + CommandRunner runner; setUp(() { initializeFakePackages(); @@ -22,7 +22,7 @@ void main() { mockPackagesDir, mockFileSystem, processRunner: processRunner); - runner = CommandRunner('analyze_command', 'Test for analyze_command'); + runner = CommandRunner('analyze_command', 'Test for analyze_command'); runner.addCommand(analyzeCommand); }); @@ -31,8 +31,8 @@ void main() { }); test('analyzes all packages', () async { - final Directory plugin1Dir = await createFakePlugin('a'); - final Directory plugin2Dir = await createFakePlugin('b'); + final Directory plugin1Dir = createFakePlugin('a'); + final Directory plugin2Dir = createFakePlugin('b'); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -42,20 +42,22 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('pub', ['global', 'activate', 'tuneup'], + ProcessCall('pub', const ['global', 'activate', 'tuneup'], mockPackagesDir.path), - ProcessCall('flutter', ['packages', 'get'], plugin1Dir.path), - ProcessCall('flutter', ['packages', 'get'], plugin2Dir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + ProcessCall( + 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['packages', 'get'], plugin2Dir.path), + ProcessCall('pub', const ['global', 'run', 'tuneup', 'check'], plugin1Dir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + ProcessCall('pub', const ['global', 'run', 'tuneup', 'check'], plugin2Dir.path), ])); }); group('verifies analysis settings', () { test('fails analysis_options.yaml', () async { - await createFakePlugin('foo', withExtraFiles: >[ + createFakePlugin('foo', withExtraFiles: >[ ['analysis_options.yaml'] ]); @@ -64,7 +66,7 @@ void main() { }); test('fails .analysis_options', () async { - await createFakePlugin('foo', withExtraFiles: >[ + createFakePlugin('foo', withExtraFiles: >[ ['.analysis_options'] ]); @@ -74,7 +76,7 @@ void main() { test('takes an allow list', () async { final Directory pluginDir = - await createFakePlugin('foo', withExtraFiles: >[ + createFakePlugin('foo', withExtraFiles: >[ ['analysis_options.yaml'] ]); @@ -86,17 +88,20 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('pub', ['global', 'activate', 'tuneup'], + ProcessCall('pub', const ['global', 'activate', 'tuneup'], mockPackagesDir.path), - ProcessCall('flutter', ['packages', 'get'], pluginDir.path), - ProcessCall('pub', ['global', 'run', 'tuneup', 'check'], + ProcessCall( + 'flutter', const ['packages', 'get'], pluginDir.path), + ProcessCall( + 'pub', + const ['global', 'run', 'tuneup', 'check'], pluginDir.path), ])); }); // See: https://github.com/flutter/flutter/issues/78994 test('takes an empty allow list', () async { - await createFakePlugin('foo', withExtraFiles: >[ + createFakePlugin('foo', withExtraFiles: >[ ['analysis_options.yaml'] ]); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index f9ee6dcf25d..40da27d4492 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -13,10 +13,10 @@ import 'util.dart'; void main() { group('test build_example_command', () { - CommandRunner runner; + CommandRunner runner; RecordingProcessRunner processRunner; final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; setUp(() { initializeFakePackages(); @@ -25,7 +25,7 @@ void main() { mockPackagesDir, mockFileSystem, processRunner: processRunner); - runner = CommandRunner( + runner = CommandRunner( 'build_examples_command', 'Test for build_example_command'); runner.addCommand(command); cleanupPackages(); @@ -102,7 +102,7 @@ void main() { orderedEquals([ ProcessCall( flutterCommand, - [ + const [ 'build', 'ios', '--no-codesign', @@ -179,7 +179,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, ['build', 'linux'], + ProcessCall(flutterCommand, const ['build', 'linux'], pluginExampleDirectory.path), ])); cleanupPackages(); @@ -249,7 +249,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, ['build', 'macos'], + ProcessCall(flutterCommand, const ['build', 'macos'], pluginExampleDirectory.path), ])); cleanupPackages(); @@ -318,7 +318,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, ['build', 'web'], + ProcessCall(flutterCommand, const ['build', 'web'], pluginExampleDirectory.path), ])); cleanupPackages(); @@ -390,7 +390,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, ['build', 'windows'], + ProcessCall(flutterCommand, const ['build', 'windows'], pluginExampleDirectory.path), ])); cleanupPackages(); @@ -466,7 +466,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, ['build', 'apk'], + ProcessCall(flutterCommand, const ['build', 'apk'], pluginExampleDirectory.path), ])); cleanupPackages(); @@ -498,7 +498,7 @@ void main() { orderedEquals([ ProcessCall( flutterCommand, - ['build', 'apk', '--enable-experiment=exp1'], + const ['build', 'apk', '--enable-experiment=exp1'], pluginExampleDirectory.path), ])); cleanupPackages(); @@ -528,7 +528,7 @@ void main() { orderedEquals([ ProcessCall( flutterCommand, - [ + const [ 'build', 'ios', '--no-codesign', diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index 6f51ade64e8..57bc1e20f91 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -15,7 +15,7 @@ import 'util.dart'; void main() { RecordingProcessRunner processRunner; - CommandRunner runner; + CommandRunner runner; List plugins; List> gitDirCommands; String gitDiffResponse; @@ -25,16 +25,17 @@ void main() { gitDiffResponse = ''; final MockGitDir gitDir = MockGitDir(); when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0]); + gitDirCommands.add(invocation.positionalArguments[0] as List); final MockProcessResult mockProcessResult = MockProcessResult(); if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + when(mockProcessResult.stdout as String) + .thenReturn(gitDiffResponse); } return Future.value(mockProcessResult); }); initializeFakePackages(); processRunner = RecordingProcessRunner(); - plugins = []; + plugins = []; final SamplePluginCommand samplePluginCommand = SamplePluginCommand( plugins, mockPackagesDir, @@ -43,7 +44,7 @@ void main() { gitDir: gitDir, ); runner = - CommandRunner('common_command', 'Test for common functionality'); + CommandRunner('common_command', 'Test for common functionality'); runner.addCommand(samplePluginCommand); }); @@ -108,7 +109,7 @@ void main() { test('all plugins should be tested if there are no plugin related changes.', () async { - gitDiffResponse = ".cirrus"; + gitDiffResponse = '.cirrus'; final Directory plugin1 = createFakePlugin('plugin1'); final Directory plugin2 = createFakePlugin('plugin2'); await runner.run( @@ -118,7 +119,7 @@ void main() { }); test('Only changed plugin should be tested.', () async { - gitDiffResponse = "packages/plugin1/plugin1.dart"; + gitDiffResponse = 'packages/plugin1/plugin1.dart'; final Directory plugin1 = createFakePlugin('plugin1'); createFakePlugin('plugin2'); await runner.run( @@ -226,12 +227,14 @@ packages/plugin3/plugin3.dart gitDiffResponse = ''; gitDir = MockGitDir(); when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0]); + gitDirCommands.add(invocation.positionalArguments[0] as List); final MockProcessResult mockProcessResult = MockProcessResult(); if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + when(mockProcessResult.stdout as String) + .thenReturn(gitDiffResponse); } else if (invocation.positionalArguments[0][0] == 'merge-base') { - when(mockProcessResult.stdout).thenReturn(mergeBaseResponse); + when(mockProcessResult.stdout as String) + .thenReturn(mergeBaseResponse); } return Future.value(mockProcessResult); }); @@ -245,7 +248,7 @@ packages/plugin3/plugin3.dart test('No git diff should result no files changed', () async { final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - List changedFiles = await finder.getChangedFiles(); + final List changedFiles = await finder.getChangedFiles(); expect(changedFiles, isEmpty); }); @@ -256,7 +259,7 @@ file1/file1.cc file2/file2.cc '''; final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - List changedFiles = await finder.getChangedFiles(); + final List changedFiles = await finder.getChangedFiles(); expect( changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); @@ -268,7 +271,7 @@ file1/pubspec.yaml file2/file2.cc '''; final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - List changedFiles = await finder.getChangedPubSpecs(); + final List changedFiles = await finder.getChangedPubSpecs(); expect(changedFiles, equals(['file1/pubspec.yaml'])); }); @@ -281,26 +284,27 @@ file2/file2.cc '''; final GitVersionFinder finder = GitVersionFinder(gitDir, null); await finder.getChangedFiles(); - verify(gitDir - .runCommand(['diff', '--name-only', mergeBaseResponse, 'HEAD'])); + verify(gitDir.runCommand( + ['diff', '--name-only', mergeBaseResponse, 'HEAD'])); }); test('use correct base sha if specified', () async { - final String customBaseSha = 'aklsjdcaskf12312'; + 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'])); + verify(gitDir + .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); }); }); } class SamplePluginCommand extends PluginCommand { SamplePluginCommand( - this.plugins_, + this._plugins, Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), @@ -308,7 +312,7 @@ class SamplePluginCommand extends PluginCommand { }) : super(packagesDir, fileSystem, processRunner: processRunner, gitDir: gitDir); - List plugins_; + final List _plugins; @override final String name = 'sample'; @@ -317,9 +321,9 @@ class SamplePluginCommand extends PluginCommand { final String description = 'sample command'; @override - Future run() async { - await for (Directory package in getPlugins()) { - this.plugins_.add(package.path); + Future run() async { + await for (final Directory package in getPlugins()) { + _plugins.add(package.path); } } } diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 58f24c9a3de..fedc0468463 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -12,7 +12,7 @@ import 'util.dart'; void main() { group('$CreateAllPluginsAppCommand', () { - CommandRunner runner; + CommandRunner runner; FileSystem fileSystem; Directory testRoot; Directory packagesDir; @@ -22,7 +22,7 @@ void main() { // Since the core of this command is a call to 'flutter create', the test // has to use the real filesystem. Put everything possible in a unique // temporary to minimize affect on the host system. - fileSystem = LocalFileSystem(); + fileSystem = const LocalFileSystem(); testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); @@ -32,7 +32,7 @@ void main() { pluginsRoot: testRoot, ); appDir = command.appDirectory; - runner = CommandRunner( + runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPluginsAppCommand'); runner.addCommand(command); }); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 58b8fb0d087..c5960b2c342 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -14,10 +14,10 @@ import 'util.dart'; void main() { group('test drive_example_command', () { - CommandRunner runner; + CommandRunner runner; RecordingProcessRunner processRunner; final String flutterCommand = - LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; setUp(() { initializeFakePackages(); processRunner = RecordingProcessRunner(); @@ -25,7 +25,7 @@ void main() { mockPackagesDir, mockFileSystem, processRunner: processRunner); - runner = CommandRunner( + runner = CommandRunner( 'drive_examples_command', 'Test for drive_example_command'); runner.addCommand(command); }); @@ -60,8 +60,8 @@ void main() { ]), ); - String deviceTestPath = p.join('test', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + final String deviceTestPath = p.join('test', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); print(processRunner.recordedCalls); expect( processRunner.recordedCalls, @@ -105,8 +105,8 @@ void main() { ]), ); - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); print(processRunner.recordedCalls); expect( processRunner.recordedCalls, @@ -173,7 +173,8 @@ void main() { ]), ); - String driverTestPath = p.join('test_driver', 'integration_test.dart'); + final String driverTestPath = + p.join('test_driver', 'integration_test.dart'); print(processRunner.recordedCalls); expect( processRunner.recordedCalls, @@ -259,8 +260,8 @@ void main() { ]), ); - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); print(processRunner.recordedCalls); expect( processRunner.recordedCalls, @@ -336,8 +337,8 @@ void main() { ]), ); - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); print(processRunner.recordedCalls); expect( processRunner.recordedCalls, @@ -415,8 +416,8 @@ void main() { ]), ); - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); print(processRunner.recordedCalls); expect( processRunner.recordedCalls, @@ -496,8 +497,8 @@ void main() { ]), ); - String deviceTestPath = p.join('test_driver', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); print(processRunner.recordedCalls); expect( processRunner.recordedCalls, @@ -567,8 +568,8 @@ void main() { '--enable-experiment=exp1', ]); - String deviceTestPath = p.join('test', 'plugin.dart'); - String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + final String deviceTestPath = p.join('test', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); print(processRunner.recordedCalls); expect( processRunner.recordedCalls, diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index f1141ae19d8..d6068691d54 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -15,7 +15,7 @@ import 'util.dart'; void main() { group('$FirebaseTestLabCommand', () { final List printedMessages = []; - CommandRunner runner; + CommandRunner runner; RecordingProcessRunner processRunner; setUp(() { @@ -26,7 +26,7 @@ void main() { processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString())); - runner = CommandRunner( + runner = CommandRunner( 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); runner.addCommand(command); }); @@ -60,7 +60,7 @@ void main() { expect( printedMessages, contains( - "\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.")); + '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.')); }); test('runs e2e tests', () async { @@ -86,7 +86,7 @@ void main() { ], ]); - final List output = await runCapturingPrint(runner, [ + await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', 'model=flame,version=29', diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index b036d7ec0c6..ba016915223 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.dart @@ -12,7 +12,7 @@ import 'util.dart'; void main() { group('$JavaTestCommand', () { - CommandRunner runner; + CommandRunner runner; final RecordingProcessRunner processRunner = RecordingProcessRunner(); setUp(() { @@ -22,7 +22,7 @@ void main() { processRunner: processRunner); runner = - CommandRunner('java_test_test', 'Test for $JavaTestCommand'); + CommandRunner('java_test_test', 'Test for $JavaTestCommand'); runner.addCommand(command); }); @@ -50,7 +50,7 @@ void main() { orderedEquals([ ProcessCall( p.join(plugin.path, 'example/android/gradlew'), - ['testDebugUnitTest', '--info'], + const ['testDebugUnitTest', '--info'], p.join(plugin.path, 'example/android'), ), ]), @@ -76,7 +76,7 @@ void main() { orderedEquals([ ProcessCall( p.join(plugin.path, 'example/android/gradlew'), - ['testDebugUnitTest', '--info'], + const ['testDebugUnitTest', '--info'], p.join(plugin.path, 'example/android'), ), ]), diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index f8646da5d83..94606f54b76 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -11,7 +11,7 @@ import 'package:test/test.dart'; void main() { group('$LicenseCheckCommand', () { - CommandRunner runner; + CommandRunner runner; FileSystem fileSystem; List printedMessages; Directory root; @@ -29,7 +29,7 @@ void main() { print: (Object message) => printedMessages.add(message.toString()), ); runner = - CommandRunner('license_test', 'Test for $LicenseCheckCommand'); + CommandRunner('license_test', 'Test for $LicenseCheckCommand'); runner.addCommand(command); }); @@ -51,15 +51,15 @@ void main() { 'found in the LICENSE file.', ], }) { - List lines = ['$prefix$comment$copyright']; - for (String line in license) { + final List lines = ['$prefix$comment$copyright']; + for (final String line in license) { lines.add('$comment$line'); } file.writeAsStringSync(lines.join('\n') + suffix + '\n'); } test('looks at only expected extensions', () async { - Map extensions = { + final Map extensions = { 'c': true, 'cc': true, 'cpp': true, @@ -98,7 +98,7 @@ void main() { }); test('ignore list overrides extension matches', () async { - List ignoredFiles = [ + final List ignoredFiles = [ // Ignored base names. 'flutter_export_environment.sh', 'GeneratedPluginRegistrant.java', @@ -124,11 +124,11 @@ void main() { }); test('passes if all checked files have license blocks', () async { - File checked = root.childFile('checked.cc'); + final File checked = root.childFile('checked.cc'); checked.createSync(); _writeLicense(checked); - File not_checked = root.childFile('not_checked.md'); - not_checked.createSync(); + final File notChecked = root.childFile('not_checked.md'); + notChecked.createSync(); await runner.run(['license-check']); @@ -138,15 +138,15 @@ void main() { }); test('handles the comment styles for all supported languages', () async { - File file_a = root.childFile('file_a.cc'); - file_a.createSync(); - _writeLicense(file_a, comment: '// '); - File file_b = root.childFile('file_b.sh'); - file_b.createSync(); - _writeLicense(file_b, comment: '# '); - File file_c = root.childFile('file_c.html'); - file_c.createSync(); - _writeLicense(file_c, comment: '', prefix: ''); + final File fileA = root.childFile('file_a.cc'); + fileA.createSync(); + _writeLicense(fileA, comment: '// '); + final File fileB = root.childFile('file_b.sh'); + fileB.createSync(); + _writeLicense(fileB, comment: '# '); + final File fileC = root.childFile('file_c.html'); + fileC.createSync(); + _writeLicense(fileC, comment: '', prefix: ''); await runner.run(['license-check']); @@ -158,12 +158,12 @@ void main() { }); test('fails if any checked files are missing license blocks', () async { - File good_a = root.childFile('good.cc'); - good_a.createSync(); - _writeLicense(good_a); - File good_b = root.childFile('good.h'); - good_b.createSync(); - _writeLicense(good_b); + final File goodA = root.childFile('good.cc'); + goodA.createSync(); + _writeLicense(goodA); + final File goodB = root.childFile('good.h'); + goodB.createSync(); + _writeLicense(goodB); root.childFile('bad.cc').createSync(); root.childFile('bad.h').createSync(); @@ -183,10 +183,10 @@ void main() { }); test('fails if any checked files are missing just the copyright', () async { - File good = root.childFile('good.cc'); + final File good = root.childFile('good.cc'); good.createSync(); _writeLicense(good); - File bad = root.childFile('bad.cc'); + final File bad = root.childFile('bad.cc'); bad.createSync(); _writeLicense(bad, copyright: ''); @@ -205,10 +205,10 @@ void main() { }); test('fails if any checked files are missing just the license', () async { - File good = root.childFile('good.cc'); + final File good = root.childFile('good.cc'); good.createSync(); _writeLicense(good); - File bad = root.childFile('bad.cc'); + final File bad = root.childFile('bad.cc'); bad.createSync(); _writeLicense(bad, license: []); @@ -228,7 +228,7 @@ void main() { test('fails if any third-party code is not in a third_party directory', () async { - File thirdPartyFile = root.childFile('third_party.cc'); + final File thirdPartyFile = root.childFile('third_party.cc'); thirdPartyFile.createSync(); _writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); @@ -247,7 +247,7 @@ void main() { }); test('succeeds for third-party code in a third_party directory', () async { - File thirdPartyFile = root + final File thirdPartyFile = root .childDirectory('a_plugin') .childDirectory('lib') .childDirectory('src') @@ -270,10 +270,10 @@ void main() { }); test('fails for licenses that the tool does not expect', () async { - File good = root.childFile('good.cc'); + final File good = root.childFile('good.cc'); good.createSync(); _writeLicense(good); - File bad = root.childDirectory('third_party').childFile('bad.cc'); + final File bad = root.childDirectory('third_party').childFile('bad.cc'); bad.createSync(recursive: true); _writeLicense(bad, license: [ 'This program is free software: you can redistribute it and/or modify', @@ -296,26 +296,28 @@ void main() { test('Apache is not recognized for new authors without validation changes', () async { - File good = root.childFile('good.cc'); + final File good = root.childFile('good.cc'); good.createSync(); _writeLicense(good); - File bad = root.childDirectory('third_party').childFile('bad.cc'); + final File bad = root.childDirectory('third_party').childFile('bad.cc'); bad.createSync(recursive: true); _writeLicense( bad, copyright: 'Copyright 2017 Some New Authors.', - license: [ - 'Licensed under the Apache License, Version 2.0 (the "License");', - 'you may not use this file except in compliance with the License.' - ], + license: [ + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the License.' + ], ); await expectLater(() => runner.run(['license-check']), throwsA(const TypeMatcher())); // Failure should give information about the problematic files. - expect(printedMessages, - contains('No recognized license was found for the following third-party files:')); + expect( + printedMessages, + contains( + 'No recognized license was found for the following third-party files:')); expect(printedMessages, contains(' third_party/bad.cc')); // Failure shouldn't print the success message. expect(printedMessages, @@ -324,7 +326,7 @@ void main() { test('passes if all first-party LICENSE files are correctly formatted', () async { - File license = root.childFile('LICENSE'); + final File license = root.childFile('LICENSE'); license.createSync(); license.writeAsStringSync(_correctLicenseFileText); @@ -337,7 +339,7 @@ void main() { test('fails if any first-party LICENSE files are incorrectly formatted', () async { - File license = root.childFile('LICENSE'); + final File license = root.childFile('LICENSE'); license.createSync(); license.writeAsStringSync(_incorrectLicenseFileText); @@ -349,7 +351,8 @@ void main() { }); test('ignores third-party LICENSE format', () async { - File license = root.childDirectory('third_party').childFile('LICENSE'); + final File license = + root.childDirectory('third_party').childFile('LICENSE'); license.createSync(recursive: true); license.writeAsStringSync(_incorrectLicenseFileText); @@ -362,8 +365,8 @@ void main() { }); } -const String _correctLicenseFileText = - '''Copyright 2013 The Flutter Authors. All rights reserved. +const String _correctLicenseFileText = ''' +Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -392,8 +395,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // A common incorrect version created by copying text intended for a code file, // with comment markers. -const String _incorrectLicenseFileText = - '''// Copyright 2013 The Flutter Authors. All rights reserved. +const String _incorrectLicenseFileText = ''' +// Copyright 2013 The Flutter Authors. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 5475641cba9..a1fa1f7c774 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.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:io'; - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; @@ -17,7 +15,7 @@ import 'util.dart'; void main() { group('$LintPodspecsCommand', () { - CommandRunner runner; + CommandRunner runner; MockPlatform mockPlatform; final RecordingProcessRunner processRunner = RecordingProcessRunner(); List printedMessages; @@ -37,7 +35,7 @@ void main() { ); runner = - CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); + CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); runner.addCommand(command); final MockProcess mockLintProcess = MockProcess(); mockLintProcess.exitCodeCompleter.complete(0); @@ -64,7 +62,7 @@ void main() { }); test('runs pod lib lint on a podspec', () async { - Directory plugin1Dir = + final Directory plugin1Dir = createFakePlugin('plugin1', withExtraFiles: >[ ['ios', 'plugin1.podspec'], ['bogus.dart'], // Ignore non-podspecs. @@ -78,7 +76,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall('which', const ['pod'], mockPackagesDir.path), ProcessCall( 'pod', [ @@ -103,8 +101,7 @@ void main() { ]), ); - expect( - printedMessages, contains('Linting plugin1.podspec')); + expect(printedMessages, contains('Linting plugin1.podspec')); expect(printedMessages, contains('Foo')); expect(printedMessages, contains('Bar')); }); @@ -123,13 +120,13 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall('which', const ['pod'], mockPackagesDir.path), ]), ); }); test('allow warnings for podspecs with known warnings', () async { - Directory plugin1Dir = + final Directory plugin1Dir = createFakePlugin('plugin1', withExtraFiles: >[ ['plugin1.podspec'], ]); @@ -139,7 +136,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('which', ['pod'], mockPackagesDir.path), + ProcessCall('which', const ['pod'], mockPackagesDir.path), ProcessCall( 'pod', [ @@ -166,8 +163,7 @@ void main() { ]), ); - expect( - printedMessages, contains('Linting plugin1.podspec')); + expect(printedMessages, contains('Linting plugin1.podspec')); }); }); } diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index 19e9df0dd38..e7fcfe7a7c4 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -11,13 +11,13 @@ import 'util.dart'; void main() { group('$ListCommand', () { - CommandRunner runner; + CommandRunner runner; setUp(() { initializeFakePackages(); final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); - runner = CommandRunner('list_test', 'Test for $ListCommand'); + runner = CommandRunner('list_test', 'Test for $ListCommand'); runner.addCommand(command); }); diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 2ef9d72e363..ad1f357fb47 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -33,5 +33,5 @@ class MockIOSink extends Mock implements IOSink { List lines = []; @override - void writeln([Object obj = ""]) => lines.add(obj); + void writeln([Object obj = '']) => lines.add(obj.toString()); } diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index dbe6e2cfe54..568d35eeff2 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -17,7 +17,7 @@ import 'util.dart'; void main() { group('$PublishCheckProcessRunner tests', () { PublishCheckProcessRunner processRunner; - CommandRunner runner; + CommandRunner runner; setUp(() { initializeFakePackages(); @@ -26,7 +26,7 @@ void main() { mockPackagesDir, mockFileSystem, processRunner: processRunner); - runner = CommandRunner( + runner = CommandRunner( 'publish_check_command', 'Test for publish-check command.', ); @@ -38,8 +38,8 @@ void main() { }); test('publish check all packages', () async { - final Directory plugin1Dir = await createFakePlugin('a'); - final Directory plugin2Dir = await createFakePlugin('b'); + final Directory plugin1Dir = createFakePlugin('a'); + final Directory plugin2Dir = createFakePlugin('b'); processRunner.processesToReturn.add( MockProcess()..exitCodeCompleter.complete(0), @@ -52,15 +52,19 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', - ['pub', 'publish', '--', '--dry-run'], plugin1Dir.path), - ProcessCall('flutter', - ['pub', 'publish', '--', '--dry-run'], plugin2Dir.path), + ProcessCall( + 'flutter', + const ['pub', 'publish', '--', '--dry-run'], + plugin1Dir.path), + ProcessCall( + 'flutter', + const ['pub', 'publish', '--', '--dry-run'], + plugin2Dir.path), ])); }); test('fail on negative test', () async { - await createFakePlugin('a'); + createFakePlugin('a'); final MockProcess process = MockProcess(); process.stdoutController.close(); // ignore: unawaited_futures @@ -76,7 +80,7 @@ void main() { }); test('fail on bad pubspec', () async { - final Directory dir = await createFakePlugin('c'); + final Directory dir = createFakePlugin('c'); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); final MockProcess process = MockProcess(); @@ -87,9 +91,9 @@ void main() { }); test('pass on prerelease', () async { - await createFakePlugin('d'); + createFakePlugin('d'); - final String preReleaseOutput = 'Package has 1 warning.' + const String preReleaseOutput = 'Package has 1 warning.' 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; final MockProcess process = MockProcess(); diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index cfa40b9dc0a..03e7858d3bc 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -27,7 +27,7 @@ void main() { Directory pluginDir; GitDir gitDir; TestProcessRunner processRunner; - CommandRunner commandRunner; + CommandRunner commandRunner; MockStdin mockStdin; setUp(() async { @@ -48,7 +48,7 @@ void main() { await gitDir.runCommand(['commit', '-m', 'Initial commit']); processRunner = TestProcessRunner(); mockStdin = MockStdin(); - commandRunner = CommandRunner('tester', '') + commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand( mockPackagesDir, mockPackagesDir.fileSystem, processRunner: processRunner, @@ -66,13 +66,17 @@ void main() { await expectLater(() => commandRunner.run(['publish-plugin']), throwsA(const TypeMatcher())); expect( - printedMessages.last, contains("Must specify a package to publish.")); + printedMessages.last, contains('Must specify a package to publish.')); }); test('requires an existing flag', () async { await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', 'iamerror', '--no-push-tags']), + () => commandRunner.run([ + 'publish-plugin', + '--package', + 'iamerror', + '--no-push-tags' + ]), throwsA(const TypeMatcher())); expect(printedMessages.last, contains('iamerror does not exist')); @@ -82,8 +86,12 @@ void main() { pluginDir.childFile('tmp').createSync(); await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName, '--no-push-tags']), + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags' + ]), throwsA(const TypeMatcher())); expect( @@ -97,7 +105,7 @@ void main() { () => commandRunner .run(['publish-plugin', '--package', testPluginName]), throwsA(const TypeMatcher())); - expect(processRunner.results.last.stderr, contains("No such remote")); + expect(processRunner.results.last.stderr, contains('No such remote')); }); test("doesn't validate the remote if it's not pushing tags", () async { @@ -202,7 +210,7 @@ void main() { ]), throwsA(const TypeMatcher())); - expect(printedMessages, contains("Publish failed. Exiting.")); + expect(printedMessages, contains('Publish failed. Exiting.')); }); }); @@ -218,7 +226,7 @@ void main() { final String tag = (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) - .stdout; + .stdout as String; expect(tag, isNotEmpty); }); @@ -233,11 +241,11 @@ void main() { ]), throwsA(const TypeMatcher())); - expect(printedMessages, contains("Publish failed. Exiting.")); + expect(printedMessages, contains('Publish failed. Exiting.')); final String tag = (await gitDir.runCommand( ['show-ref', 'fake_package-v0.0.1'], throwOnError: false)) - .stdout; + .stdout as String; expect(tag, isEmpty); }); }); @@ -359,7 +367,7 @@ class MockStdin extends Mock implements io.Stdin { String readLineOutput; @override - Stream transform(StreamTransformer streamTransformer) { + Stream transform(StreamTransformer, S> streamTransformer) { return controller.stream.transform(streamTransformer); } diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 66471263f66..1dd0c158293 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -11,7 +11,7 @@ import 'util.dart'; void main() { group('$TestCommand', () { - CommandRunner runner; + CommandRunner runner; final RecordingProcessRunner processRunner = RecordingProcessRunner(); setUp(() { @@ -19,7 +19,7 @@ void main() { final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, processRunner: processRunner); - runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner = CommandRunner('test_test', 'Test for $TestCommand'); runner.addCommand(command); }); @@ -43,8 +43,10 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', ['test', '--color'], plugin1Dir.path), - ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ProcessCall( + 'flutter', const ['test', '--color'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['test', '--color'], plugin2Dir.path), ]), ); @@ -63,7 +65,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', ['test', '--color'], plugin2Dir.path), + ProcessCall( + 'flutter', const ['test', '--color'], plugin2Dir.path), ]), ); @@ -89,12 +92,12 @@ void main() { orderedEquals([ ProcessCall( 'flutter', - ['test', '--color', '--enable-experiment=exp1'], + const ['test', '--color', '--enable-experiment=exp1'], plugin1Dir.path), - ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall('pub', const ['get'], plugin2Dir.path), ProcessCall( 'pub', - ['run', '--enable-experiment=exp1', 'test'], + const ['run', '--enable-experiment=exp1', 'test'], plugin2Dir.path), ]), ); @@ -117,8 +120,10 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', - ['test', '--color', '--platform=chrome'], pluginDir.path), + ProcessCall( + 'flutter', + const ['test', '--color', '--platform=chrome'], + pluginDir.path), ]), ); }); @@ -142,12 +147,12 @@ void main() { orderedEquals([ ProcessCall( 'flutter', - ['test', '--color', '--enable-experiment=exp1'], + const ['test', '--color', '--enable-experiment=exp1'], plugin1Dir.path), - ProcessCall('pub', ['get'], plugin2Dir.path), + ProcessCall('pub', const ['get'], plugin2Dir.path), ProcessCall( 'pub', - ['run', '--enable-experiment=exp1', 'test'], + const ['run', '--enable-experiment=exp1', 'test'], plugin2Dir.path), ]), ); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index d02b756ee9c..5a2c42bd319 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -3,11 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:meta/meta.dart'; import 'package:platform/platform.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:quiver/collection.dart'; @@ -16,7 +18,7 @@ import 'package:quiver/collection.dart'; // for each test, to eliminate the chance of files from one test interfering // with another test. FileSystem mockFileSystem = MemoryFileSystem( - style: LocalPlatform().isWindows + style: const LocalPlatform().isWindows ? FileSystemStyle.windows : FileSystemStyle.posix); Directory mockPackagesDir; @@ -83,20 +85,19 @@ Directory createFakePlugin( final Directory exampleDir = pluginDirectory.childDirectory('example') ..createSync(); createFakePubspec(exampleDir, - name: "${name}_example", isFlutter: isFlutter); + name: '${name}_example', isFlutter: isFlutter); } else if (withExamples.isNotEmpty) { final Directory exampleDir = pluginDirectory.childDirectory('example') ..createSync(); - for (String example in withExamples) { + for (final String example in withExamples) { final Directory currentExample = exampleDir.childDirectory(example) ..createSync(); createFakePubspec(currentExample, name: example, isFlutter: isFlutter); } } - for (List file in withExtraFiles) { - final List newFilePath = [pluginDirectory.path] - ..addAll(file); + for (final List file in withExtraFiles) { + final List newFilePath = [pluginDirectory.path, ...file]; final File newFile = mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); newFile.createSync(recursive: true); @@ -195,7 +196,7 @@ void cleanupPackages() { /// Run the command [runner] with the given [args] and return /// what was printed. Future> runCapturingPrint( - CommandRunner runner, List args) async { + CommandRunner runner, List args) async { final List prints = []; final ZoneSpecification spec = ZoneSpecification( print: (_, __, ___, String message) { @@ -237,8 +238,8 @@ class RecordingProcessRunner extends ProcessRunner { Future run(String executable, List args, {Directory workingDir, bool exitOnError = false, - stdoutEncoding = io.systemEncoding, - stderrEncoding = io.systemEncoding}) async { + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding}) async { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); io.ProcessResult result; @@ -279,6 +280,7 @@ class RecordingProcessRunner extends ProcessRunner { } /// A recorded process call. +@immutable class ProcessCall { const ProcessCall(this.executable, this.args, this.workingDir); @@ -293,13 +295,10 @@ class ProcessCall { @override bool operator ==(dynamic other) { - if (other is! ProcessCall) { - return false; - } - final ProcessCall otherCall = other; - return executable == otherCall.executable && - listsEqual(args, otherCall.args) && - workingDir == otherCall.workingDir; + return other is ProcessCall && + executable == other.executable && + listsEqual(args, other.args) && + workingDir == other.workingDir; } @override @@ -311,7 +310,7 @@ class ProcessCall { @override String toString() { - final List command = [executable]..addAll(args); + final List command = [executable, ...args]; return '"${command.join(' ')}" in $workingDir'; } } diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index dc36c6c3228..96a460d7e72 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -3,14 +3,15 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; +import 'dart:io' as io; import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; -import "package:test/test.dart"; -import "package:flutter_plugin_tools/src/version_check_command.dart"; +import 'package:test/test.dart'; +import 'package:flutter_plugin_tools/src/version_check_command.dart'; import 'package:pub_semver/pub_semver.dart'; import 'util.dart'; @@ -36,11 +37,11 @@ void testAllowedVersion( class MockGitDir extends Mock implements GitDir {} -class MockProcessResult extends Mock implements ProcessResult {} +class MockProcessResult extends Mock implements io.ProcessResult {} void main() { group('$VersionCheckCommand', () { - CommandRunner runner; + CommandRunner runner; RecordingProcessRunner processRunner; List> gitDirCommands; String gitDiffResponse; @@ -52,16 +53,17 @@ void main() { gitShowResponses = {}; final MockGitDir gitDir = MockGitDir(); when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0]); + gitDirCommands.add(invocation.positionalArguments[0] as List); final MockProcessResult mockProcessResult = MockProcessResult(); if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout).thenReturn(gitDiffResponse); + when(mockProcessResult.stdout as String) + .thenReturn(gitDiffResponse); } else if (invocation.positionalArguments[0][0] == 'show') { final String response = gitShowResponses[invocation.positionalArguments[0][1]]; - when(mockProcessResult.stdout).thenReturn(response); + when(mockProcessResult.stdout as String).thenReturn(response); } - return Future.value(mockProcessResult); + return Future.value(mockProcessResult); }); initializeFakePackages(); processRunner = RecordingProcessRunner(); @@ -69,7 +71,7 @@ void main() { mockPackagesDir, mockFileSystem, processRunner: processRunner, gitDir: gitDir); - runner = CommandRunner( + runner = CommandRunner( 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); }); @@ -80,7 +82,7 @@ void main() { test('allows valid version', () async { createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', @@ -105,7 +107,7 @@ void main() { test('denies invalid version', () async { createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', @@ -128,7 +130,7 @@ void main() { test('gracefully handles missing pubspec.yaml', () async { createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - gitDiffResponse = "packages/plugin/pubspec.yaml"; + gitDiffResponse = 'packages/plugin/pubspec.yaml'; mockFileSystem.currentDirectory .childDirectory('packages') .childDirectory('plugin') @@ -152,7 +154,7 @@ void main() { test('allows minor changes to platform interfaces', () async { createFakePlugin('plugin_platform_interface', includeChangeLog: true, includeVersion: true); - gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', @@ -181,7 +183,7 @@ void main() { test('disallows breaking changes to platform interfaces', () async { createFakePlugin('plugin_platform_interface', includeChangeLog: true, includeVersion: true); - gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml"; + gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', @@ -214,7 +216,7 @@ void main() { createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.1'); - String changelog = ''' + const String changelog = ''' @@ -225,9 +227,9 @@ void main() { createFakeCHANGELOG(pluginDirectory, changelog); final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); - await expect( + expect( output, - containsAllInOrder([ + containsAllInOrder([ 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.', 'plugin passed version check', 'No version check errors found!' @@ -243,7 +245,7 @@ void main() { createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.1'); - String changelog = ''' + const String changelog = ''' ## 1.0.2 * Some changes. @@ -256,10 +258,10 @@ void main() { throwsA(const TypeMatcher()), ); try { - List outputValue = await output; + final List outputValue = await output; await expectLater( outputValue, - containsAllInOrder([ + containsAllInOrder([ ''' versions for plugin in CHANGELOG.md and pubspec.yaml do not match. The version in pubspec.yaml is 1.0.1. @@ -278,7 +280,7 @@ void main() { createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.1'); - String changelog = ''' + const String changelog = ''' ## 1.0.1 * Some changes. @@ -286,9 +288,9 @@ void main() { createFakeCHANGELOG(pluginDirectory, changelog); final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); - await expect( + expect( output, - containsAllInOrder([ + containsAllInOrder([ 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.', 'plugin passed version check', 'No version check errors found!' @@ -306,7 +308,7 @@ void main() { createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.0'); - String changelog = ''' + const String changelog = ''' ## 1.0.1 * Some changes. @@ -316,17 +318,17 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - Future> output = runCapturingPrint( + final Future> output = runCapturingPrint( runner, ['version-check', '--base-sha=master']); await expectLater( output, throwsA(const TypeMatcher()), ); try { - List outputValue = await output; + final List outputValue = await output; await expectLater( outputValue, - containsAllInOrder([ + containsAllInOrder([ ''' versions for plugin in CHANGELOG.md and pubspec.yaml do not match. The version in pubspec.yaml is 1.0.0. @@ -338,122 +340,97 @@ void main() { }); }); - group("Pre 1.0", () { - test("nextVersion allows patch version", () { - testAllowedVersion("0.12.0", "0.12.0+1", + group('Pre 1.0', () { + test('nextVersion allows patch version', () { + testAllowedVersion('0.12.0', '0.12.0+1', nextVersionType: NextVersionType.PATCH); - testAllowedVersion("0.12.0+4", "0.12.0+5", + testAllowedVersion('0.12.0+4', '0.12.0+5', nextVersionType: NextVersionType.PATCH); }); - test("nextVersion does not allow jumping patch", () { - testAllowedVersion("0.12.0", "0.12.0+2", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.0+4", allowed: false); + test('nextVersion does not allow jumping patch', () { + testAllowedVersion('0.12.0', '0.12.0+2', allowed: false); + testAllowedVersion('0.12.0+2', '0.12.0+4', allowed: false); }); - test("nextVersion does not allow going back", () { - testAllowedVersion("0.12.0", "0.11.0", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.0+1", allowed: false); - testAllowedVersion("0.12.0+1", "0.12.0", allowed: false); + test('nextVersion does not allow going back', () { + testAllowedVersion('0.12.0', '0.11.0', allowed: false); + testAllowedVersion('0.12.0+2', '0.12.0+1', allowed: false); + testAllowedVersion('0.12.0+1', '0.12.0', allowed: false); }); - test("nextVersion allows minor version", () { - testAllowedVersion("0.12.0", "0.12.1", + test('nextVersion allows minor version', () { + testAllowedVersion('0.12.0', '0.12.1', nextVersionType: NextVersionType.MINOR); - testAllowedVersion("0.12.0+4", "0.12.1", + testAllowedVersion('0.12.0+4', '0.12.1', nextVersionType: NextVersionType.MINOR); }); - test("nextVersion does not allow jumping minor", () { - testAllowedVersion("0.12.0", "0.12.2", allowed: false); - testAllowedVersion("0.12.0+2", "0.12.3", allowed: false); + test('nextVersion does not allow jumping minor', () { + testAllowedVersion('0.12.0', '0.12.2', allowed: false); + testAllowedVersion('0.12.0+2', '0.12.3', allowed: false); }); }); - group("Releasing 1.0", () { - test("nextVersion allows releasing 1.0", () { - testAllowedVersion("0.12.0", "1.0.0", + group('Releasing 1.0', () { + test('nextVersion allows releasing 1.0', () { + testAllowedVersion('0.12.0', '1.0.0', nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion("0.12.0+4", "1.0.0", + testAllowedVersion('0.12.0+4', '1.0.0', nextVersionType: NextVersionType.BREAKING_MAJOR); }); - test("nextVersion does not allow jumping major", () { - testAllowedVersion("0.12.0", "2.0.0", allowed: false); - testAllowedVersion("0.12.0+4", "2.0.0", allowed: false); + test('nextVersion does not allow jumping major', () { + testAllowedVersion('0.12.0', '2.0.0', allowed: false); + testAllowedVersion('0.12.0+4', '2.0.0', allowed: false); }); - test("nextVersion does not allow un-releasing", () { - testAllowedVersion("1.0.0", "0.12.0+4", allowed: false); - testAllowedVersion("1.0.0", "0.12.0", allowed: false); + test('nextVersion does not allow un-releasing', () { + testAllowedVersion('1.0.0', '0.12.0+4', allowed: false); + testAllowedVersion('1.0.0', '0.12.0', allowed: false); }); }); - group("Post 1.0", () { - test("nextVersion allows patch jumps", () { - testAllowedVersion("1.0.1", "1.0.2", + group('Post 1.0', () { + test('nextVersion allows patch jumps', () { + testAllowedVersion('1.0.1', '1.0.2', nextVersionType: NextVersionType.PATCH); - testAllowedVersion("1.0.0", "1.0.1", + testAllowedVersion('1.0.0', '1.0.1', nextVersionType: NextVersionType.PATCH); }); - test("nextVersion does not allow build jumps", () { - testAllowedVersion("1.0.1", "1.0.1+1", allowed: false); - testAllowedVersion("1.0.0+5", "1.0.0+6", allowed: false); + test('nextVersion does not allow build jumps', () { + testAllowedVersion('1.0.1', '1.0.1+1', allowed: false); + testAllowedVersion('1.0.0+5', '1.0.0+6', allowed: false); }); - test("nextVersion does not allow skipping patches", () { - testAllowedVersion("1.0.1", "1.0.3", allowed: false); - testAllowedVersion("1.0.0", "1.0.6", allowed: false); + test('nextVersion does not allow skipping patches', () { + testAllowedVersion('1.0.1', '1.0.3', allowed: false); + testAllowedVersion('1.0.0', '1.0.6', allowed: false); }); - test("nextVersion allows minor version jumps", () { - testAllowedVersion("1.0.1", "1.1.0", + test('nextVersion allows minor version jumps', () { + testAllowedVersion('1.0.1', '1.1.0', nextVersionType: NextVersionType.MINOR); - testAllowedVersion("1.0.0", "1.1.0", + testAllowedVersion('1.0.0', '1.1.0', nextVersionType: NextVersionType.MINOR); }); - test("nextVersion does not allow skipping minor versions", () { - testAllowedVersion("1.0.1", "1.2.0", allowed: false); - testAllowedVersion("1.1.0", "1.3.0", allowed: false); + test('nextVersion does not allow skipping minor versions', () { + testAllowedVersion('1.0.1', '1.2.0', allowed: false); + testAllowedVersion('1.1.0', '1.3.0', allowed: false); }); - test("nextVersion allows breaking changes", () { - testAllowedVersion("1.0.1", "2.0.0", + test('nextVersion allows breaking changes', () { + testAllowedVersion('1.0.1', '2.0.0', nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion("1.0.0", "2.0.0", + testAllowedVersion('1.0.0', '2.0.0', nextVersionType: NextVersionType.BREAKING_MAJOR); }); - test("nextVersion allows null safety pre prelease", () { - testAllowedVersion("1.0.1", "2.0.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0", "2.0.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0-nullsafety", "1.0.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0-nullsafety.1", "1.0.0-nullsafety.2", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0", "0.2.0-nullsafety", - nextVersionType: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0-nullsafety", "0.1.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0-nullsafety.1", "0.1.0-nullsafety.2", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.0.0", "1.1.0-nullsafety", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("1.1.0-nullsafety", "1.1.0-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.0", "0.1.1-nullsafety", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - testAllowedVersion("0.1.1-nullsafety", "0.1.1-nullsafety.1", - nextVersionType: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE); - }); - - test("nextVersion does not allow skipping major versions", () { - testAllowedVersion("1.0.1", "3.0.0", allowed: false); - testAllowedVersion("1.1.0", "2.3.0", allowed: false); + test('nextVersion does not allow skipping major versions', () { + testAllowedVersion('1.0.1', '3.0.0', allowed: false); + testAllowedVersion('1.1.0', '2.3.0', allowed: false); }); }); } diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index aa71c25835c..1707dc8cfb8 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -12,67 +12,67 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; -final _kDeviceListMap = { - "runtimes": [ - { - "bundlePath": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime", - "buildversion": "17A577", - "runtimeRoot": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-0", - "version": "13.0", - "isAvailable": true, - "name": "iOS 13.0" +final Map _kDeviceListMap = { + 'runtimes': >[ + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime', + 'buildversion': '17A577', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', + 'version': '13.0', + 'isAvailable': true, + 'name': 'iOS 13.0' }, - { - "bundlePath": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime", - "buildversion": "17L255", - "runtimeRoot": - "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-4", - "version": "13.4", - "isAvailable": true, - "name": "iOS 13.4" + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', + 'buildversion': '17L255', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', + 'version': '13.4', + 'isAvailable': true, + 'name': 'iOS 13.4' }, - { - "bundlePath": - "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime", - "buildversion": "17T531", - "runtimeRoot": - "/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot", - "identifier": "com.apple.CoreSimulator.SimRuntime.watchOS-6-2", - "version": "6.2.1", - "isAvailable": true, - "name": "watchOS 6.2" + { + 'bundlePath': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', + 'buildversion': '17T531', + 'runtimeRoot': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', + 'version': '6.2.1', + 'isAvailable': true, + 'name': 'watchOS 6.2' } ], - "devices": { - "com.apple.CoreSimulator.SimRuntime.iOS-13-4": [ - { - "dataPath": - "/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data", - "logPath": - "/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774", - "udid": "2706BBEB-1E01-403E-A8E9-70E8E5A24774", - "isAvailable": true, - "deviceTypeIdentifier": - "com.apple.CoreSimulator.SimDeviceType.iPhone-8", - "state": "Shutdown", - "name": "iPhone 8" + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8', + 'state': 'Shutdown', + 'name': 'iPhone 8' }, - { - "dataPath": - "/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data", - "logPath": - "/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A", - "udid": "1E76A0FD-38AC-4537-A989-EA639D7D012A", - "isAvailable": true, - "deviceTypeIdentifier": - "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", - "state": "Shutdown", - "name": "iPhone 8 Plus" + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', + 'state': 'Shutdown', + 'name': 'iPhone 8 Plus' } ] } @@ -83,7 +83,7 @@ void main() { const String _kSkip = '--skip'; group('test xctest_command', () { - CommandRunner runner; + CommandRunner runner; RecordingProcessRunner processRunner; setUp(() { @@ -93,7 +93,7 @@ void main() { mockPackagesDir, mockFileSystem, processRunner: processRunner); - runner = CommandRunner('xctest_command', 'Test for xctest_command'); + runner = CommandRunner('xctest_command', 'Test for xctest_command'); runner.addCommand(command); cleanupPackages(); }); @@ -113,19 +113,15 @@ void main() { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); processRunner.processToReturn = mockProcess; - final List output = await runCapturingPrint(runner, [ - 'xctest', - _kDestination, - 'foo_destination' - ]); + final List output = await runCapturingPrint( + runner, ['xctest', _kDestination, 'foo_destination']); expect(output, contains('iOS is not supported by this plugin.')); expect(processRunner.recordedCalls, orderedEquals([])); cleanupPackages(); }); - test('running with correct destination, skip 1 plugin', - () async { + test('running with correct destination, skip 1 plugin', () async { createFakePlugin('plugin1', withExtraFiles: >[ ['example', 'test'], @@ -149,7 +145,7 @@ void main() { processRunner.processToReturn = mockProcess; processRunner.resultStdout = '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; - List output = await runCapturingPrint(runner, [ + final List output = await runCapturingPrint(runner, [ 'xctest', _kDestination, 'foo_destination', @@ -165,7 +161,7 @@ void main() { orderedEquals([ ProcessCall( 'xcrun', - [ + const [ 'xcodebuild', 'test', 'analyze', @@ -203,9 +199,9 @@ void main() { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); processRunner.processToReturn = mockProcess; - final Map schemeCommandResult = { - "project": { - "targets": ["bar_scheme", "foo_scheme"] + final Map schemeCommandResult = { + 'project': { + 'targets': ['bar_scheme', 'foo_scheme'] } }; // For simplicity of the test, we combine all the mock results into a single mock result, each internal command @@ -219,10 +215,11 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('xcrun', ['simctl', 'list', '--json'], null), + const ProcessCall( + 'xcrun', ['simctl', 'list', '--json'], null), ProcessCall( 'xcrun', - [ + const [ 'xcodebuild', 'test', 'analyze', From b2eefc9158f0b0a88d2f7dc38adaf80d8d0ccc1b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 8 Apr 2021 13:22:19 -0700 Subject: [PATCH 026/249] Fix and update version checks (#3792) Currently our version update checks aren't actually working; the script doesn't work correctly if no explicit --base-sha is passed, but that's always how CI is calling it. Fixes https://github.com/flutter/flutter/issues/79823 (and version checks in general) This makes a number of changes: - Fixes it to work without --base-sha - Adds tests that it works in that mode - And tightens existing tests to require ToolExit, not just any error, to reduce false-positive test success - Adds verbose logging of the checks being done, to make it easier to debug this kind of issue in the future - Tightens the exception handling for missing previous versions to just the line that's expected to fail in that case - Only allows missing versions when "publish_to: none" is set - Adds that everywhere it's missing - Standardize the format in the repo to "none" (instead of also having "'none'"). - Allows the use of NEXT in CHANGELOG as a way of gathering changes that are worth noting, but not doing a publish cycle for. (Replaces the plan of using -dev versions, since that's actually harder to implement, and more confusing.) - Ensures that we don't forget to clean up NEXT entries when bumping versions --- script/tool/lib/src/common.dart | 16 +- .../tool/lib/src/version_check_command.dart | 122 ++++++---- script/tool/pubspec.yaml | 2 +- script/tool/test/version_check_test.dart | 224 +++++++++++++++--- 4 files changed, 287 insertions(+), 77 deletions(-) diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index fc1fdca6a1c..d8826c79308 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -596,10 +596,18 @@ class GitVersionFinder { return changedFiles.toList(); } - /// Get the package version specified in the pubspec file in `pubspecPath` and at the revision of `gitRef`. - Future getPackageVersion(String pubspecPath, String gitRef) async { - final io.ProcessResult gitShow = - await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); + /// 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); diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 086fa548880..14dc8fb28ef 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -13,8 +13,6 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common.dart'; -const String _kBaseSha = 'base-sha'; - /// Categories of version change types. enum NextVersionType { /// A breaking change. @@ -96,48 +94,59 @@ class VersionCheckCommand extends PluginCommand { final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); - final String baseSha = argResults[_kBaseSha] as String; + const String indentation = ' '; for (final String pubspecPath in changedPubspecs) { - try { - final File pubspecFile = fileSystem.file(pubspecPath); - if (!pubspecFile.existsSync()) { - continue; - } - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - if (pubspec.publishTo == 'none') { - continue; - } + print('Checking versions for $pubspecPath...'); + final File pubspecFile = fileSystem.file(pubspecPath); + if (!pubspecFile.existsSync()) { + print('${indentation}Deleted; skipping.'); + continue; + } + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + print('${indentation}Found "publish_to: none"; skipping.'); + continue; + } - final Version masterVersion = - await gitVersionFinder.getPackageVersion(pubspecPath, baseSha); - final Version headVersion = - await gitVersionFinder.getPackageVersion(pubspecPath, 'HEAD'); - if (headVersion == null) { - continue; // Example apps don't have versions - } + final Version headVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, gitRef: 'HEAD'); + if (headVersion == null) { + printErrorAndExit( + errorMessage: '${indentation}No version found. A package that ' + 'intentionally has no version should be marked ' + '"publish_to: none".'); + } + final Version masterVersion = + await gitVersionFinder.getPackageVersion(pubspecPath); + if (masterVersion == null) { + print('${indentation}Unable to find pubspec in master. ' + 'Safe to ignore if the project is new.'); + } - final Map allowedNextVersions = - getAllowedNextVersions(masterVersion, headVersion); + if (masterVersion == headVersion) { + print('${indentation}No version change.'); + continue; + } - if (!allowedNextVersions.containsKey(headVersion)) { - final String error = '$pubspecPath incorrectly updated version.\n' - 'HEAD: $headVersion, master: $masterVersion.\n' - 'Allowed versions: $allowedNextVersions'; - printErrorAndExit(errorMessage: error); - } + final Map allowedNextVersions = + getAllowedNextVersions(masterVersion, headVersion); - final bool isPlatformInterface = - pubspec.name.endsWith('_platform_interface'); - if (isPlatformInterface && - allowedNextVersions[headVersion] == - NextVersionType.BREAKING_MAJOR) { - final String error = '$pubspecPath breaking change detected.\n' - 'Breaking changes to platform interfaces are strongly discouraged.\n'; - printErrorAndExit(errorMessage: error); - } - } on io.ProcessException { - print('Unable to find pubspec in master for $pubspecPath.' - ' Safe to ignore if the project is new.'); + if (!allowedNextVersions.containsKey(headVersion)) { + final String error = '${indentation}Incorrectly updated version.\n' + '${indentation}HEAD: $headVersion, master: $masterVersion.\n' + '${indentation}Allowed versions: $allowedNextVersions'; + printErrorAndExit(errorMessage: error); + } else { + print('$indentation$headVersion -> $masterVersion'); + } + + final bool isPlatformInterface = + pubspec.name.endsWith('_platform_interface'); + if (isPlatformInterface && + allowedNextVersions[headVersion] == NextVersionType.BREAKING_MAJOR) { + final String error = '$pubspecPath breaking change detected.\n' + 'Breaking changes to platform interfaces are strongly discouraged.\n'; + printErrorAndExit(errorMessage: error); } } @@ -153,7 +162,7 @@ class VersionCheckCommand extends PluginCommand { final String packageName = plugin.basename; print('-----------------------------------------'); print( - 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for $packageName.'); + 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for $packageName.'); final Pubspec pubspec = _tryParsePubspec(plugin); if (pubspec == null) { @@ -169,12 +178,29 @@ class VersionCheckCommand extends PluginCommand { final Iterator iterator = lines.iterator; while (iterator.moveNext()) { if (iterator.current.trim().isNotEmpty) { - firstLineWithText = iterator.current; + firstLineWithText = iterator.current.trim(); break; } } // Remove all leading mark down syntax from the version line. - final String versionString = firstLineWithText.split(' ').last; + String versionString = firstLineWithText.split(' ').last; + + // Skip validation for the special NEXT version that's used to accumulate + // changes that don't warrant publishing on their own. + bool hasNextSection = versionString == 'NEXT'; + if (hasNextSection) { + print('Found NEXT; validating next version in the CHANGELOG.'); + // Ensure that the version in pubspec hasn't changed without updating + // CHANGELOG. That means the next version entry in the CHANGELOG pass the + // normal validation. + while (iterator.moveNext()) { + if (iterator.current.trim().startsWith('## ')) { + versionString = iterator.current.trim().split(' ').last; + break; + } + } + } + final Version fromChangeLog = Version.parse(versionString); if (fromChangeLog == null) { final String error = @@ -190,6 +216,18 @@ The first version listed in CHANGELOG.md is $fromChangeLog. '''; printErrorAndExit(errorMessage: error); } + + // If NEXT wasn't the first section, it should not exist at all. + if (!hasNextSection) { + final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); + if (lines.any((String line) => nextRegex.hasMatch(line))) { + printErrorAndExit(errorMessage: ''' +When bumping the version for release, the NEXT section should be incorporated +into the new version's release notes. + '''); + } + } + print('$packageName passed version check'); } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index e4712395900..9260e8f48fa 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_plugin_tools description: Productivity utils for hosting multiple plugins within one repository. -publish_to: 'none' +publish_to: none dependencies: args: "^1.4.3" diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index 96a460d7e72..ed953e0b7fa 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -62,6 +62,8 @@ void main() { final String response = gitShowResponses[invocation.positionalArguments[0][1]]; when(mockProcessResult.stdout as String).thenReturn(response); + } else if (invocation.positionalArguments[0][0] == 'merge-base') { + when(mockProcessResult.stdout as String).thenReturn('abc123'); } return Future.value(mockProcessResult); }); @@ -98,11 +100,12 @@ void main() { ); expect(gitDirCommands.length, equals(3)); expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect(gitDirCommands[1].join(' '), - equals('show master:packages/plugin/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin/pubspec.yaml')); + gitDirCommands, + containsAll([ + equals(['diff', '--name-only', 'master', 'HEAD']), + equals(['show', 'master:packages/plugin/pubspec.yaml']), + equals(['show', 'HEAD:packages/plugin/pubspec.yaml']), + ])); }); test('denies invalid version', () async { @@ -117,15 +120,50 @@ void main() { await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(const TypeMatcher()), ); expect(gitDirCommands.length, equals(3)); expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect(gitDirCommands[1].join(' '), - equals('show master:packages/plugin/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin/pubspec.yaml')); + gitDirCommands, + containsAll([ + equals(['diff', '--name-only', 'master', 'HEAD']), + equals(['show', 'master:packages/plugin/pubspec.yaml']), + equals(['show', 'HEAD:packages/plugin/pubspec.yaml']), + ])); + }); + + test('allows valid version without explicit base-sha', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = + await runCapturingPrint(runner, ['version-check']); + + expect( + output, + containsAllInOrder([ + 'No version check errors found!', + ]), + ); + }); + + test('denies invalid version without explicit base-sha', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + }; + final Future> result = + runCapturingPrint(runner, ['version-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); }); test('gracefully handles missing pubspec.yaml', () async { @@ -143,6 +181,8 @@ void main() { output, orderedEquals([ 'Determine diff with base sha: master', + 'Checking versions for packages/plugin/pubspec.yaml...', + ' Deleted; skipping.', 'No version check errors found!', ]), ); @@ -171,13 +211,18 @@ void main() { ); expect(gitDirCommands.length, equals(3)); expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect( - gitDirCommands[1].join(' '), - equals( - 'show master:packages/plugin_platform_interface/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + gitDirCommands, + containsAll([ + equals(['diff', '--name-only', 'master', 'HEAD']), + equals([ + 'show', + 'master:packages/plugin_platform_interface/pubspec.yaml' + ]), + equals([ + 'show', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml' + ]), + ])); }); test('disallows breaking changes to platform interfaces', () async { @@ -194,17 +239,22 @@ void main() { runner, ['version-check', '--base-sha=master']); await expectLater( output, - throwsA(const TypeMatcher()), + throwsA(const TypeMatcher()), ); expect(gitDirCommands.length, equals(3)); expect( - gitDirCommands[0].join(' '), equals('diff --name-only master HEAD')); - expect( - gitDirCommands[1].join(' '), - equals( - 'show master:packages/plugin_platform_interface/pubspec.yaml')); - expect(gitDirCommands[2].join(' '), - equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml')); + gitDirCommands, + containsAll([ + equals(['diff', '--name-only', 'master', 'HEAD']), + equals([ + 'show', + 'master:packages/plugin_platform_interface/pubspec.yaml' + ]), + equals([ + 'show', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml' + ]), + ])); }); test('Allow empty lines in front of the first version in CHANGELOG', @@ -230,7 +280,7 @@ void main() { expect( output, containsAllInOrder([ - 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.', + 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for plugin.', 'plugin passed version check', 'No version check errors found!' ]), @@ -255,7 +305,7 @@ void main() { runner, ['version-check', '--base-sha=master']); await expectLater( output, - throwsA(const TypeMatcher()), + throwsA(const TypeMatcher()), ); try { final List outputValue = await output; @@ -291,7 +341,7 @@ void main() { expect( output, containsAllInOrder([ - 'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.', + 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for plugin.', 'plugin passed version check', 'No version check errors found!' ]), @@ -322,7 +372,121 @@ void main() { runner, ['version-check', '--base-sha=master']); await expectLater( output, - throwsA(const TypeMatcher()), + throwsA(const TypeMatcher()), + ); + try { + final List outputValue = await output; + await expectLater( + outputValue, + containsAllInOrder([ + ''' + versions for plugin in CHANGELOG.md and pubspec.yaml do not match. + The version in pubspec.yaml is 1.0.0. + The first version listed in CHANGELOG.md is 1.0.1. + ''', + ]), + ); + } on ToolExit catch (_) {} + }); + + test('Allow NEXT as a placeholder for gathering CHANGELOG entries', + () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.0'); + const String changelog = ''' +## NEXT + +* Some changes that won't be published until the next time there's a release. + +## 1.0.0 + +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + containsAllInOrder([ + 'Found NEXT; validating next version in the CHANGELOG.', + 'plugin passed version check', + 'No version check errors found!', + ]), + ); + }); + + test('Fail if NEXT is left in the CHANGELOG when adding a version bump', + () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.1'); + const String changelog = ''' +## 1.0.1 + +* Some changes. + +## NEXT + +* Some changes that should have been folded in 1.0.1. + +## 1.0.0 + +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final Future> output = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + try { + final List outputValue = await output; + await expectLater( + outputValue, + containsAllInOrder([ + ''' + versions for plugin in CHANGELOG.md and pubspec.yaml do not match. + The version in pubspec.yaml is 1.0.0. + The first version listed in CHANGELOG.md is 1.0.1. + ''', + ]), + ); + } on ToolExit catch (_) {} + }); + + test('Fail if the version changes without replacing NEXT', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + + final Directory pluginDirectory = + mockPackagesDir.childDirectory('plugin'); + + createFakePubspec(pluginDirectory, + isFlutter: true, includeVersion: true, version: '1.0.1'); + const String changelog = ''' +## NEXT + +* Some changes that should be listed as part of 1.0.1. + +## 1.0.0 + +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final Future> output = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), ); try { final List outputValue = await output; From 63d42fcdb91ecbc987c500b6d7689f50808d4945 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 20 Apr 2021 04:24:02 -0700 Subject: [PATCH 027/249] Update PULL_REQUEST_TEMPLATE.md (#3801) --- script/tool/README.md | 57 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/script/tool/README.md b/script/tool/README.md index 162ca0d98a7..daea4d86390 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -1,8 +1,61 @@ # Flutter Plugin Tools +Note: The commands in tools are designed to run at the root of the repository or `/packages/`. + To run the tool: ```sh -dart pub get -dart run lib/src/main.dart +cd /script/tool && dart pub get && cd ../../ +dart run ./script/tool/lib/src/main.dart +``` + +## Format Code + +```sh +cd +dart run /script/tool/lib/src/main.dart format --plugins plugin_name +``` + +## Run static analyzer + +```sh +cd +pub run ./script/tool/lib/src/main.dart analyze --plugins plugin_name +``` + +## Run Dart unit tests + +```sh +cd +pub run ./script/tool/lib/src/main.dart test --plugins plugin_name +``` + +## Run XCTests + +```sh +cd +dart run lib/src/main.dart xctest --target RunnerUITests --skip ``` + +## Publish and tag release + +``sh +cd +git checkout +dart run ./script/tool/lib/src/main.dart publish-plugin --package +`` + +By default the tool tries to push tags to the `upstream` remote, but some +additional settings can be configured. Run `dart run ./script/tool/lib/src/main.dart publish-plugin --help` for more usage information. + +The tool wraps `pub publish` for pushing the package to pub, and then will +automatically use git to try to create and push tags. It has some additional +safety checking around `pub publish` too. By default `pub publish` publishes +_everything_, including untracked or uncommitted files in version control. +`publish-plugin` will first check the status of the local +directory and refuse to publish if there are any mismatched files with version +control present. + +There is a lot about this process that is still to be desired. Some top level +items are being tracked in +[flutter/flutter#27258](https://github.com/flutter/flutter/issues/27258). From c4170d8f6464fbfe2fcfca98249910d58541e41d Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 21 Apr 2021 16:40:20 -0700 Subject: [PATCH 028/249] [tools] fix version check command not working for new packages (#3818) --- .../tool/lib/src/version_check_command.dart | 2 +- script/tool/test/version_check_test.dart | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 14dc8fb28ef..0b552e8bff4 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io' as io; import 'package:meta/meta.dart'; import 'package:file/file.dart'; @@ -121,6 +120,7 @@ class VersionCheckCommand extends PluginCommand { if (masterVersion == null) { print('${indentation}Unable to find pubspec in master. ' 'Safe to ignore if the project is new.'); + continue; } if (masterVersion == headVersion) { diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index ed953e0b7fa..04348310a2f 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -61,6 +61,9 @@ void main() { } else if (invocation.positionalArguments[0][0] == 'show') { final String response = gitShowResponses[invocation.positionalArguments[0][1]]; + if (response == null) { + throw const io.ProcessException('git', ['show']); + } when(mockProcessResult.stdout as String).thenReturn(response); } else if (invocation.positionalArguments[0][0] == 'merge-base') { when(mockProcessResult.stdout as String).thenReturn('abc123'); @@ -150,6 +153,23 @@ void main() { ); }); + test('allows valid version for new package.', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'HEAD:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + final List output = + await runCapturingPrint(runner, ['version-check']); + + expect( + output, + containsAllInOrder([ + 'No version check errors found!', + ]), + ); + }); + test('denies invalid version without explicit base-sha', () async { createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; From 3c57df37c050b9582aca97781c0bc814cedcd2dc Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 23 Apr 2021 10:19:03 -0700 Subject: [PATCH 029/249] Move all null safety packages' min dart sdk to 2.12.0 (#3822) --- .../tool/lib/src/publish_check_command.dart | 16 ++++++++++++- .../tool/test/publish_check_command_test.dart | 23 +++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 07bf0aa8667..0fb9dbad60a 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -18,7 +18,17 @@ class PublishCheckCommand extends PluginCommand { Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag( + _allowPrereleaseFlag, + help: 'Allows the pre-release SDK warning to pass.\n' + 'When enabled, a pub warning, which asks to publish the package as a pre-release version when ' + 'the SDK constraint is a pre-release version, is ignored.', + defaultsTo: false, + ); + } + + static const String _allowPrereleaseFlag = 'allow-pre-release'; @override final String name = 'publish-check'; @@ -98,6 +108,10 @@ class PublishCheckCommand extends PluginCommand { return true; } + if (!(argResults[_allowPrereleaseFlag] as bool)) { + return false; + } + await stdOutCompleter.future; await stdInCompleter.future; diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 568d35eeff2..0a9d36f2ea6 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -90,7 +90,7 @@ void main() { throwsA(isA())); }); - test('pass on prerelease', () async { + test('pass on prerelease if --allow-pre-release flag is on', () async { createFakePlugin('d'); const String preReleaseOutput = 'Package has 1 warning.' @@ -105,7 +105,26 @@ void main() { processRunner.processesToReturn.add(process); - expect(runner.run(['publish-check']), completes); + expect(runner.run(['publish-check', '--allow-pre-release']), + completes); + }); + + test('fail on prerelease if --allow-pre-release flag is off', () async { + createFakePlugin('d'); + + const String preReleaseOutput = 'Package has 1 warning.' + 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; + + final MockProcess process = MockProcess(); + process.stdoutController.add(preReleaseOutput.codeUnits); + process.stdoutController.close(); // ignore: unawaited_futures + process.stderrController.close(); // ignore: unawaited_futures + + process.exitCodeCompleter.complete(1); + + processRunner.processesToReturn.add(process); + + expect(runner.run(['publish-check']), throwsA(isA())); }); }); } From 189845bb8af7b0e6fc96000093bc4b43753978c8 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 27 Apr 2021 09:35:05 -0700 Subject: [PATCH 030/249] [tool] combine run and runAndExitOnError (#3827) --- script/tool/lib/src/common.dart | 43 +++++++----------- .../lib/src/firebase_test_lab_command.dart | 15 ++++--- script/tool/lib/src/format_command.dart | 19 +++++--- .../tool/lib/src/lint_podspecs_command.dart | 9 +++- .../tool/lib/src/publish_plugin_command.dart | 44 ++++++++++++------- .../test/publish_plugin_command_test.dart | 6 ++- script/tool/test/util.dart | 26 +++-------- 7 files changed, 87 insertions(+), 75 deletions(-) diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index d8826c79308..c16ba8e957e 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -500,17 +500,33 @@ class ProcessRunner { /// /// 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 { - return io.Process.run(executable, args, + 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]. @@ -526,31 +542,6 @@ class ProcessRunner { return process; } - /// Run the [executable] with [args], throwing an error on non-zero exit code. - /// - /// Unlike [runAndStream], this does not stream the process output to stdout. - /// It also unconditionally throws an error on a non-zero exit code. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// Returns the [io.ProcessResult] of running the [executable]. - Future runAndExitOnError( - String executable, - List args, { - Directory workingDir, - }) async { - final io.ProcessResult result = await io.Process.run(executable, args, - workingDirectory: workingDir?.path); - if (result.exitCode != 0) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error Stderr:\n${result.stdout}'); - throw ToolExit(result.exitCode); - } - return result; - } - String _getErrorString(String executable, List args, {Directory workingDir}) { final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 2998522da03..ff7d05bf4e8 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -73,11 +73,16 @@ class FirebaseTestLabCommand extends PluginCommand { } else { _firebaseProjectConfigured = Completer(); } - await processRunner.runAndExitOnError('gcloud', [ - 'auth', - 'activate-service-account', - '--key-file=${argResults['service-key']}', - ]); + await processRunner.run( + 'gcloud', + [ + 'auth', + 'activate-service-account', + '--key-file=${argResults['service-key']}', + ], + exitOnError: true, + logOnError: true, + ); final int exitCode = await processRunner.runAndStream('gcloud', [ 'config', 'set', diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 19b6004d244..9c29f0f8c68 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -56,9 +56,13 @@ class FormatCommand extends PluginCommand { } Future _didModifyAnything() async { - final io.ProcessResult modifiedFiles = await processRunner - .runAndExitOnError('git', ['ls-files', '--modified'], - workingDir: packagesDir); + final io.ProcessResult modifiedFiles = await processRunner.run( + 'git', + ['ls-files', '--modified'], + workingDir: packagesDir, + exitOnError: true, + logOnError: true, + ); print('\n\n'); @@ -76,8 +80,13 @@ class FormatCommand extends PluginCommand { 'this command into your terminal:'); print('patch -p1 <['diff'], workingDir: packagesDir); + final io.ProcessResult diff = await processRunner.run( + 'git', + ['diff'], + workingDir: packagesDir, + exitOnError: true, + logOnError: true, + ); print(diff.stdout); print('DONE'); return true; diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 1fe6b71cf11..ebcefd6c234 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -58,8 +58,13 @@ class LintPodspecsCommand extends PluginCommand { return; } - await processRunner.runAndExitOnError('which', ['pod'], - workingDir: packagesDir); + await processRunner.run( + 'which', + ['pod'], + workingDir: packagesDir, + exitOnError: true, + logOnError: true, + ); _print('Starting podspec lint test'); diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 0dae3a502be..201130db754 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -136,8 +136,13 @@ class PublishPluginCommand extends PluginCommand { @required bool shouldPushTag}) async { final String tag = _getTag(packageDir); _print('Tagging release $tag...'); - await processRunner.runAndExitOnError('git', ['tag', tag], - workingDir: packageDir); + await processRunner.run( + 'git', + ['tag', tag], + workingDir: packageDir, + exitOnError: true, + logOnError: true, + ); if (!shouldPushTag) { return; } @@ -163,15 +168,13 @@ class PublishPluginCommand extends PluginCommand { } Future _checkGitStatus(Directory packageDir) async { - final ProcessResult statusResult = await processRunner.runAndExitOnError( - 'git', - [ - 'status', - '--porcelain', - '--ignored', - packageDir.absolute.path - ], - workingDir: packageDir); + final ProcessResult statusResult = await processRunner.run( + 'git', + ['status', '--porcelain', '--ignored', packageDir.absolute.path], + workingDir: packageDir, + logOnError: true, + exitOnError: true, + ); final String statusOutput = statusResult.stdout as String; if (statusOutput.isNotEmpty) { @@ -184,9 +187,13 @@ class PublishPluginCommand extends PluginCommand { } Future _verifyRemote(String remote) async { - final ProcessResult remoteInfo = await processRunner.runAndExitOnError( - 'git', ['remote', 'get-url', remote], - workingDir: packagesDir); + final ProcessResult remoteInfo = await processRunner.run( + 'git', + ['remote', 'get-url', remote], + workingDir: packagesDir, + exitOnError: true, + logOnError: true, + ); return remoteInfo.stdout as String; } @@ -239,7 +246,12 @@ class PublishPluginCommand extends PluginCommand { _print('Tag push canceled.'); throw ToolExit(1); } - await processRunner.runAndExitOnError('git', ['push', remote, tag], - workingDir: packagesDir); + await processRunner.run( + 'git', + ['push', remote, tag], + workingDir: packagesDir, + exitOnError: true, + logOnError: true, + ); } } diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 03e7858d3bc..0cf709adc0d 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -328,10 +328,14 @@ class TestProcessRunner extends ProcessRunner { final List pushTagsArgs = []; @override - Future runAndExitOnError( + Future run( String executable, List args, { Directory workingDir, + bool exitOnError = false, + bool logOnError = false, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding, }) async { // Don't ever really push tags. if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 5a2c42bd319..4dc019c968d 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -235,32 +235,18 @@ class RecordingProcessRunner extends ProcessRunner { /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. @override - Future run(String executable, List args, - {Directory workingDir, - bool exitOnError = false, - Encoding stdoutEncoding = io.systemEncoding, - Encoding stderrEncoding = io.systemEncoding}) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - io.ProcessResult result; - - if (processToReturn != null) { - result = io.ProcessResult( - processToReturn.pid, - await processToReturn.exitCode, - resultStdout ?? processToReturn.stdout, - resultStderr ?? processToReturn.stderr); - } - return Future.value(result); - } - - @override - Future runAndExitOnError( + Future run( String executable, List args, { Directory workingDir, + bool exitOnError = false, + bool logOnError = false, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding, }) async { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); io.ProcessResult result; + if (processToReturn != null) { result = io.ProcessResult( processToReturn.pid, From fe6b84793243c817c632516c3a38c025310900cc Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 29 Apr 2021 20:05:38 -0700 Subject: [PATCH 031/249] Prep the tools for publishing (#3836) Re-adds the LICENSE and CHANGELOG, and updates the README and pubspec, in preparation for pushing an updated version of the package. We are still using flutter_plugin_tools in flutter/packages, so this allows us to use updates (e.g., license checks, fixed version checks) in that repository as well. The README has been updated to note that it is no longer intended for general use, and we will (if it allows publishing) continue to mark the package as discontinued to reflect that. --- script/tool/CHANGELOG.md | 307 +++++++++++++++++++++++++++++++++++++++ script/tool/LICENSE | 25 ++++ script/tool/README.md | 9 +- script/tool/pubspec.yaml | 9 +- 4 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 script/tool/CHANGELOG.md create mode 100644 script/tool/LICENSE diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md new file mode 100644 index 00000000000..e489a29d0cf --- /dev/null +++ b/script/tool/CHANGELOG.md @@ -0,0 +1,307 @@ +## 0.1.0 + +- **NOTE**: This is no longer intended as a general-purpose package, and is now + supported only for flutter/plugins and flutter/tools. +- Fix version checks + - Remove handling of pre-release null-safe versions +- Fix build all for null-safe template apps +- Improve handling of web integration tests +- Supports enforcing standardized copyright files +- Improve handling of iOS tests + +## v.0.0.45+3 + +- Pin `collection` to `1.14.13` to be able to target Flutter stable (v1.22.6). + +## v.0.0.45+2 + +- Make `publish-plugin` to work on non-flutter packages. + +## v.0.0.45+1 + +- Don't call `flutter format` if there are no Dart files to format. + +## v.0.0.45 + +- Add exclude flag to exclude any plugin from further processing. + +## v.0.0.44+7 + +- `all-plugins-app` doesn't override the AGP version. + +## v.0.0.44+6 + +- Fix code formatting. + +## v.0.0.44+5 + +- Remove `-v` flag on drive-examples. + +## v.0.0.44+4 + +- Fix bug where directory isn't passed + +## v.0.0.44+3 + +- More verbose logging + +## v.0.0.44+2 + +- Remove pre-alpha Windows workaround to create examples on the fly. + +## v.0.0.44+1 + +- Print packages that passed tests in `xctest` command. +- Remove printing the whole list of simulators. + +## v.0.0.44 + +- Add 'xctest' command to run xctests. + +## v.0.0.43 + +- Allow minor `*-nullsafety` pre release packages. + +## v.0.0.42+1 + +- Fix test command when `--enable-experiment` is called. + +## v.0.0.42 + +- Allow `*-nullsafety` pre release packages. + +## v.0.0.41 + +- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, +and `firebase-test-lab`. + +## v.0.0.40 + +- Support `integration_test/` directory for `drive-examples` command + +## v.0.0.39 + +- Support `integration_test/` directory for `package:integration_test` + +## v.0.0.38 + +- Add C++ and ObjC++ to clang-format. + +## v.0.0.37+2 + +- Make `http` and `http_multi_server` dependency version constraint more flexible. + +## v.0.0.37+1 + +- All_plugin test puts the plugin dependencies into dependency_overrides. + +## v.0.0.37 + +- Only builds mobile example apps when necessary. + +## v.0.0.36+3 + +- Add support for Linux plugins. + +## v.0.0.36+2 + +- Default to showing podspec lint warnings + +## v.0.0.36+1 + +- Serialize linting podspecs. + +## v.0.0.36 + +- Remove retry on Firebase Test Lab's call to gcloud set. +- Remove quiet flag from Firebase Test Lab's gcloud set command. +- Allow Firebase Test Lab command to continue past gcloud set network failures. + This is a mitigation for the network service sometimes not responding, + but it isn't actually necessary to have a network connection for this command. + +## v.0.0.35+1 + +- Minor cleanup to the analyze test. + +## v.0.0.35 + +- Firebase Test Lab command generates a configurable unique path suffix for results. + +## v.0.0.34 + +- Firebase Test Lab command now only tries to configure the project once +- Firebase Test Lab command now retries project configuration up to five times. + +## v.0.0.33+1 + +- Fixes formatting issues that got past our CI due to + https://github.com/flutter/flutter/issues/51585. +- Changes the default package name for testing method `createFakePubspec` back + its previous behavior. + +## v.0.0.33 + +- Version check command now fails on breaking changes to platform interfaces. +- Updated version check test to be more flexible. + +## v.0.0.32+7 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. + +## v.0.0.32+6 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. + +## v.0.0.32+5 + +- Remove --fail-fast and --silent from lint podspec command. + +## v.0.0.32+4 + +- Update `publish-plugin` to use `flutter pub publish` instead of just `pub + publish`. Enforces a `pub publish` command that matches the Dart SDK in the + user's Flutter install. + +## v.0.0.32+3 + +- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). + +## v.0.0.32+2 + +- Runs pub get before building macos to avoid failures. + +## v.0.0.32+1 + +- Default macOS example builds to false. Previously they were running whenever + CI was itself running on macOS. + +## v.0.0.32 + +- `analyze` now asserts that the global `analysis_options.yaml` is the only one + by default. Individual directories can be excluded from this check with the + new `--custom-analysis` flag. + +## v.0.0.31+1 + +- Add --skip and --no-analyze flags to podspec command. + +## v.0.0.31 + +- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. + +## v.0.0.30 + +- Adopt pedantic analysis options, fix firebase_test_lab_test. + +## v.0.0.29 + +- Add a command to run pod lib lint on podspec files. + +## v.0.0.28 + +- Increase Firebase test lab timeouts to 5 minutes. + +## v.0.0.27 + +- Run tests with `--platform=chrome` for web plugins. + +## v.0.0.26 + +- Add a command for publishing plugins to pub. + +## v.0.0.25 + +- Update `DriveExamplesCommand` to use `ProcessRunner`. +- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. +- Add simple tests for `DriveExamplesCommand`. + +## v.0.0.24 + +- Gracefully handle pubspec.yaml files for new plugins. +- Additional unit testing. + +## v.0.0.23 + +- Add a test case for transitive dependency solving in the + `create_all_plugins_app` command. + +## v.0.0.22 + +- Updated firebase-test-lab command with updated conventions for test locations. +- Updated firebase-test-lab to add an optional "device" argument. +- Updated version-check command to always compare refs instead of using the working copy. +- Added unit tests for the firebase-test-lab and version-check commands. +- Add ProcessRunner to mock running processes for testing. + +## v.0.0.21 + +- Support the `--plugins` argument for federated plugins. + +## v.0.0.20 + +- Support for finding federated plugins, where one directory contains + multiple packages for different platform implementations. + +## v.0.0.19+3 + +- Use `package:file` for file I/O. + +## v.0.0.19+2 + +- Use java as language when calling `flutter create`. + +## v.0.0.19+1 + +- Rename command for `CreateAllPluginsAppCommand`. + +## v.0.0.19 + +- Use flutter create to build app testing plugin compilation. + +## v.0.0.18+2 + +- Fix `.travis.yml` file name in `README.md`. + +## v0.0.18+1 + +- Skip version check if it contains `publish_to: none`. + +## v0.0.18 + +- Add option to exclude packages from generated pubspec command. + +## v0.0.17+4 + +- Avoid trying to version-check pubspecs that are missing a version. + +## v0.0.17+3 + +- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). + +## v0.0.17+2 + +- Fix exception handling for version checker + +## v0.0.17+1 + +- Fix bug where we used a flag instead of an option + +## v0.0.17 + +- Add a command for checking the version number + +## v0.0.16 + +- Add a command for generating `pubspec.yaml` for All Plugins app. + +## v0.0.15 + +- Add a command for running driver tests of plugin examples. + +## v0.0.14 + +- Check for dependencies->flutter instead of top level flutter node. + +## v0.0.13 + +- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/script/tool/LICENSE b/script/tool/LICENSE new file mode 100644 index 00000000000..c6823b81eb8 --- /dev/null +++ b/script/tool/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/script/tool/README.md b/script/tool/README.md index daea4d86390..b759b9a9ce5 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -1,8 +1,15 @@ # Flutter Plugin Tools +This is a set of utilities used in the flutter/plugins and flutter/packages +repositories. It is no longer explictily maintained as a general-purpose tool +for multi-package repositories, so your mileage may vary if using it in other +repositories. + +---- + Note: The commands in tools are designed to run at the root of the repository or `/packages/`. -To run the tool: +To run the tool (in flutter/plugins): ```sh cd /script/tool && dart pub get && cd ../../ diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 9260e8f48fa..4b5f4987dde 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,6 +1,7 @@ name: flutter_plugin_tools -description: Productivity utils for hosting multiple plugins within one repository. -publish_to: none +description: Productivity utils for flutter/plugins and flutter/packages +repository: https://github.com/flutter/plugins/tree/master/script/tool +version: 0.1.0 dependencies: args: "^1.4.3" @@ -19,12 +20,12 @@ dependencies: file: ^5.0.10 uuid: ^2.0.4 http_multi_server: ^2.2.0 - collection: 1.14.13 + collection: ^1.14.13 dev_dependencies: matcher: ^0.12.6 mockito: ^4.1.1 - pedantic: 1.8.0 + pedantic: ^1.8.0 environment: sdk: ">=2.3.0 <3.0.0" From 142fc9793d3f09835c881f4ad5dd2290966d8884 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Fri, 30 Apr 2021 07:02:19 -0700 Subject: [PATCH 032/249] switch from using 'tuneup' to analyze to 'dart analyze' (#3837) --- script/tool/lib/src/analyze_command.dart | 12 +++--------- script/tool/test/analyze_command_test.dart | 15 +++------------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index e22bb0b17ba..e17400fa887 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -29,8 +29,8 @@ class AnalyzeCommand extends PluginCommand { final String name = 'analyze'; @override - final String description = 'Analyzes all packages using package:tuneup.\n\n' - 'This command requires "pub" and "flutter" to be in your path.'; + final String description = 'Analyzes all packages using dart analyze.\n\n' + 'This command requires "dart" and "flutter" to be in your path.'; @override Future run() async { @@ -57,11 +57,6 @@ class AnalyzeCommand extends PluginCommand { throw ToolExit(1); } - print('Activating tuneup package...'); - await processRunner.runAndStream( - 'pub', ['global', 'activate', 'tuneup'], - workingDir: packagesDir, exitOnError: true); - await for (final Directory package in getPackages()) { if (isFlutterPackage(package, fileSystem)) { await processRunner.runAndStream('flutter', ['packages', 'get'], @@ -75,8 +70,7 @@ class AnalyzeCommand extends PluginCommand { final List failingPackages = []; await for (final Directory package in getPlugins()) { final int exitCode = await processRunner.runAndStream( - 'pub', ['global', 'run', 'tuneup', 'check'], - workingDir: package); + 'dart', ['analyze'], workingDir: package); if (exitCode != 0) { failingPackages.add(p.basename(package.path)); } diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 7b23ea9778e..ac6b8eea676 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -42,16 +42,12 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('pub', const ['global', 'activate', 'tuneup'], - mockPackagesDir.path), ProcessCall( 'flutter', const ['packages', 'get'], plugin1Dir.path), ProcessCall( 'flutter', const ['packages', 'get'], plugin2Dir.path), - ProcessCall('pub', const ['global', 'run', 'tuneup', 'check'], - plugin1Dir.path), - ProcessCall('pub', const ['global', 'run', 'tuneup', 'check'], - plugin2Dir.path), + ProcessCall('dart', const ['analyze'], plugin1Dir.path), + ProcessCall('dart', const ['analyze'], plugin2Dir.path), ])); }); @@ -88,14 +84,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('pub', const ['global', 'activate', 'tuneup'], - mockPackagesDir.path), ProcessCall( 'flutter', const ['packages', 'get'], pluginDir.path), - ProcessCall( - 'pub', - const ['global', 'run', 'tuneup', 'check'], - pluginDir.path), + ProcessCall('dart', const ['analyze'], pluginDir.path), ])); }); From 2bd3f401a7d1d1e26c3869858a7bec17c12b9845 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 30 Apr 2021 12:47:14 -0700 Subject: [PATCH 033/249] Re-add bin/ to flutter_plugin_tools (#3839) This should have been re-added in #3836 but was missed, so it doesn't work correctly via `pub global`. --- script/tool/CHANGELOG.md | 4 ++++ script/tool/bin/flutter_plugin_tools.dart | 5 +++++ script/tool/pubspec.yaml | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 script/tool/bin/flutter_plugin_tools.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index e489a29d0cf..0411a9e53ab 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0+1 + +- Re-add the bin/ directory. + ## 0.1.0 - **NOTE**: This is no longer intended as a general-purpose package, and is now diff --git a/script/tool/bin/flutter_plugin_tools.dart b/script/tool/bin/flutter_plugin_tools.dart new file mode 100644 index 00000000000..0f30bee0d25 --- /dev/null +++ b/script/tool/bin/flutter_plugin_tools.dart @@ -0,0 +1,5 @@ +// 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. + +export 'package:flutter_plugin_tools/src/main.dart'; diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 4b5f4987dde..18631fef02d 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.1.0 +version: 0.1.0+1 dependencies: args: "^1.4.3" From 184e9a7023f14fbb505cfdda96ba301e1422d8a7 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 30 Apr 2021 15:29:04 -0700 Subject: [PATCH 034/249] [tool] add `all` and `dry-run` flags to publish-plugin command (#3776) --- .../tool/lib/src/publish_plugin_command.dart | 368 +++++++++-- .../test/publish_plugin_command_test.dart | 606 +++++++++++++++++- script/tool/test/util.dart | 13 +- 3 files changed, 889 insertions(+), 98 deletions(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 201130db754..9bfa0e71743 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -4,12 +4,14 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; +import 'dart:io' as io; import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'common.dart'; @@ -32,10 +34,12 @@ class PublishPluginCommand extends PluginCommand { FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), Print print = print, - Stdin stdinput, + io.Stdin stdinput, + GitDir gitDir, }) : _print = print, - _stdin = stdinput ?? stdin, - super(packagesDir, fileSystem, processRunner: processRunner) { + _stdin = stdinput ?? io.stdin, + super(packagesDir, fileSystem, + processRunner: processRunner, gitDir: gitDir) { argParser.addOption( _packageOption, help: 'The package to publish.' @@ -64,6 +68,22 @@ class PublishPluginCommand extends PluginCommand { // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. defaultsTo: 'upstream', ); + argParser.addFlag( + _allChangedFlag, + help: + 'Release all plugins that contains pubspec changes at the current commit compares to the base-sha.\n' + 'The $_packageOption option is ignored if this is on.', + defaultsTo: false, + ); + argParser.addFlag( + _dryRunFlag, + help: + 'Skips the real `pub publish` and `git tag` commands and assumes both commands are successful.\n' + 'This does not run `pub publish --dry-run`.\n' + 'If you want to run the command with `pub publish --dry-run`, use `pub-publish-flags=--dry-run`', + defaultsTo: false, + negatable: true, + ); } static const String _packageOption = 'package'; @@ -71,6 +91,8 @@ class PublishPluginCommand extends PluginCommand { static const String _pushTagsOption = 'push-tags'; static const String _pubFlagsOption = 'pub-publish-flags'; static const String _remoteOption = 'remote'; + static const String _allChangedFlag = 'all-changed'; + static const String _dryRunFlag = 'dry-run'; // Version tags should follow -v. For example, // `flutter_plugin_tools-v0.0.24`. @@ -84,14 +106,14 @@ class PublishPluginCommand extends PluginCommand { 'Attempts to publish the given plugin and tag its release on GitHub.'; final Print _print; - final Stdin _stdin; - // The directory of the actual package that we are publishing. + final io.Stdin _stdin; StreamSubscription _stdinSubscription; @override Future run() async { final String package = argResults[_packageOption] as String; - if (package == null) { + final bool publishAllChanged = argResults[_allChangedFlag] as bool; + if (package == null && !publishAllChanged) { _print( 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); throw ToolExit(1); @@ -102,6 +124,8 @@ class PublishPluginCommand extends PluginCommand { _print('$packagesDir is not a valid Git repository.'); throw ToolExit(1); } + final GitDir baseGitDir = + await GitDir.fromExisting(packagesDir.path, allowSubdirectory: true); final bool shouldPushTag = argResults[_pushTagsOption] == true; final String remote = argResults[_remoteOption] as String; @@ -110,50 +134,229 @@ class PublishPluginCommand extends PluginCommand { remoteUrl = await _verifyRemote(remote); } _print('Local repo is ready!'); + if (argResults[_dryRunFlag] as bool) { + _print('=============== DRY RUN ==============='); + } + + bool successful; + if (publishAllChanged) { + successful = await _publishAllChangedPackages( + remote: remote, + remoteUrl: remoteUrl, + shouldPushTag: shouldPushTag, + baseGitDir: baseGitDir, + ); + } else { + successful = await _publishAndTagPackage( + packageDir: _getPackageDir(package), + remote: remote, + remoteUrl: remoteUrl, + shouldPushTag: shouldPushTag, + ); + } + await _finish(successful); + } + + Future _publishAllChangedPackages({ + String remote, + String remoteUrl, + bool shouldPushTag, + GitDir baseGitDir, + }) async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + if (changedPubspecs.isEmpty) { + _print('No version updates in this commit.'); + return true; + } + _print('Getting existing tags...'); + final io.ProcessResult existingTagsResult = + await baseGitDir.runCommand(['tag', '--sort=-committerdate']); + final List existingTags = (existingTagsResult.stdout as String) + .split('\n') + ..removeWhere((String element) => element.isEmpty); + + final List packagesReleased = []; + final List packagesFailed = []; + + for (final String pubspecPath in changedPubspecs) { + final File pubspecFile = + fileSystem.directory(baseGitDir.path).childFile(pubspecPath); + final _CheckNeedsReleaseResult result = await _checkNeedsRelease( + pubspecFile: pubspecFile, + gitVersionFinder: gitVersionFinder, + existingTags: existingTags, + ); + switch (result) { + case _CheckNeedsReleaseResult.release: + break; + case _CheckNeedsReleaseResult.noRelease: + continue; + case _CheckNeedsReleaseResult.failure: + packagesFailed.add(pubspecFile.parent.basename); + continue; + } + _print('\n'); + if (await _publishAndTagPackage( + packageDir: pubspecFile.parent, + remote: remote, + remoteUrl: remoteUrl, + shouldPushTag: shouldPushTag, + )) { + packagesReleased.add(pubspecFile.parent.basename); + } else { + packagesFailed.add(pubspecFile.parent.basename); + } + _print('\n'); + } + if (packagesReleased.isNotEmpty) { + _print('Packages released: ${packagesReleased.join(', ')}'); + } + if (packagesFailed.isNotEmpty) { + _print( + 'Failed to release the following packages: ${packagesFailed.join(', ')}, see above for details.'); + } + return packagesFailed.isEmpty; + } - final Directory packageDir = _getPackageDir(package); - await _publishPlugin(packageDir: packageDir); + // Publish the package to pub with `pub publish`. + // If `_tagReleaseOption` is on, git tag the release. + // If `shouldPushTag` is `true`, the tag will be pushed to `remote`. + // Returns `true` if publishing and tag are successful. + Future _publishAndTagPackage({ + @required Directory packageDir, + @required String remote, + @required String remoteUrl, + @required bool shouldPushTag, + }) async { + if (!await _publishPlugin(packageDir: packageDir)) { + return false; + } if (argResults[_tagReleaseOption] as bool) { - await _tagRelease( - packageDir: packageDir, - remote: remote, - remoteUrl: remoteUrl, - shouldPushTag: shouldPushTag); + if (!await _tagRelease( + packageDir: packageDir, + remote: remote, + remoteUrl: remoteUrl, + shouldPushTag: shouldPushTag, + )) { + return false; + } } - await _finishSuccesfully(); + _print('Released [${packageDir.basename}] successfully.'); + return true; } - Future _publishPlugin({@required Directory packageDir}) async { - await _checkGitStatus(packageDir); - await _publish(packageDir); + // Returns a [_CheckNeedsReleaseResult] that indicates the result. + Future<_CheckNeedsReleaseResult> _checkNeedsRelease({ + @required File pubspecFile, + @required GitVersionFinder gitVersionFinder, + @required List existingTags, + }) async { + if (!pubspecFile.existsSync()) { + _print(''' +The file at The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. +Safe to ignore if the package is deleted in this commit. +'''); + return _CheckNeedsReleaseResult.noRelease; + } + + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + return _CheckNeedsReleaseResult.noRelease; + } + + if (pubspec.version == null) { + _print('No version found. A package that intentionally has no version should be marked "publish_to: none"'); + return _CheckNeedsReleaseResult.failure; + } + + if (pubspec.name == null) { + _print('Fatal: Package name is null.'); + return _CheckNeedsReleaseResult.failure; + } + // Get latest tagged version and compare with the current version. + // TODO(cyanglaz): Check latest version of the package on pub instead of git + // https://github.com/flutter/flutter/issues/81047 + + final String latestTag = existingTags.firstWhere( + (String tag) => tag.split('-v').first == pubspec.name, + orElse: () => ''); + if (latestTag.isNotEmpty) { + final String latestTaggedVersion = latestTag.split('-v').last; + final Version latestVersion = Version.parse(latestTaggedVersion); + if (pubspec.version < latestVersion) { + _print( + 'The new version (${pubspec.version}) is lower than the current version ($latestVersion) for ${pubspec.name}.\nThis git commit is a revert, no release is tagged.'); + return _CheckNeedsReleaseResult.noRelease; + } + } + return _CheckNeedsReleaseResult.release; + } + + // Publish the plugin. + // + // Returns `true` if successful, `false` otherwise. + Future _publishPlugin({@required Directory packageDir}) async { + final bool gitStatusOK = await _checkGitStatus(packageDir); + if (!gitStatusOK) { + return false; + } + final bool publishOK = await _publish(packageDir); + if (!publishOK) { + return false; + } _print('Package published!'); + return true; } - Future _tagRelease( - {@required Directory packageDir, - @required String remote, - @required String remoteUrl, - @required bool shouldPushTag}) async { + // Tag the release with -v + // + // Return `true` if successful, `false` otherwise. + Future _tagRelease({ + @required Directory packageDir, + @required String remote, + @required String remoteUrl, + @required bool shouldPushTag, + }) async { final String tag = _getTag(packageDir); _print('Tagging release $tag...'); - await processRunner.run( - 'git', - ['tag', tag], - workingDir: packageDir, - exitOnError: true, - logOnError: true, - ); + if (!(argResults[_dryRunFlag] as bool)) { + final io.ProcessResult result = await processRunner.run( + 'git', + ['tag', tag], + workingDir: packageDir, + exitOnError: false, + logOnError: true, + ); + if (result.exitCode != 0) { + return false; + } + } + if (!shouldPushTag) { - return; + return true; } _print('Pushing tag to $remote...'); - await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); + return await _pushTagToRemote( + remote: remote, + tag: tag, + remoteUrl: remoteUrl, + ); } - Future _finishSuccesfully() async { - await _stdinSubscription.cancel(); - _print('Done!'); + Future _finish(bool successful) async { + if (_stdinSubscription != null) { + await _stdinSubscription.cancel(); + _stdinSubscription = null; + } + if (successful) { + _print('Done!'); + } else { + _print('Failed, see above for details.'); + throw ToolExit(1); + } } // Returns the packageDirectory based on the package name. @@ -167,14 +370,17 @@ class PublishPluginCommand extends PluginCommand { return packageDir; } - Future _checkGitStatus(Directory packageDir) async { - final ProcessResult statusResult = await processRunner.run( + Future _checkGitStatus(Directory packageDir) async { + final io.ProcessResult statusResult = await processRunner.run( 'git', ['status', '--porcelain', '--ignored', packageDir.absolute.path], workingDir: packageDir, logOnError: true, - exitOnError: true, + exitOnError: false, ); + if (statusResult.exitCode != 0) { + return false; + } final String statusOutput = statusResult.stdout as String; if (statusOutput.isNotEmpty) { @@ -182,12 +388,12 @@ class PublishPluginCommand extends PluginCommand { "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" '$statusOutput\n' 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); - throw ToolExit(1); } + return statusOutput.isEmpty; } Future _verifyRemote(String remote) async { - final ProcessResult remoteInfo = await processRunner.run( + final io.ProcessResult remoteInfo = await processRunner.run( 'git', ['remote', 'get-url', remote], workingDir: packagesDir, @@ -197,28 +403,31 @@ class PublishPluginCommand extends PluginCommand { return remoteInfo.stdout as String; } - Future _publish(Directory packageDir) async { + Future _publish(Directory packageDir) async { final List publishFlags = argResults[_pubFlagsOption] as List; _print( 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); - final Process publish = await processRunner.start( - 'flutter', ['pub', 'publish'] + publishFlags, - workingDirectory: packageDir); - publish.stdout - .transform(utf8.decoder) - .listen((String data) => _print(data)); - publish.stderr - .transform(utf8.decoder) - .listen((String data) => _print(data)); - _stdinSubscription = _stdin - .transform(utf8.decoder) - .listen((String data) => publish.stdin.writeln(data)); - final int result = await publish.exitCode; - if (result != 0) { - _print('Publish failed. Exiting.'); - throw ToolExit(result); + if (!(argResults[_dryRunFlag] as bool)) { + final io.Process publish = await processRunner.start( + 'flutter', ['pub', 'publish'] + publishFlags, + workingDirectory: packageDir); + publish.stdout + .transform(utf8.decoder) + .listen((String data) => _print(data)); + publish.stderr + .transform(utf8.decoder) + .listen((String data) => _print(data)); + _stdinSubscription ??= _stdin + .transform(utf8.decoder) + .listen((String data) => publish.stdin.writeln(data)); + final int result = await publish.exitCode; + if (result != 0) { + _print('Publish ${packageDir.basename} failed.'); + return false; + } } + return true; } String _getTag(Directory packageDir) { @@ -235,23 +444,44 @@ class PublishPluginCommand extends PluginCommand { .replaceAll('%VERSION%', version); } - Future _pushTagToRemote( - {@required String remote, - @required String tag, - @required String remoteUrl}) async { + // Pushes the `tag` to `remote` + // + // Return `true` if successful, `false` otherwise. + Future _pushTagToRemote({ + @required String remote, + @required String tag, + @required String remoteUrl, + }) async { assert(remote != null && tag != null && remoteUrl != null); _print('Ready to push $tag to $remoteUrl (y/n)?'); final String input = _stdin.readLineSync(); if (input.toLowerCase() != 'y') { _print('Tag push canceled.'); - throw ToolExit(1); + return false; } - await processRunner.run( - 'git', - ['push', remote, tag], - workingDir: packagesDir, - exitOnError: true, - logOnError: true, - ); + if (!(argResults[_dryRunFlag] as bool)) { + final io.ProcessResult result = await processRunner.run( + 'git', + ['push', remote, tag], + workingDir: packagesDir, + exitOnError: false, + logOnError: true, + ); + if (result.exitCode != 0) { + return false; + } + } + return true; } } + +enum _CheckNeedsReleaseResult { + // The package needs to be released. + release, + + // The package does not need to be released. + noRelease, + + // There's an error when trying to determine whether the package needs to be released. + failure, +} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 0cf709adc0d..9966e59867d 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -53,7 +53,8 @@ void main() { mockPackagesDir, mockPackagesDir.fileSystem, processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString()), - stdinput: mockStdin)); + stdinput: mockStdin, + gitDir: await GitDir.fromExisting(mockPackagesDir.path))); }); tearDown(() { @@ -95,9 +96,11 @@ void main() { throwsA(const TypeMatcher())); expect( - printedMessages.last, - contains( - "There are files in the package directory that haven't been saved in git.")); + printedMessages, + containsAllInOrder([ + 'There are files in the package directory that haven\'t been saved in git. Refusing to publish these files:\n\n?? foo/tmp\n\nIf the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.', + 'Failed, see above for details.', + ])); }); test('fails immediately if the remote doesn\'t exist', () async { @@ -110,7 +113,7 @@ void main() { test("doesn't validate the remote if it's not pushing tags", () async { // Immediately return 0 when running `pub publish`. - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', @@ -131,7 +134,7 @@ void main() { await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', '--package', @@ -152,9 +155,10 @@ void main() { '--no-push-tags', '--no-tag-release' ]); - processRunner.mockPublishProcess.stdoutController.add(utf8.encode('Foo')); - processRunner.mockPublishProcess.stderrController.add(utf8.encode('Bar')); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + + processRunner.mockPublishStdout = 'Foo'; + processRunner.mockPublishStderr = 'Bar'; + processRunner.mockPublishCompleteCode = 0; await publishCommand; @@ -170,8 +174,8 @@ void main() { '--no-push-tags', '--no-tag-release' ]); - mockStdin.controller.add(utf8.encode('user input')); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + mockStdin.mockUserInputs.add(utf8.encode('user input')); + processRunner.mockPublishCompleteCode = 0; await publishCommand; @@ -180,7 +184,7 @@ void main() { }); test('forwards --pub-publish-flags to pub publish', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', '--package', @@ -199,7 +203,7 @@ void main() { }); test('throws if pub publish fails', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + processRunner.mockPublishCompleteCode = 128; await expectLater( () => commandRunner.run([ 'publish-plugin', @@ -210,13 +214,35 @@ void main() { ]), throwsA(const TypeMatcher())); - expect(printedMessages, contains('Publish failed. Exiting.')); + expect(printedMessages, contains('Publish foo failed.')); + }); + + test('publish, dry run', () async { + // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. + processRunner.mockPublishCompleteCode = 1; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--dry-run', + '--no-push-tags', + '--no-tag-release', + ]); + + expect(processRunner.pushTagsArgs, isEmpty); + expect( + printedMessages, + containsAllInOrder([ + '=============== DRY RUN ===============', + 'Running `pub publish ` in ${pluginDir.path}...\n', + 'Done!' + ])); }); }); group('Tags release', () { test('with the version and name from the pubspec.yaml', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', '--package', @@ -231,7 +257,7 @@ void main() { }); test('only if publishing succeeded', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(128); + processRunner.mockPublishCompleteCode = 128; await expectLater( () => commandRunner.run([ 'publish-plugin', @@ -241,7 +267,7 @@ void main() { ]), throwsA(const TypeMatcher())); - expect(printedMessages, contains('Publish failed. Exiting.')); + expect(printedMessages, contains('Publish foo failed.')); final String tag = (await gitDir.runCommand( ['show-ref', 'fake_package-v0.0.1'], throwOnError: false)) @@ -257,7 +283,7 @@ void main() { }); test('requires user confirmation', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'help'; await expectLater( () => commandRunner.run([ @@ -272,7 +298,7 @@ void main() { test('to upstream by default', () async { await gitDir.runCommand(['tag', 'garbage']); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner.run([ 'publish-plugin', @@ -286,10 +312,30 @@ void main() { expect(printedMessages.last, 'Done!'); }); + test('to upstream by default, dry run', () async { + await gitDir.runCommand(['tag', 'garbage']); + // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. + processRunner.mockPublishCompleteCode = 1; + mockStdin.readLineOutput = 'y'; + await commandRunner.run( + ['publish-plugin', '--package', testPluginName, '--dry-run']); + + expect(processRunner.pushTagsArgs, isEmpty); + expect( + printedMessages, + containsAllInOrder([ + '=============== DRY RUN ===============', + 'Running `pub publish ` in ${pluginDir.path}...\n', + 'Tagging release fake_package-v0.0.1...', + 'Pushing tag to upstream...', + 'Done!' + ])); + }); + test('to different remotes based on a flag', () async { await gitDir.runCommand( ['remote', 'add', 'origin', 'http://localhost:8001']); - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; await commandRunner.run([ 'publish-plugin', @@ -306,7 +352,7 @@ void main() { }); test('only if tagging and pushing to remotes are both enabled', () async { - processRunner.mockPublishProcess.exitCodeCompleter.complete(0); + processRunner.mockPublishCompleteCode = 0; await commandRunner.run([ 'publish-plugin', '--package', @@ -318,15 +364,495 @@ void main() { expect(printedMessages.last, 'Done!'); }); }); + + group('Auto release (all-changed flag)', () { + setUp(() async { + io.Process.runSync('git', ['init'], + workingDirectory: mockPackagesDir.path); + gitDir = await GitDir.fromExisting(mockPackagesDir.path); + await gitDir.runCommand( + ['remote', 'add', 'upstream', 'http://localhost:8000']); + }); + + test('can release newly created plugins', () async { + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', + withSingleExample: true, packagesDirectory: mockPackagesDir); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', + withSingleExample: true, + parentDirectoryName: 'plugin2', + packagesDirectory: mockPackagesDir); + createFakePubspec(pluginDir1, + name: 'plugin1', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + }); + + test('can release newly created plugins, while there are existing plugins', + () async { + // Prepare an exiting plugin and tag it + final Directory pluginDir0 = createFakePlugin('plugin0', + withSingleExample: true, packagesDirectory: mockPackagesDir); + createFakePubspec(pluginDir0, + name: 'plugin0', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + processRunner.pushTagsArgs.clear(); + + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', + withSingleExample: true, packagesDirectory: mockPackagesDir); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', + withSingleExample: true, + parentDirectoryName: 'plugin2', + packagesDirectory: mockPackagesDir); + createFakePubspec(pluginDir1, + name: 'plugin1', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + }); + + test('can release newly created plugins, dry run', () async { + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', + withSingleExample: true, packagesDirectory: mockPackagesDir); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', + withSingleExample: true, + parentDirectoryName: 'plugin2', + packagesDirectory: mockPackagesDir); + createFakePubspec(pluginDir1, + name: 'plugin1', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. + processRunner.mockPublishCompleteCode = 1; + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--all-changed', + '--base-sha=HEAD~', + '--dry-run' + ]); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + '=============== DRY RUN ===============', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Tagging release plugin1-v0.0.1...', + 'Pushing tag to upstream...', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Tagging release plugin2-v0.0.1...', + 'Pushing tag to upstream...', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isEmpty); + }); + + test('version change triggers releases.', () async { + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', + withSingleExample: true, packagesDirectory: mockPackagesDir); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', + withSingleExample: true, + parentDirectoryName: 'plugin2', + packagesDirectory: mockPackagesDir); + createFakePubspec(pluginDir1, + name: 'plugin1', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + + processRunner.pushTagsArgs.clear(); + printedMessages.clear(); + + final List plugin1Pubspec = + pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); + plugin1Pubspec[plugin1Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.2'; + pluginDir1 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin1Pubspec.join('\n')); + final List plugin2Pubspec = + pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); + plugin2Pubspec[plugin2Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.2'; + pluginDir2 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin2Pubspec.join('\n')); + await gitDir.runCommand(['add', '-A']); + await gitDir + .runCommand(['commit', '-m', 'Update versions to 0.0.2']); + + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); + }); + + test( + 'delete package will not trigger publish but exit the command successfully.', + () async { + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', + withSingleExample: true, packagesDirectory: mockPackagesDir); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', + withSingleExample: true, + parentDirectoryName: 'plugin2', + packagesDirectory: mockPackagesDir); + createFakePubspec(pluginDir1, + name: 'plugin1', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + + processRunner.pushTagsArgs.clear(); + printedMessages.clear(); + + final List plugin1Pubspec = + pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); + plugin1Pubspec[plugin1Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.2'; + pluginDir1 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin1Pubspec.join('\n')); + + pluginDir2.deleteSync(recursive: true); + + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand([ + 'commit', + '-m', + 'Update plugin1 versions to 0.0.2, delete plugin2' + ]); + + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'The file at The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n', + 'Packages released: plugin1', + 'Done!' + ])); + + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs.length, 3); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); + }); + + test( + 'versions revert do not trigger releases. Also prints out warning message.', + () async { + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', + withSingleExample: true, packagesDirectory: mockPackagesDir); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', + withSingleExample: true, + parentDirectoryName: 'plugin2', + packagesDirectory: mockPackagesDir); + createFakePubspec(pluginDir1, + name: 'plugin1', + includeVersion: true, + isFlutter: false, + version: '0.0.2'); + createFakePubspec(pluginDir2, + name: 'plugin2', + includeVersion: true, + isFlutter: false, + version: '0.0.2'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); + + processRunner.pushTagsArgs.clear(); + printedMessages.clear(); + + final List plugin1Pubspec = + pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); + plugin1Pubspec[plugin1Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.1'; + pluginDir1 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin1Pubspec.join('\n')); + final List plugin2Pubspec = + pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); + plugin2Pubspec[plugin2Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.1'; + pluginDir2 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin2Pubspec.join('\n')); + await gitDir.runCommand(['add', '-A']); + await gitDir + .runCommand(['commit', '-m', 'Update versions to 0.0.1']); + + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'The new version (0.0.1) is lower than the current version (0.0.2) for plugin1.\nThis git commit is a revert, no release is tagged.', + 'The new version (0.0.1) is lower than the current version (0.0.2) for plugin2.\nThis git commit is a revert, no release is tagged.', + 'Done!' + ])); + + expect(processRunner.pushTagsArgs, isEmpty); + }); + + test('No version change does not release any plugins', () async { + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', + withSingleExample: true, packagesDirectory: mockPackagesDir); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', + withSingleExample: true, + parentDirectoryName: 'plugin2', + packagesDirectory: mockPackagesDir); + createFakePubspec(pluginDir1, + name: 'plugin1', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', + includeVersion: true, + isFlutter: false, + version: '0.0.1'); + + io.Process.runSync('git', ['init'], + workingDirectory: mockPackagesDir.path); + gitDir = await GitDir.fromExisting(mockPackagesDir.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + + pluginDir1.childFile('plugin1.dart').createSync(); + pluginDir2.childFile('plugin2.dart').createSync(); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add dart files']); + + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'No version updates in this commit.', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isEmpty); + }); + }); } class TestProcessRunner extends ProcessRunner { final List results = []; - final MockProcess mockPublishProcess = MockProcess(); + // Most recent returned publish process. + MockProcess mockPublishProcess; final List mockPublishArgs = []; final MockProcessResult mockPushTagsResult = MockProcessResult(); final List pushTagsArgs = []; + String mockPublishStdout; + String mockPublishStderr; + int mockPublishCompleteCode; + @override Future run( String executable, @@ -362,23 +888,42 @@ class TestProcessRunner extends ProcessRunner { args[0] == 'pub' && args[1] == 'publish'); mockPublishArgs.addAll(args); + mockPublishProcess = MockProcess(); + if (mockPublishStdout != null) { + mockPublishProcess.stdoutController.add(utf8.encode(mockPublishStdout)); + } + if (mockPublishStderr != null) { + mockPublishProcess.stderrController.add(utf8.encode(mockPublishStderr)); + } + if (mockPublishCompleteCode != null) { + mockPublishProcess.exitCodeCompleter.complete(mockPublishCompleteCode); + } + return mockPublishProcess; } } class MockStdin extends Mock implements io.Stdin { - final StreamController> controller = StreamController>(); + List> mockUserInputs = >[]; + StreamController> _controller; String readLineOutput; @override Stream transform(StreamTransformer, S> streamTransformer) { - return controller.stream.transform(streamTransformer); + // In the test context, only one `PublishPluginCommand` object is created for a single test case. + // However, sometimes, we need to run multiple commands in a single test case. + // In such situation, this `MockStdin`'s StreamController might be listened to more than once, which is not allowed. + // + // Create a new controller every time so this Stdin could be listened to multiple times. + _controller = StreamController>(); + mockUserInputs.forEach(_addUserInputsToSteam); + return _controller.stream.transform(streamTransformer); } @override StreamSubscription> listen(void onData(List event), {Function onError, void onDone(), bool cancelOnError}) { - return controller.stream.listen(onData, + return _controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @@ -387,6 +932,15 @@ class MockStdin extends Mock implements io.Stdin { {Encoding encoding = io.systemEncoding, bool retainNewlines = false}) => readLineOutput; + + void _addUserInputsToSteam(List input) => _controller.add(input); } -class MockProcessResult extends Mock implements io.ProcessResult {} +class MockProcessResult extends Mock implements io.ProcessResult { + MockProcessResult({int exitCode = 0}) : _exitCode = exitCode; + + final int _exitCode; + + @override + int get exitCode => _exitCode; +} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 4dc019c968d..96e00d3bb5e 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -49,6 +49,7 @@ Directory createFakePlugin( bool isWindowsPlugin = false, bool includeChangeLog = false, bool includeVersion = false, + String version = '0.0.1', String parentDirectoryName = '', Directory packagesDirectory, }) { @@ -73,6 +74,7 @@ Directory createFakePlugin( isMacOsPlugin: isMacOsPlugin, isWindowsPlugin: isWindowsPlugin, includeVersion: includeVersion, + version: version ); if (includeChangeLog) { createFakeCHANGELOG(pluginDirectory, ''' @@ -85,14 +87,14 @@ Directory createFakePlugin( final Directory exampleDir = pluginDirectory.childDirectory('example') ..createSync(); createFakePubspec(exampleDir, - name: '${name}_example', isFlutter: isFlutter); + name: '${name}_example', isFlutter: isFlutter, includeVersion: false, publishTo: 'none'); } else if (withExamples.isNotEmpty) { final Directory exampleDir = pluginDirectory.childDirectory('example') ..createSync(); for (final String example in withExamples) { final Directory currentExample = exampleDir.childDirectory(example) ..createSync(); - createFakePubspec(currentExample, name: example, isFlutter: isFlutter); + createFakePubspec(currentExample, name: example, isFlutter: isFlutter, includeVersion: false, publishTo: 'none'); } } @@ -123,6 +125,7 @@ void createFakePubspec( bool isLinuxPlugin = false, bool isMacOsPlugin = false, bool isWindowsPlugin = false, + String publishTo = 'http://no_pub_server.com', String version = '0.0.1', }) { parent.childFile('pubspec.yaml').createSync(); @@ -180,7 +183,11 @@ dependencies: if (includeVersion) { yaml += ''' version: $version -publish_to: http://no_pub_server.com # Hardcoded safeguard to prevent this from somehow being published by a broken test. +'''; + } + if (publishTo.isNotEmpty) { + yaml += ''' +publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being published by a broken test. '''; } parent.childFile('pubspec.yaml').writeAsStringSync(yaml); From a0ca28bac616a2468156b0c03a68f6542cde9f15 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 4 May 2021 13:53:44 -0400 Subject: [PATCH 035/249] Update third_party license checking (#3844) In preparation for enabling license checks in flutter/packages, update the allowed licenses: - Allow our license, for cases where we've locally added files - Allow the license used by the bsdiff package --- script/tool/CHANGELOG.md | 4 ++++ .../tool/lib/src/license_check_command.dart | 21 ++++++++++++------- script/tool/pubspec.yaml | 2 +- .../tool/test/license_check_command_test.dart | 18 ++++++++++++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 0411a9e53ab..3a9736c35b4 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.1 + +- Update the allowed third-party licenses for flutter/packages. + ## 0.1.0+1 - Re-add the bin/ directory. diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index adb2f931529..a6528640b82 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -52,10 +52,14 @@ const Set _ignoredFullBasenameList = { final List _thirdPartyLicenseBlockRegexes = [ // Third-party code used in url_launcher_web. RegExp( - r'^// Copyright 2017 Workiva Inc..*' - '^// Licensed under the Apache License, Version 2.0', + r'^// Copyright 2017 Workiva Inc\..*' + r'^// Licensed under the Apache License, Version 2\.0', multiLine: true, dotAll: true), + // bsdiff in flutter/packages. + RegExp(r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' + r'// Use of this source code is governed by a BSD-style license that can be\n' + r'// found in the LICENSE file\.\n'), ]; // The exact format of the BSD license that our license files should contain. @@ -158,16 +162,19 @@ class LicenseCheckCommand extends PluginCommand { _print('Checking ${file.path}'); final String content = await file.readAsString(); + final String firstParyLicense = + firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? + defaultFirstParyLicenseBlock; if (_isThirdParty(file)) { + // Third-party directories allow either known third-party licenses, our + // the first-party license, as there may be local additions. if (!_thirdPartyLicenseBlockRegexes - .any((RegExp regex) => regex.hasMatch(content))) { + .any((RegExp regex) => regex.hasMatch(content)) && + !content.contains(firstParyLicense)) { unrecognizedThirdPartyFiles.add(file); } } else { - final String license = - firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? - defaultFirstParyLicenseBlock; - if (!content.contains(license)) { + if (!content.contains(firstParyLicense)) { incorrectFirstPartyFiles.add(file); } } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 18631fef02d..58dfc9fbd8b 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.1.0+1 +version: 0.1.1 dependencies: args: "^1.4.3" diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 94606f54b76..23de2581b9b 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -269,6 +269,24 @@ void main() { expect(printedMessages, contains('All source files passed validation!')); }); + test('allows first-party code in a third_party directory', () async { + final File firstPartyFileInThirdParty = root + .childDirectory('a_plugin') + .childDirectory('lib') + .childDirectory('src') + .childDirectory('third_party') + .childFile('first_party.cc'); + firstPartyFileInThirdParty.createSync(recursive: true); + _writeLicense(firstPartyFileInThirdParty); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check the file. + expect(printedMessages, + contains('Checking a_plugin/lib/src/third_party/first_party.cc')); + expect(printedMessages, contains('All source files passed validation!')); + }); + test('fails for licenses that the tool does not expect', () async { final File good = root.childFile('good.cc'); good.createSync(); From 53c8d479d43336ab7ec84532e53882b51b0cc059 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 7 May 2021 12:40:28 -0400 Subject: [PATCH 036/249] Enable analysis for the tool directory (#3853) Since the tooling doesn't live in packages/, we're not currently analyzing it. This enables analysis so that we won't have analyzer issues creeping in over time. To minimize complexity, this just adds it directly to the Cirrus configuration rather than building knowledge of the tool directory into the tool itself. --- script/tool/lib/src/drive_examples_command.dart | 6 +++--- script/tool/lib/src/version_check_command.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index e52052a49ae..0230ebd671e 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -101,13 +101,13 @@ class DriveExamplesCommand extends PluginCommand { fileSystem.directory(p.join(example.path, 'integration_test')); if (await integrationTests.exists()) { - await for (final FileSystemEntity integration_test + await for (final FileSystemEntity integrationTest in integrationTests.list()) { - if (!integration_test.basename.endsWith('_test.dart')) { + if (!integrationTest.basename.endsWith('_test.dart')) { continue; } targetPaths - .add(p.relative(integration_test.path, from: example.path)); + .add(p.relative(integrationTest.path, from: example.path)); } } diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 0b552e8bff4..39fecc03924 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -187,7 +187,7 @@ class VersionCheckCommand extends PluginCommand { // Skip validation for the special NEXT version that's used to accumulate // changes that don't warrant publishing on their own. - bool hasNextSection = versionString == 'NEXT'; + final bool hasNextSection = versionString == 'NEXT'; if (hasNextSection) { print('Found NEXT; validating next version in the CHANGELOG.'); // Ensure that the version in pubspec hasn't changed without updating From 8bec6598a505fdaa618a6654eaf8b7c8b4b8e8d0 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 7 May 2021 12:41:21 -0400 Subject: [PATCH 037/249] Add support for third_party packages in Dart code (#3847) flutter/packages has two packages in third_party/packages/, which wasn't something our tooling recognized, so no package-based CI checks are running on them. This adds knowledge that there can be a third_party/packages/ directory as a sibling of the primary packages directory. Also migrates common_tests off of the shared (now deprecated) mock filesystem and onto a test-local mock filesystem. Fixes flutter/flutter#81570 --- script/tool/lib/src/common.dart | 73 +++++++++++-------- script/tool/test/common_test.dart | 114 +++++++++++++++++++----------- script/tool/test/util.dart | 41 ++++++----- 3 files changed, 140 insertions(+), 88 deletions(-) diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index c16ba8e957e..551c399ebc8 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -300,8 +300,8 @@ abstract class PluginCommand extends Command { /// 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 one of two places relative to the packages - /// directory. + /// 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 @@ -311,6 +311,9 @@ abstract class PluginCommand extends Command { /// 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(argResults[_pluginsArg] as List); @@ -322,33 +325,42 @@ abstract class PluginCommand extends Command { plugins = await _getChangedPackages(); } - await for (final FileSystemEntity entity - in packagesDir.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: packagesDir.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; + 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; + } } } } @@ -449,8 +461,9 @@ abstract class PluginCommand extends Command { if (packages.isNotEmpty) { final String changedPackages = packages.join(','); print(changedPackages); + } else { + print('No changed packages.'); } - print('No changed packages.'); return packages; } } diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index 57bc1e20f91..3ae46ffc15d 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -6,6 +6,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:git/git.dart'; import 'package:mockito/mockito.dart'; @@ -16,11 +17,20 @@ import 'util.dart'; void main() { RecordingProcessRunner processRunner; CommandRunner runner; + FileSystem fileSystem; + Directory packagesDir; + Directory thirdPartyPackagesDir; List plugins; List> gitDirCommands; String gitDiffResponse; setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + thirdPartyPackagesDir = packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages'); + gitDirCommands = >[]; gitDiffResponse = ''; final MockGitDir gitDir = MockGitDir(); @@ -33,13 +43,13 @@ void main() { } return Future.value(mockProcessResult); }); - initializeFakePackages(); + initializeFakePackages(parentDir: packagesDir.parent); processRunner = RecordingProcessRunner(); plugins = []; final SamplePluginCommand samplePluginCommand = SamplePluginCommand( plugins, - mockPackagesDir, - mockFileSystem, + packagesDir, + fileSystem, processRunner: processRunner, gitDir: gitDir, ); @@ -48,35 +58,48 @@ void main() { runner.addCommand(samplePluginCommand); }); - tearDown(() { - mockPackagesDir.deleteSync(recursive: true); - }); - test('all plugins from file system', () async { - final Directory plugin1 = createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run(['sample']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); + test('all plugins includes third_party/packages', () async { + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin3 = + createFakePlugin('plugin3', packagesDirectory: thirdPartyPackagesDir); + await runner.run(['sample']); + expect(plugins, + unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); + }); + test('exclude plugins when plugins flag is specified', () async { - createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); expect(plugins, unorderedEquals([plugin2.path])); }); test('exclude plugins when plugins flag isn\'t specified', () async { - createFakePlugin('plugin1'); - createFakePlugin('plugin2'); + createFakePlugin('plugin1', packagesDirectory: packagesDir); + createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run(['sample', '--exclude=plugin1,plugin2']); expect(plugins, unorderedEquals([])); }); test('exclude federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2'); + createFakePlugin('plugin1', + parentDirectoryName: 'federated', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run([ 'sample', '--plugins=federated/plugin1,plugin2', @@ -87,8 +110,10 @@ void main() { test('exclude entire federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2'); + createFakePlugin('plugin1', + parentDirectoryName: 'federated', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run([ 'sample', '--plugins=federated/plugin1,plugin2', @@ -99,8 +124,10 @@ void main() { group('test run-on-changed-packages', () { test('all plugins should be tested if there are no changes.', () async { - final Directory plugin1 = createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -110,8 +137,10 @@ void main() { test('all plugins should be tested if there are no plugin related changes.', () async { gitDiffResponse = '.cirrus'; - final Directory plugin1 = createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -120,8 +149,9 @@ void main() { test('Only changed plugin should be tested.', () async { gitDiffResponse = 'packages/plugin1/plugin1.dart'; - final Directory plugin1 = createFakePlugin('plugin1'); - createFakePlugin('plugin2'); + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -133,8 +163,9 @@ void main() { packages/plugin1/plugin1.dart packages/plugin1/ios/plugin1.m '''; - final Directory plugin1 = createFakePlugin('plugin1'); - createFakePlugin('plugin2'); + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + createFakePlugin('plugin2', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -147,9 +178,11 @@ packages/plugin1/ios/plugin1.m packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m '''; - final Directory plugin1 = createFakePlugin('plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); - createFakePlugin('plugin3'); + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); + createFakePlugin('plugin3', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -164,10 +197,10 @@ packages/plugin1/plugin1/plugin1.dart packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart packages/plugin1/plugin1_web/plugin1_web.dart '''; - final Directory plugin1 = - createFakePlugin('plugin1', parentDirectoryName: 'plugin1'); - createFakePlugin('plugin2'); - createFakePlugin('plugin3'); + final Directory plugin1 = createFakePlugin('plugin1', + parentDirectoryName: 'plugin1', packagesDirectory: packagesDir); + createFakePlugin('plugin2', packagesDirectory: packagesDir); + createFakePlugin('plugin3', packagesDirectory: packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -181,10 +214,11 @@ packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; - final Directory plugin1 = - createFakePlugin('plugin1', parentDirectoryName: 'plugin1'); - final Directory plugin2 = createFakePlugin('plugin2'); - createFakePlugin('plugin3'); + final Directory plugin1 = createFakePlugin('plugin1', + parentDirectoryName: 'plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); + createFakePlugin('plugin3', packagesDirectory: packagesDir); await runner.run([ 'sample', '--plugins=plugin1,plugin2', @@ -201,10 +235,10 @@ packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; - final Directory plugin1 = - createFakePlugin('plugin1', parentDirectoryName: 'plugin1'); - createFakePlugin('plugin2'); - createFakePlugin('plugin3'); + final Directory plugin1 = createFakePlugin('plugin1', + parentDirectoryName: 'plugin1', packagesDirectory: packagesDir); + createFakePlugin('plugin2', packagesDirectory: packagesDir); + createFakePlugin('plugin3', packagesDirectory: packagesDir); await runner.run([ 'sample', '--exclude=plugin2,plugin3', diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 96e00d3bb5e..e926d60968b 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -63,19 +63,17 @@ Directory createFakePlugin( final Directory pluginDirectory = parentDirectory.childDirectory(name); pluginDirectory.createSync(recursive: true); - createFakePubspec( - pluginDirectory, - name: name, - isFlutter: isFlutter, - isAndroidPlugin: isAndroidPlugin, - isIosPlugin: isIosPlugin, - isWebPlugin: isWebPlugin, - isLinuxPlugin: isLinuxPlugin, - isMacOsPlugin: isMacOsPlugin, - isWindowsPlugin: isWindowsPlugin, - includeVersion: includeVersion, - version: version - ); + createFakePubspec(pluginDirectory, + name: name, + isFlutter: isFlutter, + isAndroidPlugin: isAndroidPlugin, + isIosPlugin: isIosPlugin, + isWebPlugin: isWebPlugin, + isLinuxPlugin: isLinuxPlugin, + isMacOsPlugin: isMacOsPlugin, + isWindowsPlugin: isWindowsPlugin, + includeVersion: includeVersion, + version: version); if (includeChangeLog) { createFakeCHANGELOG(pluginDirectory, ''' ## 0.0.1 @@ -87,21 +85,28 @@ Directory createFakePlugin( final Directory exampleDir = pluginDirectory.childDirectory('example') ..createSync(); createFakePubspec(exampleDir, - name: '${name}_example', isFlutter: isFlutter, includeVersion: false, publishTo: 'none'); + name: '${name}_example', + isFlutter: isFlutter, + includeVersion: false, + publishTo: 'none'); } else if (withExamples.isNotEmpty) { final Directory exampleDir = pluginDirectory.childDirectory('example') ..createSync(); for (final String example in withExamples) { final Directory currentExample = exampleDir.childDirectory(example) ..createSync(); - createFakePubspec(currentExample, name: example, isFlutter: isFlutter, includeVersion: false, publishTo: 'none'); + createFakePubspec(currentExample, + name: example, + isFlutter: isFlutter, + includeVersion: false, + publishTo: 'none'); } } + final FileSystem fileSystem = pluginDirectory.fileSystem; for (final List file in withExtraFiles) { final List newFilePath = [pluginDirectory.path, ...file]; - final File newFile = - mockFileSystem.file(mockFileSystem.path.joinAll(newFilePath)); + final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath)); newFile.createSync(recursive: true); } @@ -186,7 +191,7 @@ version: $version '''; } if (publishTo.isNotEmpty) { - yaml += ''' + yaml += ''' publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being published by a broken test. '''; } From 755de9be8a27456b433cddbc48e2cfbbcdd17803 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 7 May 2021 12:42:20 -0400 Subject: [PATCH 038/249] Move incremental_build.sh to run-on-changed-packages (#3846) Switch incremental_build.sh from using the older check_changed_packages implemented in bash to the newer (tested/testable) Dart implementation via --run-onchanged-packages. Also clarifies in help that the flag runs on all packages when nothing has changed. --- script/tool/lib/src/common.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index 551c399ebc8..a63d606b35c 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -203,6 +203,7 @@ abstract class PluginCommand extends Command { argParser.addFlag(_runOnChangedPackagesArg, help: 'Run the command on changed packages/plugins.\n' 'If the $_pluginsArg is specified, this flag is ignored.\n' + 'If no plugins have changed, the command runs on all plugins.\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, From 41df811fbdee2762b4cd5d616b111375ec03aa58 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 7 May 2021 14:10:03 -0400 Subject: [PATCH 039/249] Fix analyzer issues (#3863) * Make infos fatal when analyzing packages * Fix import ordering for updated analysis --- script/tool/lib/src/analyze_command.dart | 3 ++- script/tool/lib/src/version_check_command.dart | 2 +- script/tool/test/analyze_command_test.dart | 9 ++++++--- script/tool/test/publish_plugin_command_test.dart | 2 +- script/tool/test/util.dart | 2 +- script/tool/test/version_check_test.dart | 4 ++-- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index e17400fa887..61be5f98a62 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -70,7 +70,8 @@ class AnalyzeCommand extends PluginCommand { final List failingPackages = []; await for (final Directory package in getPlugins()) { final int exitCode = await processRunner.runAndStream( - 'dart', ['analyze'], workingDir: package); + 'dart', ['analyze', '--fatal-infos'], + workingDir: package); if (exitCode != 0) { failingPackages.add(p.basename(package.path)); } diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 39fecc03924..db69f4ead86 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -4,9 +4,9 @@ import 'dart:async'; -import 'package:meta/meta.dart'; import 'package:file/file.dart'; import 'package:git/git.dart'; +import 'package:meta/meta.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index ac6b8eea676..11acb59a474 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -46,8 +46,10 @@ void main() { 'flutter', const ['packages', 'get'], plugin1Dir.path), ProcessCall( 'flutter', const ['packages', 'get'], plugin2Dir.path), - ProcessCall('dart', const ['analyze'], plugin1Dir.path), - ProcessCall('dart', const ['analyze'], plugin2Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin1Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin2Dir.path), ])); }); @@ -86,7 +88,8 @@ void main() { orderedEquals([ ProcessCall( 'flutter', const ['packages', 'get'], pluginDir.path), - ProcessCall('dart', const ['analyze'], pluginDir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + pluginDir.path), ])); }); diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 9966e59867d..02d6d8140a6 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -9,8 +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/publish_plugin_command.dart'; import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; import 'package:git/git.dart'; import 'package:matcher/matcher.dart'; import 'package:mockito/mockito.dart'; diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index e926d60968b..25b248b00af 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -9,9 +9,9 @@ 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:meta/meta.dart'; import 'package:platform/platform.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; import 'package:quiver/collection.dart'; // TODO(stuartmorgan): Eliminate this in favor of setting up a clean filesystem diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index 04348310a2f..ea1a82ae744 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -8,11 +8,11 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/version_check_command.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; -import 'package:flutter_plugin_tools/src/version_check_command.dart'; import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; import 'util.dart'; void testAllowedVersion( From ce899a3151d112e7a8e497fac3b34de39d58ef49 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 10 May 2021 14:14:45 -0400 Subject: [PATCH 040/249] Ensure that integration tests are actually being run (#3857) Many of our integration tests weren't actually being run on CI because they were in the wrong place and/or missing the driver file, and it seems we've just never noticed. This makes a number of changes: - Ensures that all packages with integration tests also have a driver file in the right location. - Also standardizes the format of those files, as the boilerplate `main()` is available in `integration_test`. - Ensures that all integration_test directories are in the right place. - In a couple of places, removes a duplicate copy of the integration test file. - Makes it an error for a plugin that's not excluded to not have integration tests, so this can't easily happen again. - Adds logging of what's being run and skipped, so if something does go wrong in the future it's easy to determine what from the logs. - Excludes `*_platform_interface` since the logging was (potentially confusingly) reporting that they were skipped because they don't support the current platform. Skipping them is correct, just not for that reason. - Excludes the plugins that currently have no integration tests, with references to issues about adding them. Fixes https://github.com/flutter/flutter/issues/81929 --- .../tool/lib/src/drive_examples_command.dart | 37 ++++++++++-- .../test/drive_examples_command_test.dart | 57 +++++++++++++++++++ 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 0230ebd671e..1572b941078 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -52,23 +52,37 @@ class DriveExamplesCommand extends PluginCommand { @override Future run() async { final List failingTests = []; + final List pluginsWithoutTests = []; final bool isLinux = argResults[kLinux] == true; final bool isMacos = argResults[kMacos] == true; final bool isWeb = argResults[kWeb] == true; final bool isWindows = argResults[kWindows] == true; await for (final Directory plugin in getPlugins()) { + final String pluginName = plugin.basename; + if (pluginName.endsWith('_platform_interface') && + !plugin.childDirectory('example').existsSync()) { + // Platform interface packages generally aren't intended to have + // examples, and don't need integration tests, so silently skip them + // unless for some reason there is an example directory. + continue; + } + print('\n==========\nChecking $pluginName...'); + if (!(await _pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { + print('Not supported for the target platform; skipping.'); + continue; + } + int examplesFound = 0; + bool testsRan = false; final String flutterCommand = const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; for (final Directory example in getExamplesForPlugin(plugin)) { + ++examplesFound; final String packageName = p.relative(example.path, from: packagesDir.path); - if (!(await _pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { - continue; - } final Directory driverTests = fileSystem.directory(p.join(example.path, 'test_driver')); if (!driverTests.existsSync()) { - // No driver tests available for this example + print('No driver tests found for $packageName'); continue; } // Look for driver tests ending in _test.dart in test_driver/ @@ -160,6 +174,7 @@ Tried searching for the following: } for (final String targetPath in targetPaths) { + testsRan = true; final int exitCode = await processRunner.runAndStream( flutterCommand, [ @@ -177,6 +192,11 @@ Tried searching for the following: } } } + if (!testsRan) { + pluginsWithoutTests.add(pluginName); + print( + 'No driver tests run for $pluginName ($examplesFound examples found)'); + } } print('\n\n'); @@ -188,6 +208,15 @@ Tried searching for the following: throw ToolExit(1); } + if (pluginsWithoutTests.isNotEmpty) { + print('The following plugins did not run any integration tests:'); + for (final String plugin in pluginsWithoutTests) { + print(' * $plugin'); + } + print('If this is intentional, they must be explicitly excluded.'); + throw ToolExit(1); + } + print('All driver tests successful!'); } diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index c5960b2c342..5ba8d8af25f 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -55,6 +55,7 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', '\n\n', 'All driver tests successful!', ]), @@ -100,6 +101,7 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', '\n\n', 'All driver tests successful!', ]), @@ -143,6 +145,25 @@ void main() { throwsA(const TypeMatcher())); }); + test('a plugin without any integration test files is reported as an error', + () async { + createFakePlugin('plugin', + withExtraFiles: >[ + ['example', 'lib', 'main.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + mockPackagesDir.childDirectory('plugin').childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await expectLater( + () => runCapturingPrint(runner, ['drive-examples']), + throwsA(const TypeMatcher())); + }); + test( 'driving under folder "test_driver" when targets are under "integration_test"', () async { @@ -168,6 +189,7 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', '\n\n', 'All driver tests successful!', ]), @@ -223,6 +245,8 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', '\n\n', 'All driver tests successful!', ]), @@ -255,6 +279,7 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', '\n\n', 'All driver tests successful!', ]), @@ -300,6 +325,8 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', '\n\n', 'All driver tests successful!', ]), @@ -332,6 +359,7 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', '\n\n', 'All driver tests successful!', ]), @@ -379,6 +407,8 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', '\n\n', 'All driver tests successful!', ]), @@ -411,6 +441,7 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', '\n\n', 'All driver tests successful!', ]), @@ -460,6 +491,8 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', '\n\n', 'All driver tests successful!', ]), @@ -492,6 +525,7 @@ void main() { expect( output, orderedEquals([ + '\n==========\nChecking plugin...', '\n\n', 'All driver tests successful!', ]), @@ -535,6 +569,29 @@ void main() { 'drive-examples', ]); + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', + '\n\n', + 'All driver tests successful!', + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('platform interface plugins are silently skipped', () async { + createFakePlugin('aplugin_platform_interface'); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + expect( output, orderedEquals([ From 87cfd6ab03b2c9dd327596e3b438711c0b0ede56 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 11 May 2021 12:48:30 -0700 Subject: [PATCH 041/249] [tool] version-check publish-check commands can check against pub (#3840) Add a PubVersionFinder class to easily fetch the version from pub. Add an against-pub flag to check-version command, which allows it to check the version against pub server Make the 'publish-check' command to check against pub to determine if the specific versions of packages need to be published. Add a log-status flag, which allows the publish-check command to log the final status of the result. This helps other ci tools to easily grab the results and use it to determine what to do next. See option 3 in flutter/flutter#81444 This PR also fixes some tests. partially flutter/flutter#81444 --- script/tool/CHANGELOG.md | 5 + script/tool/lib/src/common.dart | 93 +++++ .../tool/lib/src/publish_check_command.dart | 189 ++++++++-- .../tool/lib/src/version_check_command.dart | 76 +++- script/tool/test/common_test.dart | 78 +++++ .../tool/test/publish_check_command_test.dart | 219 ++++++++++++ script/tool/test/util.dart | 14 +- script/tool/test/version_check_test.dart | 328 ++++++++++++++---- 8 files changed, 900 insertions(+), 102 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 3a9736c35b4..bc2a775db1f 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +- Add `against-pub` flag for version-check, which allows the command to check version with pub. +- Add `machine` flag for publish-check, which replaces outputs to something parsable by machines. + ## 0.1.1 - Update the allowed third-party licenses for flutter/packages. diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index a63d606b35c..1e864fcbc38 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -11,6 +11,7 @@ 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:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; @@ -563,6 +564,98 @@ class ProcessRunner { } } +/// 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 != null && 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: null, + result: PubVersionFinderResult.noPackageFound, + httpResponse: response); + } else if (response.statusCode != 200) { + return PubVersionFinderResponse( + versions: null, + 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({this.versions, this.result, this.httpResponse}) { + if (versions != null && 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 diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 0fb9dbad60a..84503f4540c 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -3,10 +3,14 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'common.dart'; @@ -18,7 +22,10 @@ class PublishCheckCommand extends PluginCommand { Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { + this.httpClient, + }) : _pubVersionFinder = + PubVersionFinder(httpClient: httpClient ?? http.Client()), + super(packagesDir, fileSystem, processRunner: processRunner) { argParser.addFlag( _allowPrereleaseFlag, help: 'Allows the pre-release SDK warning to pass.\n' @@ -26,9 +33,29 @@ class PublishCheckCommand extends PluginCommand { 'the SDK constraint is a pre-release version, is ignored.', defaultsTo: false, ); + argParser.addFlag(_machineFlag, + help: 'Switch outputs to a machine readable JSON. \n' + 'The JSON contains a "status" field indicating the final status of the command, the possible values are:\n' + ' $_statusNeedsPublish: There is at least one package need to be published. They also passed all publish checks.\n' + ' $_statusMessageNoPublish: There are no packages needs to be published. Either no pubspec change detected or all versions have already been published.\n' + ' $_statusMessageError: Some error has occurred.', + defaultsTo: false, + negatable: true); } static const String _allowPrereleaseFlag = 'allow-pre-release'; + static const String _machineFlag = 'machine'; + static const String _statusNeedsPublish = 'needs-publish'; + static const String _statusMessageNoPublish = 'no-publish'; + static const String _statusMessageError = 'error'; + static const String _statusKey = 'status'; + static const String _humanMessageKey = 'humanMessage'; + + final List _validStatus = [ + _statusNeedsPublish, + _statusMessageNoPublish, + _statusMessageError + ]; @override final String name = 'publish-check'; @@ -37,31 +64,74 @@ class PublishCheckCommand extends PluginCommand { final String description = 'Checks to make sure that a plugin *could* be published.'; + /// The custom http client used to query versions on pub. + final http.Client httpClient; + + final PubVersionFinder _pubVersionFinder; + + // The output JSON when the _machineFlag is on. + final Map _machineOutput = {}; + + final List _humanMessages = []; + @override Future run() async { + final ZoneSpecification logSwitchSpecification = ZoneSpecification( + print: (Zone self, ZoneDelegate parent, Zone zone, String message) { + final bool logMachineMessage = argResults[_machineFlag] as bool; + if (logMachineMessage && message != _prettyJson(_machineOutput)) { + _humanMessages.add(message); + } else { + parent.print(zone, message); + } + }); + + await runZoned(_runCommand, zoneSpecification: logSwitchSpecification); + } + + Future _runCommand() async { final List failedPackages = []; + String status = _statusMessageNoPublish; await for (final Directory plugin in getPlugins()) { - if (!(await _passesPublishCheck(plugin))) { - failedPackages.add(plugin); + final _PublishCheckResult result = await _passesPublishCheck(plugin); + switch (result) { + case _PublishCheckResult._notPublished: + if (failedPackages.isEmpty) { + status = _statusNeedsPublish; + } + break; + case _PublishCheckResult._published: + break; + case _PublishCheckResult._error: + failedPackages.add(plugin); + status = _statusMessageError; + break; } } + _pubVersionFinder.httpClient.close(); if (failedPackages.isNotEmpty) { final String error = - 'FAIL: The following ${failedPackages.length} package(s) failed the ' + 'The following ${failedPackages.length} package(s) failed the ' 'publishing check:'; final String joinedFailedPackages = failedPackages.join('\n'); + _printImportantStatusMessage('$error\n$joinedFailedPackages', + isError: true); + } else { + _printImportantStatusMessage('All packages passed publish check!', + isError: false); + } - final Colorize colorizedError = Colorize('$error\n$joinedFailedPackages') - ..red(); - print(colorizedError); - throw ToolExit(1); + if (argResults[_machineFlag] as bool) { + _setStatus(status); + _machineOutput[_humanMessageKey] = _humanMessages; + print(_prettyJson(_machineOutput)); } - final Colorize passedMessage = - Colorize('All packages passed publish check!')..green(); - print(passedMessage); + if (failedPackages.isNotEmpty) { + throw ToolExit(1); + } } Pubspec _tryParsePubspec(Directory package) { @@ -89,8 +159,11 @@ class PublishCheckCommand extends PluginCommand { final Completer stdOutCompleter = Completer(); process.stdout.listen( (List event) { - io.stdout.add(event); - outputBuffer.write(String.fromCharCodes(event)); + final String output = String.fromCharCodes(event); + if (output.isNotEmpty) { + print(output); + outputBuffer.write(output); + } }, onDone: () => stdOutCompleter.complete(), ); @@ -98,8 +171,11 @@ class PublishCheckCommand extends PluginCommand { final Completer stdInCompleter = Completer(); process.stderr.listen( (List event) { - io.stderr.add(event); - outputBuffer.write(String.fromCharCodes(event)); + final String output = String.fromCharCodes(event); + if (output.isNotEmpty) { + _printImportantStatusMessage(output, isError: true); + outputBuffer.write(output); + } }, onDone: () => stdInCompleter.complete(), ); @@ -121,24 +197,97 @@ class PublishCheckCommand extends PluginCommand { 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); } - Future _passesPublishCheck(Directory package) async { + Future<_PublishCheckResult> _passesPublishCheck(Directory package) async { final String packageName = package.basename; print('Checking that $packageName can be published.'); final Pubspec pubspec = _tryParsePubspec(package); if (pubspec == null) { - return false; + print('no pubspec'); + return _PublishCheckResult._error; } else if (pubspec.publishTo == 'none') { print('Package $packageName is marked as unpublishable. Skipping.'); - return true; + return _PublishCheckResult._published; + } + + final Version version = pubspec.version; + final _PublishCheckResult alreadyPublishedResult = + await _checkIfAlreadyPublished( + packageName: packageName, version: version); + if (alreadyPublishedResult == _PublishCheckResult._published) { + print( + 'Package $packageName version: $version has already be published on pub.'); + return alreadyPublishedResult; + } else if (alreadyPublishedResult == _PublishCheckResult._error) { + print('Check pub version failed $packageName'); + return _PublishCheckResult._error; } if (await _hasValidPublishCheckRun(package)) { print('Package $packageName is able to be published.'); - return true; + return _PublishCheckResult._notPublished; } else { print('Unable to publish $packageName'); - return false; + return _PublishCheckResult._error; + } + } + + // Check if `packageName` already has `version` published on pub. + Future<_PublishCheckResult> _checkIfAlreadyPublished( + {String packageName, Version version}) async { + final PubVersionFinderResponse pubVersionFinderResponse = + await _pubVersionFinder.getPackageVersion(package: packageName); + _PublishCheckResult result; + switch (pubVersionFinderResponse.result) { + case PubVersionFinderResult.success: + result = pubVersionFinderResponse.versions.contains(version) + ? _PublishCheckResult._published + : _PublishCheckResult._notPublished; + break; + case PubVersionFinderResult.fail: + print(''' +Error fetching version on pub for $packageName. +HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} +HTTP response: ${pubVersionFinderResponse.httpResponse.body} +'''); + result = _PublishCheckResult._error; + break; + case PubVersionFinderResult.noPackageFound: + result = _PublishCheckResult._notPublished; + break; } + return result; + } + + void _setStatus(String status) { + assert(_validStatus.contains(status)); + _machineOutput[_statusKey] = status; + } + + String _prettyJson(Map map) { + return const JsonEncoder.withIndent(' ').convert(_machineOutput); } + + void _printImportantStatusMessage(String message, {@required bool isError}) { + final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; + if (argResults[_machineFlag] as bool) { + print(statusMessage); + } else { + final Colorize colorizedMessage = Colorize(statusMessage); + if (isError) { + colorizedMessage.red(); + } else { + colorizedMessage.green(); + } + print(colorizedMessage); + } + } +} + +enum _PublishCheckResult { + _notPublished, + + _published, + + _error, } diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index db69f4ead86..475caf5d285 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:git/git.dart'; +import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; @@ -74,8 +75,21 @@ class VersionCheckCommand extends PluginCommand { FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), GitDir gitDir, - }) : super(packagesDir, fileSystem, - processRunner: processRunner, gitDir: gitDir); + this.httpClient, + }) : _pubVersionFinder = + PubVersionFinder(httpClient: httpClient ?? http.Client()), + super(packagesDir, fileSystem, + processRunner: processRunner, gitDir: gitDir) { + argParser.addFlag( + _againstPubFlag, + help: 'Whether the version check should run against the version on pub.\n' + 'Defaults to false, which means the version check only run against the previous version in code.', + defaultsTo: false, + negatable: true, + ); + } + + static const String _againstPubFlag = 'against-pub'; @override final String name = 'version-check'; @@ -86,6 +100,11 @@ class VersionCheckCommand extends PluginCommand { 'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n' 'This command requires "pub" and "flutter" to be in your path.'; + /// The http client used to query pub server. + final http.Client httpClient; + + final PubVersionFinder _pubVersionFinder; + @override Future run() async { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); @@ -115,29 +134,61 @@ class VersionCheckCommand extends PluginCommand { 'intentionally has no version should be marked ' '"publish_to: none".'); } - final Version masterVersion = - await gitVersionFinder.getPackageVersion(pubspecPath); - if (masterVersion == null) { - print('${indentation}Unable to find pubspec in master. ' - 'Safe to ignore if the project is new.'); + Version sourceVersion; + if (argResults[_againstPubFlag] as bool) { + final String packageName = pubspecFile.parent.basename; + final PubVersionFinderResponse pubVersionFinderResponse = + await _pubVersionFinder.getPackageVersion(package: packageName); + switch (pubVersionFinderResponse.result) { + case PubVersionFinderResult.success: + sourceVersion = pubVersionFinderResponse.versions.first; + print( + '$indentation$packageName: Current largest version on pub: $sourceVersion'); + break; + case PubVersionFinderResult.fail: + printErrorAndExit(errorMessage: ''' +${indentation}Error fetching version on pub for $packageName. +${indentation}HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} +${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} +'''); + break; + case PubVersionFinderResult.noPackageFound: + sourceVersion = null; + break; + } + } else { + sourceVersion = await gitVersionFinder.getPackageVersion(pubspecPath); + } + if (sourceVersion == null) { + String safeToIgnoreMessage; + if (argResults[_againstPubFlag] as bool) { + safeToIgnoreMessage = + '${indentation}Unable to find package on pub server.'; + } else { + safeToIgnoreMessage = + '${indentation}Unable to find pubspec in master.'; + } + print('$safeToIgnoreMessage Safe to ignore if the project is new.'); continue; } - if (masterVersion == headVersion) { + if (sourceVersion == headVersion) { print('${indentation}No version change.'); continue; } final Map allowedNextVersions = - getAllowedNextVersions(masterVersion, headVersion); + getAllowedNextVersions(sourceVersion, headVersion); if (!allowedNextVersions.containsKey(headVersion)) { + final String source = + (argResults[_againstPubFlag] as bool) ? 'pub' : 'master'; final String error = '${indentation}Incorrectly updated version.\n' - '${indentation}HEAD: $headVersion, master: $masterVersion.\n' + '${indentation}HEAD: $headVersion, $source: $sourceVersion.\n' '${indentation}Allowed versions: $allowedNextVersions'; printErrorAndExit(errorMessage: error); } else { - print('$indentation$headVersion -> $masterVersion'); + print('$indentation$headVersion -> $sourceVersion'); } final bool isPlatformInterface = @@ -153,6 +204,7 @@ class VersionCheckCommand extends PluginCommand { await for (final Directory plugin in getPlugins()) { await _checkVersionsMatch(plugin); } + _pubVersionFinder.httpClient.close(); print('No version check errors found!'); } @@ -224,7 +276,7 @@ The first version listed in CHANGELOG.md is $fromChangeLog. printErrorAndExit(errorMessage: ''' When bumping the version for release, the NEXT section should be incorporated into the new version's release notes. - '''); +'''); } } diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index 3ae46ffc15d..d6ac449e7fd 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -2,6 +2,7 @@ // 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:args/command_runner.dart'; @@ -9,7 +10,10 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:git/git.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'; import 'util.dart'; @@ -334,6 +338,80 @@ file2/file2.cc .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, isNull); + 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, isNull); + 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 SamplePluginCommand extends PluginCommand { diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 0a9d36f2ea6..eca7caf5340 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -3,12 +3,15 @@ // found in the LICENSE file. import 'dart:collection'; +import 'dart:convert'; import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:flutter_plugin_tools/src/publish_check_command.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -126,6 +129,222 @@ void main() { expect(runner.run(['publish-check']), throwsA(isA())); }); + + test( + '--machine: Log JSON with status:no-publish and correct human message, if there are no packages need to be published. ', + () async { + const Map httpResponseA = { + 'name': 'a', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + const Map httpResponseB = { + 'name': 'b', + 'versions': [ + '0.0.1', + '0.1.0', + '0.2.0', + ], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'no_publish_a.json') { + return http.Response(json.encode(httpResponseA), 200); + } else if (request.url.pathSegments.last == 'no_publish_b.json') { + return http.Response(json.encode(httpResponseB), 200); + } + return null; + }); + final PublishCheckCommand command = PublishCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, httpClient: mockClient); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(command); + + final Directory plugin1Dir = + createFakePlugin('no_publish_a', includeVersion: true); + final Directory plugin2Dir = + createFakePlugin('no_publish_b', includeVersion: true); + + createFakePubspec(plugin1Dir, + name: 'no_publish_a', includeVersion: true, version: '0.1.0'); + createFakePubspec(plugin2Dir, + name: 'no_publish_b', includeVersion: true, version: '0.2.0'); + + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + final List output = await runCapturingPrint( + runner, ['publish-check', '--machine']); + + // ignore: use_raw_strings + expect(output.first, ''' +{ + "status": "no-publish", + "humanMessage": [ + "Checking that no_publish_a can be published.", + "Package no_publish_a version: 0.1.0 has already be published on pub.", + "Checking that no_publish_b can be published.", + "Package no_publish_b version: 0.2.0 has already be published on pub.", + "SUCCESS: All packages passed publish check!" + ] +}'''); + }); + + test( + '--machine: Log JSON with status:needs-publish and correct human message, if there is at least 1 plugin needs to be published.', + () async { + const Map httpResponseA = { + 'name': 'a', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + const Map httpResponseB = { + 'name': 'b', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'no_publish_a.json') { + return http.Response(json.encode(httpResponseA), 200); + } else if (request.url.pathSegments.last == 'no_publish_b.json') { + return http.Response(json.encode(httpResponseB), 200); + } + return null; + }); + final PublishCheckCommand command = PublishCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, httpClient: mockClient); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(command); + + final Directory plugin1Dir = + createFakePlugin('no_publish_a', includeVersion: true); + final Directory plugin2Dir = + createFakePlugin('no_publish_b', includeVersion: true); + + createFakePubspec(plugin1Dir, + name: 'no_publish_a', includeVersion: true, version: '0.1.0'); + createFakePubspec(plugin2Dir, + name: 'no_publish_b', includeVersion: true, version: '0.2.0'); + + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + + final List output = await runCapturingPrint( + runner, ['publish-check', '--machine']); + + // ignore: use_raw_strings + expect(output.first, ''' +{ + "status": "needs-publish", + "humanMessage": [ + "Checking that no_publish_a can be published.", + "Package no_publish_a version: 0.1.0 has already be published on pub.", + "Checking that no_publish_b can be published.", + "Package no_publish_b is able to be published.", + "SUCCESS: All packages passed publish check!" + ] +}'''); + }); + + test( + '--machine: Log correct JSON, if there is at least 1 plugin contains error.', + () async { + const Map httpResponseA = { + 'name': 'a', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + const Map httpResponseB = { + 'name': 'b', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + print('url ${request.url}'); + print(request.url.pathSegments.last); + if (request.url.pathSegments.last == 'no_publish_a.json') { + return http.Response(json.encode(httpResponseA), 200); + } else if (request.url.pathSegments.last == 'no_publish_b.json') { + return http.Response(json.encode(httpResponseB), 200); + } + return null; + }); + final PublishCheckCommand command = PublishCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, httpClient: mockClient); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(command); + + final Directory plugin1Dir = + createFakePlugin('no_publish_a', includeVersion: true); + final Directory plugin2Dir = + createFakePlugin('no_publish_b', includeVersion: true); + + createFakePubspec(plugin1Dir, + name: 'no_publish_a', includeVersion: true, version: '0.1.0'); + createFakePubspec(plugin2Dir, + name: 'no_publish_b', includeVersion: true, version: '0.2.0'); + await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + + bool hasError = false; + final List output = await runCapturingPrint( + runner, ['publish-check', '--machine'], + errorHandler: (Error error) { + expect(error, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + // ignore: use_raw_strings + expect(output.first, ''' +{ + "status": "error", + "humanMessage": [ + "Checking that no_publish_a can be published.", + "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException: line 1, column 1: Not a map\\n ╷\\n1 │ bad-yaml\\n │ ^^^^^^^^\\n ╵}", + "no pubspec", + "Checking that no_publish_b can be published.", + "url https://pub.dev/packages/no_publish_b.json", + "no_publish_b.json", + "Package no_publish_b is able to be published.", + "ERROR: The following 1 package(s) failed the publishing check:\\nMemoryDirectory: '/packages/no_publish_a'" + ] +}'''); + }); }); } diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 25b248b00af..d708dc71bef 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -205,19 +205,29 @@ void cleanupPackages() { }); } +typedef _ErrorHandler = void Function(Error error); + /// Run the command [runner] with the given [args] and return /// what was printed. +/// A custom [errorHandler] can be used to handle the runner error as desired without throwing. Future> runCapturingPrint( - CommandRunner runner, List args) async { + CommandRunner runner, List args, {_ErrorHandler errorHandler}) async { final List prints = []; final ZoneSpecification spec = ZoneSpecification( print: (_, __, ___, String message) { prints.add(message); }, ); - await Zone.current + try { + await Zone.current .fork(specification: spec) .run>(() => runner.run(args)); + } on Error catch (e) { + if (errorHandler == null) { + rethrow; + } + errorHandler(e); + } return prints; } diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index ea1a82ae744..d67103f716a 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; import 'package:args/command_runner.dart'; @@ -10,6 +11,8 @@ import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:flutter_plugin_tools/src/version_check_command.dart'; import 'package:git/git.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'; @@ -39,19 +42,29 @@ class MockGitDir extends Mock implements GitDir {} class MockProcessResult extends Mock implements io.ProcessResult {} +const String _redColorMessagePrefix = '\x1B[31m'; +const String _redColorMessagePostfix = '\x1B[0m'; + +// Some error message was printed in a "Colorized" red message. So `\x1B[31m` and `\x1B[0m` needs to be included. +String _redColorString(String string) { + return '$_redColorMessagePrefix$string$_redColorMessagePostfix'; +} + void main() { + const String indentation = ' '; group('$VersionCheckCommand', () { CommandRunner runner; RecordingProcessRunner processRunner; List> gitDirCommands; String gitDiffResponse; Map gitShowResponses; + MockGitDir gitDir; setUp(() { gitDirCommands = >[]; gitDiffResponse = ''; gitShowResponses = {}; - final MockGitDir gitDir = MockGitDir(); + gitDir = MockGitDir(); when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { gitDirCommands.add(invocation.positionalArguments[0] as List); final MockProcessResult mockProcessResult = MockProcessResult(); @@ -165,6 +178,7 @@ void main() { expect( output, containsAllInOrder([ + '${indentation}Unable to find pubspec in master. Safe to ignore if the project is new.', 'No version check errors found!', ]), ); @@ -321,25 +335,27 @@ void main() { * Some changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - final Future> output = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); - await expectLater( + bool hasError = false; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( output, - throwsA(const TypeMatcher()), + containsAllInOrder([ + _redColorString(''' +versions for plugin in CHANGELOG.md and pubspec.yaml do not match. +The version in pubspec.yaml is 1.0.1. +The first version listed in CHANGELOG.md is 1.0.2. +'''), + ]), ); - try { - final List outputValue = await output; - await expectLater( - outputValue, - containsAllInOrder([ - ''' - versions for plugin in CHANGELOG.md and pubspec.yaml do not match. - The version in pubspec.yaml is 1.0.1. - The first version listed in CHANGELOG.md is 1.0.2. - ''', - ]), - ); - } on ToolExit catch (_) {} }); test('Success if CHANGELOG and pubspec versions match', () async { @@ -388,25 +404,29 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - final Future> output = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); - await expectLater( + bool hasError = false; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( output, - throwsA(const TypeMatcher()), - ); - try { - final List outputValue = await output; - await expectLater( - outputValue, - containsAllInOrder([ + containsAllInOrder([ + _redColorString( ''' - versions for plugin in CHANGELOG.md and pubspec.yaml do not match. - The version in pubspec.yaml is 1.0.0. - The first version listed in CHANGELOG.md is 1.0.1. - ''', - ]), - ); - } on ToolExit catch (_) {} +versions for plugin in CHANGELOG.md and pubspec.yaml do not match. +The version in pubspec.yaml is 1.0.0. +The first version listed in CHANGELOG.md is 1.0.1. +''', + ) + ]), + ); }); test('Allow NEXT as a placeholder for gathering CHANGELOG entries', @@ -463,25 +483,28 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - final Future> output = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); - await expectLater( + bool hasError = false; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( output, - throwsA(const TypeMatcher()), - ); - try { - final List outputValue = await output; - await expectLater( - outputValue, - containsAllInOrder([ + containsAllInOrder([ + _redColorString( ''' - versions for plugin in CHANGELOG.md and pubspec.yaml do not match. - The version in pubspec.yaml is 1.0.0. - The first version listed in CHANGELOG.md is 1.0.1. - ''', - ]), - ); - } on ToolExit catch (_) {} +When bumping the version for release, the NEXT section should be incorporated +into the new version's release notes. +''', + ) + ]), + ); }); test('Fail if the version changes without replacing NEXT', () async { @@ -502,25 +525,194 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - final Future> output = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); - await expectLater( + bool hasError = false; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( output, - throwsA(const TypeMatcher()), + containsAllInOrder([ + 'Found NEXT; validating next version in the CHANGELOG.', + _redColorString( + ''' +versions for plugin in CHANGELOG.md and pubspec.yaml do not match. +The version in pubspec.yaml is 1.0.1. +The first version listed in CHANGELOG.md is 1.0.0. +''', + ) + ]), + ); + }); + + test('allows valid against pub', () async { + const Map httpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + '1.0.0', + ], + }; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(httpResponse), 200); + }); + final VersionCheckCommand command = VersionCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = await runCapturingPrint(runner, + ['version-check', '--base-sha=master', '--against-pub']); + + expect( + output, + containsAllInOrder([ + '${indentation}plugin: Current largest version on pub: 1.0.0', + 'No version check errors found!', + ]), ); - try { - final List outputValue = await output; - await expectLater( - outputValue, - containsAllInOrder([ + }); + + test('denies invalid against pub', () async { + const Map httpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + ], + }; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(httpResponse), 200); + }); + final VersionCheckCommand command = VersionCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + + bool hasError = false; + final List result = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + result, + containsAllInOrder([ + _redColorString( + ''' +${indentation}Incorrectly updated version. +${indentation}HEAD: 2.0.0, pub: 0.0.2. +${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: NextVersionType.MINOR, 0.0.3: NextVersionType.PATCH}''', + ) + ]), + ); + }); + + test( + 'throw and print error message if http request failed when checking against pub', + () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('xx', 400); + }); + final VersionCheckCommand command = VersionCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + bool hasError = false; + final List result = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + result, + containsAllInOrder([ + _redColorString( ''' - versions for plugin in CHANGELOG.md and pubspec.yaml do not match. - The version in pubspec.yaml is 1.0.0. - The first version listed in CHANGELOG.md is 1.0.1. - ''', - ]), - ); - } on ToolExit catch (_) {} +${indentation}Error fetching version on pub for plugin. +${indentation}HTTP Status 400 +${indentation}HTTP response: xx +''', + ) + ]), + ); + }); + + test('when checking against pub, allow any version if http status is 404.', + () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('xx', 404); + }); + final VersionCheckCommand command = VersionCheckCommand( + mockPackagesDir, mockFileSystem, + processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List result = await runCapturingPrint(runner, + ['version-check', '--base-sha=master', '--against-pub']); + + expect( + result, + containsAllInOrder([ + '${indentation}Unable to find package on pub server. Safe to ignore if the project is new.', + 'No version check errors found!', + ]), + ); }); }); From ec7bf3b46437974655882b23225cb43b7bad48a6 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 11 May 2021 16:08:01 -0400 Subject: [PATCH 042/249] Temporarily run Windows tests via GitHub Actions (#3875) Until the infrastructure limitations that led to flutter/flutter#82032 are fixed, run basic Windows tests via GitHub Actions (which do have VS available) so that we aren't missing all Windows coverage. --- script/tool/lib/src/firebase_test_lab_command.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index ff7d05bf4e8..d4b8382f3b4 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -27,8 +27,8 @@ class FirebaseTestLabCommand extends PluginCommand { help: 'The Firebase project name.', ); argParser.addOption('service-key', - defaultsTo: - p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); + defaultsTo: p.join( + io.Platform.environment['HOME'] ?? '/', 'gcloud-service-key.json')); argParser.addOption('test-run-id', defaultsTo: Uuid().v4(), help: From 3e28166394901054a0922a629d676727bbaf7c3d Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 11 May 2021 14:39:58 -0700 Subject: [PATCH 043/249] [tool] add an `skip-confirmation` flag to publish-package for running the command on ci (#3842) the skip-confirmation flag will add a --force flag to pub publish, it will also let users to skip the y/n question when pushing tags to remote. Fixes flutter/flutter#79830 --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/publish_plugin_command.dart | 115 ++++++++++++++---- .../test/publish_plugin_command_test.dart | 112 ++++++++++++----- 3 files changed, 171 insertions(+), 57 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index bc2a775db1f..5e9ce994683 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,7 @@ - Add `against-pub` flag for version-check, which allows the command to check version with pub. - Add `machine` flag for publish-check, which replaces outputs to something parsable by machines. +- Add `skip-conformation` flag to publish-plugin to allow auto publishing. ## 0.1.1 diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 9bfa0e71743..1c8a2dc5752 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -84,6 +84,12 @@ class PublishPluginCommand extends PluginCommand { defaultsTo: false, negatable: true, ); + argParser.addFlag(_skipConfirmationFlag, + help: 'Run the command without asking for Y/N inputs.\n' + 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n' + 'It also skips the y/n inputs when pushing tags to remote.\n', + defaultsTo: false, + negatable: true); } static const String _packageOption = 'package'; @@ -93,6 +99,9 @@ class PublishPluginCommand extends PluginCommand { static const String _remoteOption = 'remote'; static const String _allChangedFlag = 'all-changed'; static const String _dryRunFlag = 'dry-run'; + static const String _skipConfirmationFlag = 'skip-confirmation'; + + static const String _pubCredentialName = 'PUB_CREDENTIALS'; // Version tags should follow -v. For example, // `flutter_plugin_tools-v0.0.24`. @@ -103,7 +112,9 @@ class PublishPluginCommand extends PluginCommand { @override final String description = - 'Attempts to publish the given plugin and tag its release on GitHub.'; + 'Attempts to publish the given plugin and tag its release on GitHub.\n' + 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' + 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; final Print _print; final io.Stdin _stdin; @@ -267,7 +278,8 @@ Safe to ignore if the package is deleted in this commit. } if (pubspec.version == null) { - _print('No version found. A package that intentionally has no version should be marked "publish_to: none"'); + _print( + 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); return _CheckNeedsReleaseResult.failure; } @@ -408,24 +420,33 @@ Safe to ignore if the package is deleted in this commit. argResults[_pubFlagsOption] as List; _print( 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); - if (!(argResults[_dryRunFlag] as bool)) { - final io.Process publish = await processRunner.start( - 'flutter', ['pub', 'publish'] + publishFlags, - workingDirectory: packageDir); - publish.stdout - .transform(utf8.decoder) - .listen((String data) => _print(data)); - publish.stderr - .transform(utf8.decoder) - .listen((String data) => _print(data)); - _stdinSubscription ??= _stdin - .transform(utf8.decoder) - .listen((String data) => publish.stdin.writeln(data)); - final int result = await publish.exitCode; - if (result != 0) { - _print('Publish ${packageDir.basename} failed.'); - return false; - } + if (argResults[_dryRunFlag] as bool) { + return true; + } + + if (argResults[_skipConfirmationFlag] as bool) { + publishFlags.add('--force'); + } + if (publishFlags.contains('--force')) { + _ensureValidPubCredential(); + } + + final io.Process publish = await processRunner.start( + 'flutter', ['pub', 'publish'] + publishFlags, + workingDirectory: packageDir); + publish.stdout + .transform(utf8.decoder) + .listen((String data) => _print(data)); + publish.stderr + .transform(utf8.decoder) + .listen((String data) => _print(data)); + _stdinSubscription ??= _stdin + .transform(utf8.decoder) + .listen((String data) => publish.stdin.writeln(data)); + final int result = await publish.exitCode; + if (result != 0) { + _print('Publish ${packageDir.basename} failed.'); + return false; } return true; } @@ -453,11 +474,13 @@ Safe to ignore if the package is deleted in this commit. @required String remoteUrl, }) async { assert(remote != null && tag != null && remoteUrl != null); - _print('Ready to push $tag to $remoteUrl (y/n)?'); - final String input = _stdin.readLineSync(); - if (input.toLowerCase() != 'y') { - _print('Tag push canceled.'); - return false; + if (!(argResults[_skipConfirmationFlag] as bool)) { + _print('Ready to push $tag to $remoteUrl (y/n)?'); + final String input = _stdin.readLineSync(); + if (input.toLowerCase() != 'y') { + _print('Tag push canceled.'); + return false; + } } if (!(argResults[_dryRunFlag] as bool)) { final io.ProcessResult result = await processRunner.run( @@ -473,8 +496,50 @@ Safe to ignore if the package is deleted in this commit. } return true; } + + void _ensureValidPubCredential() { + final File credentialFile = fileSystem.file(_credentialsPath); + if (credentialFile.existsSync() && + credentialFile.readAsStringSync().isNotEmpty) { + return; + } + final String credential = io.Platform.environment[_pubCredentialName]; + if (credential == null) { + printErrorAndExit(errorMessage: ''' +No pub credential available. Please check if `~/.pub-cache/credentials.json` is valid. +If running this command on CI, you can set the pub credential content in the $_pubCredentialName environment variable. +'''); + } + credentialFile.openSync(mode: FileMode.writeOnlyAppend) + ..writeStringSync(credential) + ..closeSync(); + } + + /// Returns the correct path where the pub credential is stored. + @visibleForTesting + static String getCredentialPath() { + return _credentialsPath; + } } +/// The path in which pub expects to find its credentials file. +final String _credentialsPath = () { + // This follows the same logic as pub: + // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 + String cacheDir; + final String pubCache = io.Platform.environment['PUB_CACHE']; + print(pubCache); + if (pubCache != null) { + cacheDir = pubCache; + } else if (io.Platform.isWindows) { + final String appData = io.Platform.environment['APPDATA']; + cacheDir = p.join(appData, 'Pub', 'Cache'); + } else { + cacheDir = p.join(io.Platform.environment['HOME'], '.pub-cache'); + } + return p.join(cacheDir, 'credentials.json'); +}(); + enum _CheckNeedsReleaseResult { // The package needs to be released. release, diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 02d6d8140a6..b622e05861f 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -29,32 +29,39 @@ void main() { TestProcessRunner processRunner; CommandRunner commandRunner; MockStdin mockStdin; + // This test uses a local file system instead of an in memory one throughout + // so that git actually works. In setup we initialize a mono repo of plugins + // with one package and commit everything to Git. + const FileSystem fileSystem = LocalFileSystem(); + + void _createMockCredentialFile() { + final String credentialPath = PublishPluginCommand.getCredentialPath(); + fileSystem.file(credentialPath) + ..createSync(recursive: true) + ..writeAsStringSync('some credential'); + } setUp(() async { - // This test uses a local file system instead of an in memory one throughout - // so that git actually works. In setup we initialize a mono repo of plugins - // with one package and commit everything to Git. - parentDir = const LocalFileSystem() - .systemTempDirectory + parentDir = fileSystem.systemTempDirectory .createTempSync('publish_plugin_command_test-'); initializeFakePackages(parentDir: parentDir); - pluginDir = createFakePlugin(testPluginName, withSingleExample: false); + pluginDir = createFakePlugin(testPluginName, + withSingleExample: false, packagesDirectory: parentDir); assert(pluginDir != null && pluginDir.existsSync()); createFakePubspec(pluginDir, includeVersion: true); io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); + workingDirectory: parentDir.path); + gitDir = await GitDir.fromExisting(parentDir.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Initial commit']); processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand( - mockPackagesDir, mockPackagesDir.fileSystem, + ..addCommand(PublishPluginCommand(parentDir, fileSystem, processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString()), stdinput: mockStdin, - gitDir: await GitDir.fromExisting(mockPackagesDir.path))); + gitDir: await GitDir.fromExisting(parentDir.path))); }); tearDown(() { @@ -129,8 +136,8 @@ void main() { test('can publish non-flutter package', () async { createFakePubspec(pluginDir, includeVersion: true, isFlutter: false); io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); + workingDirectory: parentDir.path); + gitDir = await GitDir.fromExisting(parentDir.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. @@ -202,6 +209,29 @@ void main() { expect(processRunner.mockPublishArgs[3], '--server=foo'); }); + test( + '--skip-confirmation flag automatically adds --force to --pub-publish-flags', + () async { + processRunner.mockPublishCompleteCode = 0; + _createMockCredentialFile(); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + '--skip-confirmation', + '--pub-publish-flags', + '--server=foo' + ]); + + expect(processRunner.mockPublishArgs.length, 4); + expect(processRunner.mockPublishArgs[0], 'pub'); + expect(processRunner.mockPublishArgs[1], 'publish'); + expect(processRunner.mockPublishArgs[2], '--server=foo'); + expect(processRunner.mockPublishArgs[3], '--force'); + }); + test('throws if pub publish fails', () async { processRunner.mockPublishCompleteCode = 128; await expectLater( @@ -312,6 +342,24 @@ void main() { expect(printedMessages.last, 'Done!'); }); + test('does not ask for user input if the --skip-confirmation flag is on', + () async { + await gitDir.runCommand(['tag', 'garbage']); + processRunner.mockPublishCompleteCode = 0; + _createMockCredentialFile(); + await commandRunner.run([ + 'publish-plugin', + '--skip-confirmation', + '--package', + testPluginName, + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + test('to upstream by default, dry run', () async { await gitDir.runCommand(['tag', 'garbage']); // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. @@ -368,8 +416,8 @@ void main() { group('Auto release (all-changed flag)', () { setUp(() async { io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); + workingDirectory: parentDir.path); + gitDir = await GitDir.fromExisting(parentDir.path); await gitDir.runCommand( ['remote', 'add', 'upstream', 'http://localhost:8000']); }); @@ -377,12 +425,12 @@ void main() { test('can release newly created plugins', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -424,7 +472,7 @@ void main() { () async { // Prepare an exiting plugin and tag it final Directory pluginDir0 = createFakePlugin('plugin0', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); createFakePubspec(pluginDir0, name: 'plugin0', includeVersion: true, @@ -441,12 +489,12 @@ void main() { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -485,12 +533,12 @@ void main() { test('can release newly created plugins, dry run', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -534,12 +582,12 @@ void main() { test('version change triggers releases.', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -625,12 +673,12 @@ void main() { () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -713,12 +761,12 @@ void main() { () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -795,12 +843,12 @@ void main() { test('No version change does not release any plugins', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -813,8 +861,8 @@ void main() { version: '0.0.1'); io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); + workingDirectory: parentDir.path); + gitDir = await GitDir.fromExisting(parentDir.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); From f7f736a9665abd095af076f31a15a48af8774888 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 13 May 2021 13:14:04 -0400 Subject: [PATCH 044/249] Revert "Temporarily run Windows tests via GitHub Actions (#3875)" (#3885) --- script/tool/lib/src/firebase_test_lab_command.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index d4b8382f3b4..ff7d05bf4e8 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -27,8 +27,8 @@ class FirebaseTestLabCommand extends PluginCommand { help: 'The Firebase project name.', ); argParser.addOption('service-key', - defaultsTo: p.join( - io.Platform.environment['HOME'] ?? '/', 'gcloud-service-key.json')); + defaultsTo: + p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); argParser.addOption('test-run-id', defaultsTo: Uuid().v4(), help: From e46aa5583fb382b5ca0687d25903ca11ce287750 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 14 May 2021 20:03:23 -0400 Subject: [PATCH 045/249] Update tool README for packages use (#3882) Updates to better reflect that this tooling is still used in flutter/packages, and how that differs from the flutter/plugins usage. --- script/tool/README.md | 86 +++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/script/tool/README.md b/script/tool/README.md index b759b9a9ce5..c142b66178b 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -5,46 +5,84 @@ repositories. It is no longer explictily maintained as a general-purpose tool for multi-package repositories, so your mileage may vary if using it in other repositories. ----- - Note: The commands in tools are designed to run at the root of the repository or `/packages/`. -To run the tool (in flutter/plugins): +## Getting Started + +In flutter/plugins, the tool is run from source. In flutter/packages, the +[published version](https://pub.dev/packages/flutter_plugin_tools) is used +instead. (It is marked as Discontinued since it is no longer maintained as +a general-purpose tool, but updates are still published for use in +flutter/packages.) + +### From Source (flutter/plugins only) + +Set up: + +```sh +cd ./script/tool && dart pub get && cd ../../ +``` + +Run: ```sh -cd /script/tool && dart pub get && cd ../../ dart run ./script/tool/lib/src/main.dart ``` -## Format Code +### Published Version + +Set up: ```sh -cd +dart pub global activate flutter_plugin_tools +``` + +Run: + +```sh +dart pub global run flutter_plugin_tools +``` + +## Commands + +Run with `--help` for a full list of commands and arguments, but the +following shows a number of common commands. + +All examples assume running from source; see above for running the +published version instead. + +Note that the `plugins` argument, despite the name, applies to any package. +(It will likely be renamed `packages` in the future.) + +### Format Code + +```sh +cd dart run /script/tool/lib/src/main.dart format --plugins plugin_name ``` -## Run static analyzer +### Run the Dart Static Analyzer ```sh -cd -pub run ./script/tool/lib/src/main.dart analyze --plugins plugin_name +cd +dart run ./script/tool/lib/src/main.dart analyze --plugins plugin_name ``` -## Run Dart unit tests +### Run Dart Unit Tests ```sh -cd -pub run ./script/tool/lib/src/main.dart test --plugins plugin_name +cd +dart run ./script/tool/lib/src/main.dart test --plugins plugin_name ``` -## Run XCTests +### Run XCTests ```sh -cd -dart run lib/src/main.dart xctest --target RunnerUITests --skip +cd +dart run ./script/tool/lib/src/main.dart xctest --target RunnerUITests --skip ``` -## Publish and tag release +### Publish a Release ``sh cd @@ -63,6 +101,16 @@ _everything_, including untracked or uncommitted files in version control. directory and refuse to publish if there are any mismatched files with version control present. -There is a lot about this process that is still to be desired. Some top level -items are being tracked in -[flutter/flutter#27258](https://github.com/flutter/flutter/issues/27258). +Automated publishing is under development. Follow +[flutter/flutter#27258](https://github.com/flutter/flutter/issues/27258) +for updates. + +## Updating the Tool + +For flutter/plugins, just changing the source here is all that's needed. + +For changes that are relevant to flutter/packages, you will also need to: +- Update the tool's pubspec.yaml and CHANGELOG +- Publish the tool +- Update the pinned version in + [flutter/packages](https://github.com/flutter/packages/blob/master/.cirrus.yml) From bd0081258ae21090e0ee6d5eebc97ef50857d476 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 14 May 2021 20:04:26 -0400 Subject: [PATCH 046/249] Begin migrating tools to NNBD (#3891) - Updates dependencies to null-safe versions - Migrates common.dart (which doesn't depend on anything) - Migrates common_tests.dart and its one dependency, utils.dart - Adds build_runner for Mockito mock generation - Adds a new utility methods for getting arguments that handle both the casting and the removal of nullability to address a common problematic pattern while migrating code. - Converts all files, not just the migrated ones, to those new helpers. Migrating common.dart and utils.dart should unblock a command-by-command migration to null safety. Reverts the separate of podspect lints into a step that doesn't do a Flutter upgrade (https://github.com/flutter/plugins/pull/3700) because without that step we had a version of Dart too old to run null-safe tooling. First step of https://github.com/flutter/flutter/issues/81912 --- script/tool/bin/flutter_plugin_tools.dart | 2 + script/tool/lib/src/analyze_command.dart | 6 +- .../tool/lib/src/build_examples_command.dart | 6 +- script/tool/lib/src/common.dart | 101 +++++++------ .../src/create_all_plugins_app_command.dart | 2 + .../tool/lib/src/drive_examples_command.dart | 25 +-- .../lib/src/firebase_test_lab_command.dart | 16 +- script/tool/lib/src/format_command.dart | 10 +- script/tool/lib/src/java_test_command.dart | 2 + .../tool/lib/src/license_check_command.dart | 2 + .../tool/lib/src/lint_podspecs_command.dart | 6 +- script/tool/lib/src/list_command.dart | 4 +- script/tool/lib/src/main.dart | 2 + .../tool/lib/src/publish_check_command.dart | 10 +- .../tool/lib/src/publish_plugin_command.dart | 29 ++-- script/tool/lib/src/test_command.dart | 4 +- .../tool/lib/src/version_check_command.dart | 9 +- script/tool/lib/src/xctest_command.dart | 8 +- script/tool/pubspec.yaml | 43 +++--- script/tool/test/analyze_command_test.dart | 2 + .../test/build_examples_command_test.dart | 2 + script/tool/test/common_test.dart | 67 ++++---- script/tool/test/common_test.mocks.dart | 143 ++++++++++++++++++ .../create_all_plugins_app_command_test.dart | 2 + .../test/drive_examples_command_test.dart | 2 + script/tool/test/firebase_test_lab_test.dart | 2 + script/tool/test/java_test_command_test.dart | 2 + .../tool/test/license_check_command_test.dart | 2 + .../tool/test/lint_podspecs_command_test.dart | 2 + script/tool/test/list_command_test.dart | 2 + script/tool/test/mocks.dart | 2 + .../tool/test/publish_check_command_test.dart | 2 + .../test/publish_plugin_command_test.dart | 2 + script/tool/test/test_command_test.dart | 2 + script/tool/test/util.dart | 46 +++--- script/tool/test/version_check_test.dart | 5 +- script/tool/test/xctest_command_test.dart | 2 + 37 files changed, 397 insertions(+), 179 deletions(-) create mode 100644 script/tool/test/common_test.mocks.dart diff --git a/script/tool/bin/flutter_plugin_tools.dart b/script/tool/bin/flutter_plugin_tools.dart index 0f30bee0d25..eea0db5a7f0 100644 --- a/script/tool/bin/flutter_plugin_tools.dart +++ b/script/tool/bin/flutter_plugin_tools.dart @@ -2,4 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart=2.9 + export 'package:flutter_plugin_tools/src/main.dart'; diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 61be5f98a62..ec22e068392 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.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. +// @dart=2.9 + import 'dart:async'; import 'package:file/file.dart'; @@ -42,8 +44,8 @@ class AnalyzeCommand extends PluginCommand { continue; } - final bool allowed = (argResults[_customAnalysisFlag] as List) - .any((String directory) => + final bool allowed = (getStringListArg(_customAnalysisFlag)).any( + (String directory) => directory != null && directory.isNotEmpty && p.isWithin(p.join(packagesDir.path, directory), file.path)); diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 0069c058681..f7129865630 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.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. +// @dart=2.9 + import 'dart:async'; import 'dart:io' as io; @@ -52,7 +54,7 @@ class BuildExamplesCommand extends PluginCommand { ]; final Map platforms = { for (final String platform in platformSwitches) - platform: argResults[platform] as bool + platform: getBoolArg(platform) }; if (!platforms.values.any((bool enabled) => enabled)) { print( @@ -63,7 +65,7 @@ class BuildExamplesCommand extends PluginCommand { final String flutterCommand = const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - final String enableExperiment = argResults[kEnableExperiment] as String; + final String enableExperiment = getStringArg(kEnableExperiment); final List failingPackages = []; await for (final Directory plugin in getPlugins()) { diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index 1e864fcbc38..e975ec1ebeb 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -12,14 +12,13 @@ import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; 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); +typedef Print = void Function(Object? object); /// Key for windows platform. const String kWindows = 'windows'; @@ -50,7 +49,7 @@ const String kEnableExperiment = 'enable-experiment'; /// Returns whether the given directory contains a Flutter package. bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { - if (entity == null || entity is! Directory) { + if (entity is! Directory) { return false; } @@ -59,7 +58,7 @@ bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { fileSystem.file(p.join(entity.path, 'pubspec.yaml')); final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap dependencies = pubspecYaml['dependencies'] as YamlMap; + final YamlMap? dependencies = pubspecYaml['dependencies'] as YamlMap?; if (dependencies == null) { return false; } @@ -87,7 +86,7 @@ bool pluginSupportsPlatform( platform == kMacos || platform == kWindows || platform == kLinux); - if (entity == null || entity is! Directory) { + if (entity is! Directory) { return false; } @@ -96,15 +95,15 @@ bool pluginSupportsPlatform( fileSystem.file(p.join(entity.path, 'pubspec.yaml')); final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap flutterSection = pubspecYaml['flutter'] as YamlMap; + final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; if (flutterSection == null) { return false; } - final YamlMap pluginSection = flutterSection['plugin'] as YamlMap; + final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; if (pluginSection == null) { return false; } - final YamlMap platforms = pluginSection['platforms'] as YamlMap; + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; if (platforms == null) { // Legacy plugin specs are assumed to support iOS and Android. if (!pluginSection.containsKey('platforms')) { @@ -151,7 +150,7 @@ bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { } /// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red. -void printErrorAndExit({@required String errorMessage, int exitCode = 1}) { +void printErrorAndExit({required String errorMessage, int exitCode = 1}) { final Colorize redError = Colorize(errorMessage)..red(); print(redError); throw ToolExit(exitCode); @@ -236,17 +235,17 @@ abstract class PluginCommand extends Command { /// The git directory to use. By default it uses the parent directory. /// /// This can be mocked for testing. - final GitDir gitDir; + final GitDir? gitDir; - int _shardIndex; - int _shardCount; + 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; + return _shardIndex!; } /// The number of shards this command is divided into. @@ -254,12 +253,27 @@ abstract class PluginCommand extends Command { if (_shardCount == null) { _checkSharding(); } - return _shardCount; + 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(argResults[_shardIndexArg] as String); - final int shardCount = int.tryParse(argResults[_shardCountArg] as String); + final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); + final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); if (shardIndex == null) { usageException('$_shardIndexArg must be an integer'); } @@ -317,12 +331,10 @@ abstract class PluginCommand extends Command { /// 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(argResults[_pluginsArg] as List); + Set plugins = Set.from(getStringListArg(_pluginsArg)); final Set excludedPlugins = - Set.from(argResults[_excludeArg] as List); - final bool runOnChangedPackages = - argResults[_runOnChangedPackagesArg] as bool; + Set.from(getStringListArg(_excludeArg)); + final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); if (plugins.isEmpty && runOnChangedPackages) { plugins = await _getChangedPackages(); } @@ -429,9 +441,9 @@ abstract class PluginCommand extends Command { /// 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 = argResults[_kBaseSha] as String; + final String baseSha = getStringArg(_kBaseSha); - GitDir baseGitDir = gitDir; + GitDir? baseGitDir = gitDir; if (baseGitDir == null) { if (!await GitDir.isGitDir(rootDir)) { printErrorAndExit( @@ -490,7 +502,7 @@ class ProcessRunner { Future runAndStream( String executable, List args, { - Directory workingDir, + Directory? workingDir, bool exitOnError = false, }) async { print( @@ -522,7 +534,7 @@ class ProcessRunner { /// /// Returns the [io.ProcessResult] of the [executable]. Future run(String executable, List args, - {Directory workingDir, + {Directory? workingDir, bool exitOnError = false, bool logOnError = false, Encoding stdoutEncoding = io.systemEncoding, @@ -550,15 +562,15 @@ class ProcessRunner { /// passing [workingDir]. /// /// Returns the started [io.Process]. - Future start(String executable, List args, - {Directory workingDirectory}) async { + 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}) { + {Directory? workingDir}) { final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; } @@ -569,7 +581,7 @@ class PubVersionFinder { /// Constructor. /// /// Note: you should manually close the [httpClient] when done using the finder. - PubVersionFinder({this.pubHost = defaultPubHost, @required this.httpClient}); + PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient}); /// The default pub host to use. static const String defaultPubHost = 'https://pub.dev'; @@ -584,8 +596,8 @@ class PubVersionFinder { /// Get the package version on pub. Future getPackageVersion( - {@required String package}) async { - assert(package != null && package.isNotEmpty); + {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); @@ -618,8 +630,8 @@ class PubVersionFinder { class PubVersionFinderResponse { /// Constructor. PubVersionFinderResponse({this.versions, this.result, this.httpResponse}) { - if (versions != null && versions.isNotEmpty) { - versions.sort((Version a, Version b) { + if (versions != null && 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); @@ -631,13 +643,13 @@ class PubVersionFinderResponse { /// /// 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; + final List? versions; /// The result of the version finder. - final PubVersionFinderResult result; + final PubVersionFinderResult? result; /// The response object of the http request. - final http.Response httpResponse; + final http.Response? httpResponse; } /// An enum representing the result of [PubVersionFinder]. @@ -667,7 +679,7 @@ class GitVersionFinder { final GitDir baseGitDir; /// The base sha used to get diff. - final String baseSha; + final String? baseSha; static bool _isPubspec(String file) { return file.trim().endsWith('pubspec.yaml'); @@ -684,8 +696,7 @@ class GitVersionFinder { 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() ?? ''; + final String changedFilesStdout = changedFilesCommand.stdout.toString(); if (changedFilesStdout.isEmpty) { return []; } @@ -696,7 +707,8 @@ class GitVersionFinder { /// 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 { + Future getPackageVersion(String pubspecPath, + {String? gitRef}) async { final String ref = gitRef ?? (await _getBaseSha()); io.ProcessResult gitShow; @@ -707,20 +719,19 @@ class GitVersionFinder { return null; } final String fileContent = gitShow.stdout as String; - final String versionString = loadYaml(fileContent)['version'] 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; + if (baseSha != null && baseSha!.isNotEmpty) { + return baseSha!; } io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], throwOnError: false); - if (baseShaFromMergeBase == null || - baseShaFromMergeBase.stderr != null || + if (baseShaFromMergeBase.stderr != null || baseShaFromMergeBase.stdout == null) { baseShaFromMergeBase = await baseGitDir .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); 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 4b5b4c6b8df..d825703f173 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.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. +// @dart=2.9 + import 'dart:async'; import 'dart:io' as io; diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 1572b941078..76ba8205e17 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.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. +// @dart=2.9 + import 'dart:async'; import 'package:file/file.dart'; import 'package:path/path.dart' as p; @@ -53,10 +55,10 @@ class DriveExamplesCommand extends PluginCommand { Future run() async { final List failingTests = []; final List pluginsWithoutTests = []; - final bool isLinux = argResults[kLinux] == true; - final bool isMacos = argResults[kMacos] == true; - final bool isWeb = argResults[kWeb] == true; - final bool isWindows = argResults[kWindows] == true; + final bool isLinux = getBoolArg(kLinux); + final bool isMacos = getBoolArg(kMacos); + final bool isWeb = getBoolArg(kWeb); + final bool isWindows = getBoolArg(kWindows); await for (final Directory plugin in getPlugins()) { final String pluginName = plugin.basename; if (pluginName.endsWith('_platform_interface') && @@ -140,8 +142,7 @@ Tried searching for the following: final List driveArgs = ['drive']; - final String enableExperiment = - argResults[kEnableExperiment] as String; + final String enableExperiment = getStringArg(kEnableExperiment); if (enableExperiment.isNotEmpty) { driveArgs.add('--enable-experiment=$enableExperiment'); } @@ -222,12 +223,12 @@ Tried searching for the following: Future _pluginSupportedOnCurrentPlatform( FileSystemEntity plugin, FileSystem fileSystem) async { - final bool isAndroid = argResults[kAndroid] == true; - final bool isIOS = argResults[kIos] == true; - final bool isLinux = argResults[kLinux] == true; - final bool isMacos = argResults[kMacos] == true; - final bool isWeb = argResults[kWeb] == true; - final bool isWindows = argResults[kWindows] == true; + final bool isAndroid = getBoolArg(kAndroid); + final bool isIOS = getBoolArg(kIos); + final bool isLinux = getBoolArg(kLinux); + final bool isMacos = getBoolArg(kMacos); + final bool isWeb = getBoolArg(kWeb); + final bool isWindows = getBoolArg(kWindows); if (isAndroid) { return isAndroidPlugin(plugin, fileSystem); } diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index ff7d05bf4e8..2d91deffa2c 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.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. +// @dart=2.9 + import 'dart:async'; import 'dart:io' as io; @@ -30,7 +32,7 @@ class FirebaseTestLabCommand extends PluginCommand { defaultsTo: p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); argParser.addOption('test-run-id', - defaultsTo: Uuid().v4(), + defaultsTo: const Uuid().v4(), help: 'Optional string to append to the results path, to avoid conflicts. ' 'Randomly chosen on each invocation if none is provided. ' @@ -78,7 +80,7 @@ class FirebaseTestLabCommand extends PluginCommand { [ 'auth', 'activate-service-account', - '--key-file=${argResults['service-key']}', + '--key-file=${getStringArg('service-key')}', ], exitOnError: true, logOnError: true, @@ -87,7 +89,7 @@ class FirebaseTestLabCommand extends PluginCommand { 'config', 'set', 'project', - argResults['project'] as String, + getStringArg('project'), ]); if (exitCode == 0) { _print('\nFirebase project configured.'); @@ -125,7 +127,7 @@ class FirebaseTestLabCommand extends PluginCommand { final Directory androidDirectory = fileSystem.directory(p.join(exampleDirectory.path, 'android')); - final String enableExperiment = argResults[kEnableExperiment] as String; + final String enableExperiment = getStringArg(kEnableExperiment); final String encodedEnableExperiment = Uri.encodeComponent('--enable-experiment=$enableExperiment'); @@ -213,7 +215,7 @@ class FirebaseTestLabCommand extends PluginCommand { continue; } final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; - final String testRunId = argResults['test-run-id'] as String; + final String testRunId = getStringArg('test-run-id'); final String resultsDir = 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; final List args = [ @@ -229,10 +231,10 @@ class FirebaseTestLabCommand extends PluginCommand { 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', '--timeout', '5m', - '--results-bucket=${argResults['results-bucket']}', + '--results-bucket=${getStringArg('results-bucket')}', '--results-dir=$resultsDir', ]; - for (final String device in argResults['device'] as List) { + for (final String device in getStringListArg('device')) { args.addAll(['--device', device]); } exitCode = await processRunner.runAndStream('gcloud', args, diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 9c29f0f8c68..b3a8bd04edb 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.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. +// @dart=2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -13,8 +15,8 @@ import 'package:quiver/iterables.dart'; import 'common.dart'; -const String _googleFormatterUrl = - 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; +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'); /// A command to format all package code. class FormatCommand extends PluginCommand { @@ -47,7 +49,7 @@ class FormatCommand extends PluginCommand { await _formatJava(googleFormatterPath); await _formatCppAndObjectiveC(); - if (argResults['fail-on-change'] == true) { + if (getBoolArg('fail-on-change')) { final bool modified = await _didModifyAnything(); if (modified) { throw ToolExit(1); @@ -105,7 +107,7 @@ class FormatCommand extends PluginCommand { // 'ProcessException: Argument list too long'. final Iterable> batches = partition(allFiles, 100); for (final List batch in batches) { - await processRunner.runAndStream(argResults['clang-format'] as String, + await processRunner.runAndStream(getStringArg('clang-format'), ['-i', '--style=Google', ...batch], workingDir: packagesDir, exitOnError: true); } diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index 5df97627cec..dff83d37315 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.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. +// @dart=2.9 + import 'dart:async'; import 'package:file/file.dart'; diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index a6528640b82..cc4eb96fac3 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.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. +// @dart=2.9 + import 'dart:async'; import 'package:file/file.dart'; diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index ebcefd6c234..0bf4c69b24f 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.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. +// @dart=2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -89,7 +91,7 @@ class LintPodspecsCommand extends PluginCommand { final List podspecs = await getFiles().where((File entity) { final String filePath = entity.path; return p.extension(filePath) == '.podspec' && - !(argResults['skip'] as List) + !getStringListArg('skip') .contains(p.basenameWithoutExtension(filePath)); }).toList(); @@ -122,7 +124,7 @@ class LintPodspecsCommand extends PluginCommand { Future _runPodLint(String podspecPath, {bool libraryLint}) async { - final bool allowWarnings = (argResults['ignore-warnings'] as List) + final bool allowWarnings = (getStringListArg('ignore-warnings')) .contains(p.basenameWithoutExtension(podspecPath)); final List arguments = [ 'lib', diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 49302a91ad2..cac6cfcf352 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.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. +// @dart=2.9 + import 'dart:async'; import 'package:file/file.dart'; @@ -36,7 +38,7 @@ class ListCommand extends PluginCommand { @override Future run() async { - switch (argResults[_type] as String) { + switch (getStringArg(_type)) { case _plugin: await for (final Directory package in getPlugins()) { print(package.path); diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 6fba3b34b95..4b0e85704b2 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.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. +// @dart=2.9 + import 'dart:io' as io; import 'package:args/command_runner.dart'; diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 84503f4540c..3f90465d4f7 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.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. +// @dart=2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -78,7 +80,7 @@ class PublishCheckCommand extends PluginCommand { Future run() async { final ZoneSpecification logSwitchSpecification = ZoneSpecification( print: (Zone self, ZoneDelegate parent, Zone zone, String message) { - final bool logMachineMessage = argResults[_machineFlag] as bool; + final bool logMachineMessage = getBoolArg(_machineFlag); if (logMachineMessage && message != _prettyJson(_machineOutput)) { _humanMessages.add(message); } else { @@ -123,7 +125,7 @@ class PublishCheckCommand extends PluginCommand { isError: false); } - if (argResults[_machineFlag] as bool) { + if (getBoolArg(_machineFlag)) { _setStatus(status); _machineOutput[_humanMessageKey] = _humanMessages; print(_prettyJson(_machineOutput)); @@ -184,7 +186,7 @@ class PublishCheckCommand extends PluginCommand { return true; } - if (!(argResults[_allowPrereleaseFlag] as bool)) { + if (!getBoolArg(_allowPrereleaseFlag)) { return false; } @@ -270,7 +272,7 @@ HTTP response: ${pubVersionFinderResponse.httpResponse.body} void _printImportantStatusMessage(String message, {@required bool isError}) { final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; - if (argResults[_machineFlag] as bool) { + if (getBoolArg(_machineFlag)) { print(statusMessage); } else { final Colorize colorizedMessage = Colorize(statusMessage); diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 1c8a2dc5752..cb72c189721 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.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. +// @dart=2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -122,9 +124,9 @@ class PublishPluginCommand extends PluginCommand { @override Future run() async { - final String package = argResults[_packageOption] as String; - final bool publishAllChanged = argResults[_allChangedFlag] as bool; - if (package == null && !publishAllChanged) { + final String package = getStringArg(_packageOption); + final bool publishAllChanged = getBoolArg(_allChangedFlag); + if (package.isEmpty && !publishAllChanged) { _print( 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); throw ToolExit(1); @@ -138,14 +140,14 @@ class PublishPluginCommand extends PluginCommand { final GitDir baseGitDir = await GitDir.fromExisting(packagesDir.path, allowSubdirectory: true); - final bool shouldPushTag = argResults[_pushTagsOption] == true; - final String remote = argResults[_remoteOption] as String; + final bool shouldPushTag = getBoolArg(_pushTagsOption); + final String remote = getStringArg(_remoteOption); String remoteUrl; if (shouldPushTag) { remoteUrl = await _verifyRemote(remote); } _print('Local repo is ready!'); - if (argResults[_dryRunFlag] as bool) { + if (getBoolArg(_dryRunFlag)) { _print('=============== DRY RUN ==============='); } @@ -244,7 +246,7 @@ class PublishPluginCommand extends PluginCommand { if (!await _publishPlugin(packageDir: packageDir)) { return false; } - if (argResults[_tagReleaseOption] as bool) { + if (getBoolArg(_tagReleaseOption)) { if (!await _tagRelease( packageDir: packageDir, remote: remote, @@ -333,7 +335,7 @@ Safe to ignore if the package is deleted in this commit. }) async { final String tag = _getTag(packageDir); _print('Tagging release $tag...'); - if (!(argResults[_dryRunFlag] as bool)) { + if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await processRunner.run( 'git', ['tag', tag], @@ -416,15 +418,14 @@ Safe to ignore if the package is deleted in this commit. } Future _publish(Directory packageDir) async { - final List publishFlags = - argResults[_pubFlagsOption] as List; + final List publishFlags = getStringListArg(_pubFlagsOption); _print( 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); - if (argResults[_dryRunFlag] as bool) { + if (getBoolArg(_dryRunFlag)) { return true; } - if (argResults[_skipConfirmationFlag] as bool) { + if (getBoolArg(_skipConfirmationFlag)) { publishFlags.add('--force'); } if (publishFlags.contains('--force')) { @@ -474,7 +475,7 @@ Safe to ignore if the package is deleted in this commit. @required String remoteUrl, }) async { assert(remote != null && tag != null && remoteUrl != null); - if (!(argResults[_skipConfirmationFlag] as bool)) { + if (!getBoolArg(_skipConfirmationFlag)) { _print('Ready to push $tag to $remoteUrl (y/n)?'); final String input = _stdin.readLineSync(); if (input.toLowerCase() != 'y') { @@ -482,7 +483,7 @@ Safe to ignore if the package is deleted in this commit. return false; } } - if (!(argResults[_dryRunFlag] as bool)) { + if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await processRunner.run( 'git', ['push', remote, tag], diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index bb4f9c12a77..c4aeab7022b 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.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. +// @dart=2.9 + import 'dart:async'; import 'package:file/file.dart'; @@ -44,7 +46,7 @@ class TestCommand extends PluginCommand { print('RUNNING $packageName tests...'); - final String enableExperiment = argResults[kEnableExperiment] as String; + final String enableExperiment = getStringArg(kEnableExperiment); // `flutter test` automatically gets packages. `pub run test` does not. :( int exitCode = 0; diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 475caf5d285..a802c4d425e 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.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. +// @dart=2.9 + import 'dart:async'; import 'package:file/file.dart'; @@ -135,7 +137,7 @@ class VersionCheckCommand extends PluginCommand { '"publish_to: none".'); } Version sourceVersion; - if (argResults[_againstPubFlag] as bool) { + if (getBoolArg(_againstPubFlag)) { final String packageName = pubspecFile.parent.basename; final PubVersionFinderResponse pubVersionFinderResponse = await _pubVersionFinder.getPackageVersion(package: packageName); @@ -161,7 +163,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } if (sourceVersion == null) { String safeToIgnoreMessage; - if (argResults[_againstPubFlag] as bool) { + if (getBoolArg(_againstPubFlag)) { safeToIgnoreMessage = '${indentation}Unable to find package on pub server.'; } else { @@ -181,8 +183,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} getAllowedNextVersions(sourceVersion, headVersion); if (!allowedNextVersions.containsKey(headVersion)) { - final String source = - (argResults[_againstPubFlag] as bool) ? 'pub' : 'master'; + final String source = (getBoolArg(_againstPubFlag)) ? 'pub' : 'master'; final String error = '${indentation}Incorrectly updated version.\n' '${indentation}HEAD: $headVersion, $source: $sourceVersion.\n' '${indentation}Allowed versions: $allowedNextVersions'; diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 64f85577dbc..335e0054fc0 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.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. +// @dart=2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -48,8 +50,8 @@ class XCTestCommand extends PluginCommand { @override Future run() async { - String destination = argResults[_kiOSDestination] as String; - if (destination == null) { + String destination = getStringArg(_kiOSDestination); + if (destination.isEmpty) { final String simulatorId = await _findAvailableIphoneSimulator(); if (simulatorId == null) { print(_kFoundNoSimulatorsMessage); @@ -58,7 +60,7 @@ class XCTestCommand extends PluginCommand { destination = 'id=$simulatorId'; } - final List skipped = argResults[_kSkip] as List; + final List skipped = getStringListArg(_kSkip); final List failingPackages = []; await for (final Directory plugin in getPlugins()) { diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 58dfc9fbd8b..725c8395b81 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -4,28 +4,29 @@ repository: https://github.com/flutter/plugins/tree/master/script/tool version: 0.1.1 dependencies: - args: "^1.4.3" - path: "^1.6.1" - http: "^0.12.1" - async: "^2.0.7" - yaml: "^2.1.15" - quiver: "^2.0.2" - pub_semver: ^1.4.2 - colorize: ^2.0.0 - git: ^1.0.0 - platform: ^2.2.0 - pubspec_parse: "^0.1.4" - test: ^1.6.4 - meta: ^1.1.7 - file: ^5.0.10 - uuid: ^2.0.4 - http_multi_server: ^2.2.0 - collection: ^1.14.13 + args: ^2.1.0 + async: ^2.6.1 + collection: ^1.15.0 + colorize: ^3.0.0 + file: ^6.1.0 + git: ^2.0.0 + http: ^0.13.3 + http_multi_server: ^3.0.1 + meta: ^1.3.0 + path: ^1.8.0 + platform: ^3.0.0 + pub_semver: ^2.0.0 + pubspec_parse: ^1.0.0 + quiver: ^3.0.1 + test: ^1.17.3 + uuid: ^3.0.4 + yaml: ^3.1.0 dev_dependencies: - matcher: ^0.12.6 - mockito: ^4.1.1 - pedantic: ^1.8.0 + build_runner: ^2.0.3 + matcher: ^0.12.10 + mockito: ^5.0.7 + pedantic: ^1.11.0 environment: - sdk: ">=2.3.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 11acb59a474..1e656b4f75e 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.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. +// @dart=2.9 + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/analyze_command.dart'; diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 40da27d4492..7b488879f3b 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.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. +// @dart=2.9 + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/build_examples_command.dart'; diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index d6ac449e7fd..477e186aa24 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -2,6 +2,7 @@ // 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'; @@ -12,21 +13,24 @@ import 'package:flutter_plugin_tools/src/common.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'; +@GenerateMocks([GitDir]) void main() { - RecordingProcessRunner processRunner; - CommandRunner runner; - FileSystem fileSystem; - Directory packagesDir; - Directory thirdPartyPackagesDir; - List plugins; - List> gitDirCommands; - String gitDiffResponse; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + late FileSystem fileSystem; + late Directory packagesDir; + late Directory thirdPartyPackagesDir; + late List plugins; + late List?> gitDirCommands; + late String gitDiffResponse; setUp(() { fileSystem = MemoryFileSystem(); @@ -35,14 +39,15 @@ void main() { .childDirectory('third_party') .childDirectory('packages'); - gitDirCommands = >[]; + gitDirCommands = ?>[]; gitDiffResponse = ''; final MockGitDir gitDir = MockGitDir(); - when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0] as List); + 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) + when(mockProcessResult.stdout as String?) .thenReturn(gitDiffResponse); } return Future.value(mockProcessResult); @@ -255,23 +260,24 @@ packages/plugin3/plugin3.dart }); group('$GitVersionFinder', () { - List> gitDirCommands; - String gitDiffResponse; - String mergeBaseResponse; - MockGitDir gitDir; + late List?> gitDirCommands; + late String gitDiffResponse; + String? mergeBaseResponse; + late MockGitDir gitDir; setUp(() { - gitDirCommands = >[]; + gitDirCommands = ?>[]; gitDiffResponse = ''; gitDir = MockGitDir(); - when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0] as List); + 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) + when(mockProcessResult.stdout as String?) .thenReturn(gitDiffResponse); } else if (invocation.positionalArguments[0][0] == 'merge-base') { - when(mockProcessResult.stdout as String) + when(mockProcessResult.stdout as String?) .thenReturn(mergeBaseResponse); } return Future.value(mockProcessResult); @@ -320,10 +326,11 @@ file2/file2.cc file1/pubspec.yaml file2/file2.cc '''; + final GitVersionFinder finder = GitVersionFinder(gitDir, null); await finder.getChangedFiles(); verify(gitDir.runCommand( - ['diff', '--name-only', mergeBaseResponse, 'HEAD'])); + ['diff', '--name-only', mergeBaseResponse!, 'HEAD'])); }); test('use correct base sha if specified', () async { @@ -350,8 +357,8 @@ file2/file2.cc expect(response.versions, isNull); expect(response.result, PubVersionFinderResult.noPackageFound); - expect(response.httpResponse.statusCode, 404); - expect(response.httpResponse.body, ''); + expect(response.httpResponse!.statusCode, 404); + expect(response.httpResponse!.body, ''); }); test('HTTP error when getting versions from pub', () async { @@ -364,8 +371,8 @@ file2/file2.cc expect(response.versions, isNull); expect(response.result, PubVersionFinderResult.fail); - expect(response.httpResponse.statusCode, 400); - expect(response.httpResponse.body, ''); + expect(response.httpResponse!.statusCode, 400); + expect(response.httpResponse!.body, ''); }); test('Get a correct list of versions when http response is OK.', () async { @@ -408,8 +415,8 @@ file2/file2.cc Version.parse('0.0.1'), ]); expect(response.result, PubVersionFinderResult.success); - expect(response.httpResponse.statusCode, 200); - expect(response.httpResponse.body, json.encode(httpResponse)); + expect(response.httpResponse!.statusCode, 200); + expect(response.httpResponse!.body, json.encode(httpResponse)); }); }); } @@ -420,7 +427,7 @@ class SamplePluginCommand extends PluginCommand { Directory packagesDir, FileSystem fileSystem, { ProcessRunner processRunner = const ProcessRunner(), - GitDir gitDir, + GitDir? gitDir, }) : super(packagesDir, fileSystem, processRunner: processRunner, gitDir: gitDir); @@ -440,6 +447,4 @@ class SamplePluginCommand extends PluginCommand { } } -class MockGitDir extends Mock implements GitDir {} - class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common_test.mocks.dart b/script/tool/test/common_test.mocks.dart new file mode 100644 index 00000000000..b7f7807b3b0 --- /dev/null +++ b/script/tool/test/common_test.mocks.dart @@ -0,0 +1,143 @@ +// Mocks generated by Mockito 5.0.7 from annotations +// in flutter_plugin_tools/test/common_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i6; +import 'dart:io' as _i4; + +import 'package:git/src/branch_reference.dart' as _i3; +import 'package:git/src/commit.dart' as _i2; +import 'package:git/src/commit_reference.dart' as _i8; +import 'package:git/src/git_dir.dart' as _i5; +import 'package:git/src/tag.dart' as _i7; +import 'package:git/src/tree_entry.dart' as _i9; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +// ignore_for_file: prefer_const_constructors + +// ignore_for_file: avoid_redundant_argument_values + +class _FakeCommit extends _i1.Fake implements _i2.Commit {} + +class _FakeBranchReference extends _i1.Fake implements _i3.BranchReference {} + +class _FakeProcessResult extends _i1.Fake implements _i4.ProcessResult {} + +/// A class which mocks [GitDir]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGitDir extends _i1.Mock implements _i5.GitDir { + MockGitDir() { + _i1.throwOnMissingStub(this); + } + + @override + String get path => + (super.noSuchMethod(Invocation.getter(#path), returnValue: '') as String); + @override + _i6.Future commitCount([String? branchName = r'HEAD']) => + (super.noSuchMethod(Invocation.method(#commitCount, [branchName]), + returnValue: Future.value(0)) as _i6.Future); + @override + _i6.Future<_i2.Commit> commitFromRevision(String? revision) => + (super.noSuchMethod(Invocation.method(#commitFromRevision, [revision]), + returnValue: Future<_i2.Commit>.value(_FakeCommit())) + as _i6.Future<_i2.Commit>); + @override + _i6.Future> commits([String? branchName = r'HEAD']) => + (super.noSuchMethod(Invocation.method(#commits, [branchName]), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future<_i3.BranchReference?> branchReference(String? branchName) => + (super.noSuchMethod(Invocation.method(#branchReference, [branchName]), + returnValue: + Future<_i3.BranchReference?>.value(_FakeBranchReference())) + as _i6.Future<_i3.BranchReference?>); + @override + _i6.Future> branches() => (super.noSuchMethod( + Invocation.method(#branches, []), + returnValue: + Future>.value(<_i3.BranchReference>[])) + as _i6.Future>); + @override + _i6.Stream<_i7.Tag> tags() => + (super.noSuchMethod(Invocation.method(#tags, []), + returnValue: Stream<_i7.Tag>.empty()) as _i6.Stream<_i7.Tag>); + @override + _i6.Future> showRef( + {bool? heads = false, bool? tags = false}) => + (super.noSuchMethod( + Invocation.method(#showRef, [], {#heads: heads, #tags: tags}), + returnValue: Future>.value( + <_i8.CommitReference>[])) + as _i6.Future>); + @override + _i6.Future<_i3.BranchReference> currentBranch() => + (super.noSuchMethod(Invocation.method(#currentBranch, []), + returnValue: + Future<_i3.BranchReference>.value(_FakeBranchReference())) + as _i6.Future<_i3.BranchReference>); + @override + _i6.Future> lsTree(String? treeish, + {bool? subTreesOnly = false, String? path}) => + (super.noSuchMethod( + Invocation.method(#lsTree, [treeish], + {#subTreesOnly: subTreesOnly, #path: path}), + returnValue: Future>.value(<_i9.TreeEntry>[])) + as _i6.Future>); + @override + _i6.Future createOrUpdateBranch( + String? branchName, String? treeSha, String? commitMessage) => + (super.noSuchMethod( + Invocation.method( + #createOrUpdateBranch, [branchName, treeSha, commitMessage]), + returnValue: Future.value('')) as _i6.Future); + @override + _i6.Future commitTree(String? treeSha, String? commitMessage, + {List? parentCommitShas}) => + (super.noSuchMethod( + Invocation.method(#commitTree, [treeSha, commitMessage], + {#parentCommitShas: parentCommitShas}), + returnValue: Future.value('')) as _i6.Future); + @override + _i6.Future> writeObjects(List? paths) => + (super.noSuchMethod(Invocation.method(#writeObjects, [paths]), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future<_i4.ProcessResult> runCommand(Iterable? args, + {bool? throwOnError = true}) => + (super.noSuchMethod( + Invocation.method(#runCommand, [args], {#throwOnError: throwOnError}), + returnValue: + Future<_i4.ProcessResult>.value(_FakeProcessResult())) as _i6 + .Future<_i4.ProcessResult>); + @override + _i6.Future isWorkingTreeClean() => + (super.noSuchMethod(Invocation.method(#isWorkingTreeClean, []), + returnValue: Future.value(false)) as _i6.Future); + @override + _i6.Future<_i2.Commit?> updateBranch( + String? branchName, + _i6.Future Function(_i4.Directory)? populater, + String? commitMessage) => + (super.noSuchMethod( + Invocation.method( + #updateBranch, [branchName, populater, commitMessage]), + returnValue: Future<_i2.Commit?>.value(_FakeCommit())) + as _i6.Future<_i2.Commit?>); + @override + _i6.Future<_i2.Commit?> updateBranchWithDirectoryContents(String? branchName, + String? sourceDirectoryPath, String? commitMessage) => + (super.noSuchMethod( + Invocation.method(#updateBranchWithDirectoryContents, + [branchName, sourceDirectoryPath, commitMessage]), + returnValue: Future<_i2.Commit?>.value(_FakeCommit())) + as _i6.Future<_i2.Commit?>); +} diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index fedc0468463..caf4218dc16 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.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. +// @dart=2.9 + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 5ba8d8af25f..7905bb279f4 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.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. +// @dart=2.9 + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common.dart'; diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index d6068691d54..b4e5b5b8c72 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.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. +// @dart=2.9 + import 'dart:io'; import 'package:args/command_runner.dart'; diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index ba016915223..cfa8d448f37 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.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. +// @dart=2.9 + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/java_test_command.dart'; diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 23de2581b9b..a0c74fc6acd 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.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. +// @dart=2.9 + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index a1fa1f7c774..13bb1cc8d32 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.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. +// @dart=2.9 + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index e7fcfe7a7c4..a767671be5c 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.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. +// @dart=2.9 + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/list_command.dart'; diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index ad1f357fb47..a83ca754380 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.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. +// @dart=2.9 + import 'dart:async'; import 'dart:io' as io; diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index eca7caf5340..8c34b3313a0 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.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. +// @dart=2.9 + import 'dart:collection'; import 'dart:convert'; import 'dart:io' as io; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index b622e05861f..357e72efa00 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.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. +// @dart=2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 1dd0c158293..550249169b9 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.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. +// @dart=2.9 + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/test_command.dart'; diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index d708dc71bef..7d4278f68cc 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -21,13 +21,13 @@ FileSystem mockFileSystem = MemoryFileSystem( style: const LocalPlatform().isWindows ? FileSystemStyle.windows : FileSystemStyle.posix); -Directory mockPackagesDir; +late Directory mockPackagesDir; /// Creates a mock packages directory in the mock file system. /// /// If [parentDir] is set the mock packages dir will be creates as a child of /// it. If not [mockFileSystem] will be used instead. -void initializeFakePackages({Directory parentDir}) { +void initializeFakePackages({Directory? parentDir}) { mockPackagesDir = (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); mockPackagesDir.createSync(); @@ -51,7 +51,7 @@ Directory createFakePlugin( bool includeVersion = false, String version = '0.0.1', String parentDirectoryName = '', - Directory packagesDirectory, + Directory? packagesDirectory, }) { assert(!(withSingleExample && withExamples.isNotEmpty), 'cannot pass withSingleExample and withExamples simultaneously'); @@ -211,7 +211,8 @@ typedef _ErrorHandler = void Function(Error error); /// what was printed. /// A custom [errorHandler] can be used to handle the runner error as desired without throwing. Future> runCapturingPrint( - CommandRunner runner, List args, {_ErrorHandler errorHandler}) async { + CommandRunner runner, List args, + {_ErrorHandler? errorHandler}) async { final List prints = []; final ZoneSpecification spec = ZoneSpecification( print: (_, __, ___, String message) { @@ -220,8 +221,8 @@ Future> runCapturingPrint( ); try { await Zone.current - .fork(specification: spec) - .run>(() => runner.run(args)); + .fork(specification: spec) + .run>(() => runner.run(args)); } on Error catch (e) { if (errorHandler == null) { rethrow; @@ -234,25 +235,25 @@ Future> runCapturingPrint( /// A mock [ProcessRunner] which records process calls. class RecordingProcessRunner extends ProcessRunner { - io.Process processToReturn; + io.Process? processToReturn; final List recordedCalls = []; /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. - String resultStdout; + String? resultStdout; /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. - String resultStderr; + String? resultStderr; @override Future runAndStream( String executable, List args, { - Directory workingDir, + Directory? workingDir, bool exitOnError = false, }) async { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); return Future.value( - processToReturn == null ? 0 : await processToReturn.exitCode); + processToReturn == null ? 0 : await processToReturn!.exitCode); } /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. @@ -260,28 +261,26 @@ class RecordingProcessRunner extends ProcessRunner { Future run( String executable, List args, { - Directory workingDir, + Directory? workingDir, bool exitOnError = false, bool logOnError = false, Encoding stdoutEncoding = io.systemEncoding, Encoding stderrEncoding = io.systemEncoding, }) async { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - io.ProcessResult result; + io.ProcessResult? result; - if (processToReturn != null) { - result = io.ProcessResult( - processToReturn.pid, - await processToReturn.exitCode, - resultStdout ?? processToReturn.stdout, - resultStderr ?? processToReturn.stderr); + final io.Process? process = processToReturn; + if (process != null) { + result = io.ProcessResult(process.pid, await process.exitCode, + resultStdout ?? process.stdout, resultStderr ?? process.stderr); } return Future.value(result); } @override Future start(String executable, List args, - {Directory workingDirectory}) async { + {Directory? workingDirectory}) async { recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); return Future.value(processToReturn); } @@ -299,7 +298,7 @@ class ProcessCall { final List args; /// The working directory this process was called from. - final String workingDir; + final String? workingDir; @override bool operator ==(dynamic other) { @@ -311,10 +310,7 @@ class ProcessCall { @override int get hashCode => - executable?.hashCode ?? - 0 ^ args?.hashCode ?? - 0 ^ workingDir?.hashCode ?? - 0; + (executable.hashCode) ^ (args.hashCode) ^ (workingDir?.hashCode ?? 0); @override String toString() { diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index d67103f716a..cef2ab1cfad 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.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. +// @dart=2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -65,7 +67,8 @@ void main() { gitDiffResponse = ''; gitShowResponses = {}; gitDir = MockGitDir(); - when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) { + 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') { diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 1707dc8cfb8..53e82ac623e 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.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. +// @dart=2.9 + import 'dart:convert'; import 'package:args/command_runner.dart'; From 842ae94fde61595a470ef4029146aa386a9ad60d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 17 May 2021 14:25:01 -0400 Subject: [PATCH 047/249] Migrate some tool commands to NNBD (#3899) Now that individual commands can be migrated, migrate several commands that are trivially migratable. Part of https://github.com/flutter/flutter/issues/81912 --- script/tool/lib/src/analyze_command.dart | 2 -- .../tool/lib/src/build_examples_command.dart | 20 +++++++------------ .../tool/lib/src/drive_examples_command.dart | 2 -- script/tool/lib/src/format_command.dart | 2 -- script/tool/lib/src/java_test_command.dart | 2 -- .../tool/lib/src/license_check_command.dart | 2 -- script/tool/lib/src/list_command.dart | 2 -- script/tool/lib/src/test_command.dart | 2 -- script/tool/test/analyze_command_test.dart | 6 ++---- .../test/build_examples_command_test.dart | 6 ++---- .../test/drive_examples_command_test.dart | 6 ++---- script/tool/test/java_test_command_test.dart | 4 +--- .../tool/test/license_check_command_test.dart | 12 +++++------ script/tool/test/list_command_test.dart | 4 +--- script/tool/test/mocks.dart | 4 +--- script/tool/test/test_command_test.dart | 4 +--- 16 files changed, 22 insertions(+), 58 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index ec22e068392..0ed6c2caa5c 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_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. -// @dart=2.9 - import 'dart:async'; import 'package:file/file.dart'; diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index f7129865630..75bff3b25dc 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_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. -// @dart=2.9 - import 'dart:async'; import 'dart:io' as io; @@ -52,11 +50,7 @@ class BuildExamplesCommand extends PluginCommand { kWeb, kWindows, ]; - final Map platforms = { - for (final String platform in platformSwitches) - platform: getBoolArg(platform) - }; - if (!platforms.values.any((bool enabled) => enabled)) { + if (!platformSwitches.any((String platform) => getBoolArg(platform))) { print( 'None of ${platformSwitches.map((String platform) => '--$platform').join(', ')} ' 'were specified, so not building anything.'); @@ -73,7 +67,7 @@ class BuildExamplesCommand extends PluginCommand { final String packageName = p.relative(example.path, from: packagesDir.path); - if (platforms[kLinux]) { + if (getBoolArg(kLinux)) { print('\nBUILDING Linux for $packageName'); if (isLinuxPlugin(plugin, fileSystem)) { final int buildExitCode = await processRunner.runAndStream( @@ -93,7 +87,7 @@ class BuildExamplesCommand extends PluginCommand { } } - if (platforms[kMacos]) { + if (getBoolArg(kMacos)) { print('\nBUILDING macOS for $packageName'); if (isMacOsPlugin(plugin, fileSystem)) { final int exitCode = await processRunner.runAndStream( @@ -113,7 +107,7 @@ class BuildExamplesCommand extends PluginCommand { } } - if (platforms[kWeb]) { + if (getBoolArg(kWeb)) { print('\nBUILDING web for $packageName'); if (isWebPlugin(plugin, fileSystem)) { final int buildExitCode = await processRunner.runAndStream( @@ -133,7 +127,7 @@ class BuildExamplesCommand extends PluginCommand { } } - if (platforms[kWindows]) { + if (getBoolArg(kWindows)) { print('\nBUILDING Windows for $packageName'); if (isWindowsPlugin(plugin, fileSystem)) { final int buildExitCode = await processRunner.runAndStream( @@ -153,7 +147,7 @@ class BuildExamplesCommand extends PluginCommand { } } - if (platforms[kIpa]) { + if (getBoolArg(kIpa)) { print('\nBUILDING IPA for $packageName'); if (isIosPlugin(plugin, fileSystem)) { final int exitCode = await processRunner.runAndStream( @@ -174,7 +168,7 @@ class BuildExamplesCommand extends PluginCommand { } } - if (platforms[kApk]) { + if (getBoolArg(kApk)) { print('\nBUILDING APK for $packageName'); if (isAndroidPlugin(plugin, fileSystem)) { final int exitCode = await processRunner.runAndStream( diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 76ba8205e17..15f465f5359 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_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. -// @dart=2.9 - import 'dart:async'; import 'package:file/file.dart'; import 'package:path/path.dart' as p; diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index b3a8bd04edb..51ab7957d88 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_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. -// @dart=2.9 - import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index dff83d37315..5df97627cec 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_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. -// @dart=2.9 - import 'dart:async'; import 'package:file/file.dart'; diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index cc4eb96fac3..a6528640b82 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_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. -// @dart=2.9 - import 'dart:async'; import 'package:file/file.dart'; diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index cac6cfcf352..f834c8aa502 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_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. -// @dart=2.9 - import 'dart:async'; import 'package:file/file.dart'; diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index c4aeab7022b..7455095effc 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_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. -// @dart=2.9 - import 'dart:async'; import 'package:file/file.dart'; diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 1e656b4f75e..5adebe708a6 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.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. -// @dart=2.9 - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/analyze_command.dart'; @@ -14,8 +12,8 @@ import 'mocks.dart'; import 'util.dart'; void main() { - RecordingProcessRunner processRunner; - CommandRunner runner; + late RecordingProcessRunner processRunner; + late CommandRunner runner; setUp(() { initializeFakePackages(); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 7b488879f3b..7a8aa85fc8b 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.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. -// @dart=2.9 - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/build_examples_command.dart'; @@ -15,8 +13,8 @@ import 'util.dart'; void main() { group('test build_example_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; + late CommandRunner runner; + late RecordingProcessRunner processRunner; final String flutterCommand = const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 7905bb279f4..6fb8b8d3355 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.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. -// @dart=2.9 - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common.dart'; @@ -16,8 +14,8 @@ import 'util.dart'; void main() { group('test drive_example_command', () { - CommandRunner runner; - RecordingProcessRunner processRunner; + late CommandRunner runner; + late RecordingProcessRunner processRunner; final String flutterCommand = const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; setUp(() { diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index cfa8d448f37..2ce231651b8 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.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. -// @dart=2.9 - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/java_test_command.dart'; @@ -14,7 +12,7 @@ import 'util.dart'; void main() { group('$JavaTestCommand', () { - CommandRunner runner; + late CommandRunner runner; final RecordingProcessRunner processRunner = RecordingProcessRunner(); setUp(() { diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index a0c74fc6acd..7b49fa8e9ed 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.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. -// @dart=2.9 - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -13,10 +11,10 @@ import 'package:test/test.dart'; void main() { group('$LicenseCheckCommand', () { - CommandRunner runner; - FileSystem fileSystem; - List printedMessages; - Directory root; + late CommandRunner runner; + late FileSystem fileSystem; + late List printedMessages; + late Directory root; setUp(() { fileSystem = MemoryFileSystem(); @@ -28,7 +26,7 @@ void main() { final LicenseCheckCommand command = LicenseCheckCommand( packagesDir, fileSystem, - print: (Object message) => printedMessages.add(message.toString()), + print: (Object? message) => printedMessages.add(message.toString()), ); runner = CommandRunner('license_test', 'Test for $LicenseCheckCommand'); diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index a767671be5c..e97474dedfe 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.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. -// @dart=2.9 - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/list_command.dart'; @@ -13,7 +11,7 @@ import 'util.dart'; void main() { group('$ListCommand', () { - CommandRunner runner; + late CommandRunner runner; setUp(() { initializeFakePackages(); diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index a83ca754380..b984247af9a 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.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. -// @dart=2.9 - import 'dart:async'; import 'dart:io' as io; @@ -35,5 +33,5 @@ class MockIOSink extends Mock implements IOSink { List lines = []; @override - void writeln([Object obj = '']) => lines.add(obj.toString()); + void writeln([Object? obj = '']) => lines.add(obj.toString()); } diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 550249169b9..dc951f6ea9b 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.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. -// @dart=2.9 - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/test_command.dart'; @@ -13,7 +11,7 @@ import 'util.dart'; void main() { group('$TestCommand', () { - CommandRunner runner; + late CommandRunner runner; final RecordingProcessRunner processRunner = RecordingProcessRunner(); setUp(() { From 89fc7cebb3d4454763b60c9e8f45cbedd06c5f6a Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Mon, 17 May 2021 12:13:59 -0700 Subject: [PATCH 048/249] Bump min Android SDK to the version required at runtime (#3894) --- script/tool/lib/src/create_all_plugins_app_command.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 d825703f173..e02ebb70f50 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -81,7 +81,13 @@ class CreateAllPluginsAppCommand extends PluginCommand { final StringBuffer newGradle = StringBuffer(); for (final String line in gradleFile.readAsLinesSync()) { - newGradle.writeln(line); + if (line.contains('minSdkVersion 16')) { + // Android SDK 20 is required by Google maps. + // Android SDK 19 is required by WebView. + newGradle.writeln('minSdkVersion 20'); + } else { + newGradle.writeln(line); + } if (line.contains('defaultConfig {')) { newGradle.writeln(' multiDexEnabled true'); } else if (line.contains('dependencies {')) { From 7cf08e5683bde496f70e0e25744933ab7a18d505 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 20 May 2021 11:27:30 -0700 Subject: [PATCH 049/249] Make run-on-changed-packages flag handle repo-level changes (#3946) Changes to some files (e.g., CI scripts) have the potential to cause failures in any packages, without changes to those packages themselves. This updates the --run-on-changed-packages to consider all packages as changed if any of those files are changed, to avoid issues where a change that changes both some repo-level files and some package-specific files only run presubmit tests on the packages that are directly changed, causing post-submit-only failures. Fixes https://github.com/flutter/flutter/issues/82965 --- script/tool/CHANGELOG.md | 4 +- script/tool/lib/src/common.dart | 42 +++++++++++-- script/tool/pubspec.yaml | 2 +- script/tool/test/common_test.dart | 98 ++++++++++++++++++++++++++++++- 4 files changed, 137 insertions(+), 9 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 5e9ce994683..79e2de01dea 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,8 +1,10 @@ -## NEXT +## 0.1.2 - Add `against-pub` flag for version-check, which allows the command to check version with pub. - Add `machine` flag for publish-check, which replaces outputs to something parsable by machines. - Add `skip-conformation` flag to publish-plugin to allow auto publishing. +- Change `run-on-changed-packages` to consider all packages as changed if any + files have been changed that could affect the entire repository. ## 0.1.1 diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index e975ec1ebeb..8b16559d458 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -203,7 +203,8 @@ abstract class PluginCommand extends Command { argParser.addFlag(_runOnChangedPackagesArg, help: 'Run the command on changed packages/plugins.\n' 'If the $_pluginsArg is specified, this flag is ignored.\n' - 'If no plugins have changed, the command runs on all plugins.\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, @@ -335,7 +336,9 @@ abstract class PluginCommand extends Command { final Set excludedPlugins = Set.from(getStringListArg(_excludeArg)); final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); - if (plugins.isEmpty && runOnChangedPackages) { + if (plugins.isEmpty && + runOnChangedPackages && + !(await _changesRequireFullTest())) { plugins = await _getChangedPackages(); } @@ -458,6 +461,7 @@ abstract class PluginCommand extends Command { return gitVersionFinder; } + // Returns packages that have been changed relative to the git base. Future> _getChangedPackages() async { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); @@ -472,14 +476,40 @@ abstract class PluginCommand extends Command { packages.add(pathComponents[packagesIndex + 1]); } } - if (packages.isNotEmpty) { - final String changedPackages = packages.join(','); - print(changedPackages); - } else { + 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. diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 725c8395b81..13413b50b5d 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.1.1 +version: 0.1.2 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index 477e186aa24..faf04faaeec 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -145,7 +145,103 @@ void main() { test('all plugins should be tested if there are no plugin related changes.', () async { - gitDiffResponse = '.cirrus'; + gitDiffResponse = 'AUTHORS'; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if .cirrus.yml changes.', + () async { + gitDiffResponse = ''' +.cirrus.yml +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if .ci.yaml changes', + () async { + gitDiffResponse = ''' +.ci.yaml +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if anything in .ci/ changes', + () async { + gitDiffResponse = ''' +.ci/Dockerfile +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if anything in script changes.', + () async { + gitDiffResponse = ''' +script/tool_runner.sh +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if the root analysis options change.', + () async { + gitDiffResponse = ''' +analysis_options.yaml +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDirectory: packagesDir); + final Directory plugin2 = + createFakePlugin('plugin2', packagesDirectory: packagesDir); + await runner.run( + ['sample', '--base-sha=master', '--run-on-changed-packages']); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if formatting options change.', + () async { + gitDiffResponse = ''' +.clang-format +packages/plugin1/CHANGELOG +'''; final Directory plugin1 = createFakePlugin('plugin1', packagesDirectory: packagesDir); final Directory plugin2 = From c6a5122ace984909ddabc81645c23b9848e33e78 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 20 May 2021 14:13:47 -0700 Subject: [PATCH 050/249] Remove codesign overrides from xctest command (#3952) Workaround for https://github.com/flutter/flutter/issues/83048 --- script/tool/lib/src/xctest_command.dart | 2 -- script/tool/test/xctest_command_test.dart | 4 ---- 2 files changed, 6 deletions(-) diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 335e0054fc0..2bd8639cd85 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -121,8 +121,6 @@ class XCTestCommand extends PluginCommand { 'Runner', '-destination', destination, - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ]; final String completeTestCommand = diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 53e82ac623e..505730f37a6 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -175,8 +175,6 @@ void main() { 'Runner', '-destination', 'foo_destination', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory2.path), @@ -233,8 +231,6 @@ void main() { 'Runner', '-destination', 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED=NO', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), From 04181cf38cea3948224343cfa25fce3887ea1058 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 24 May 2021 08:39:05 -0700 Subject: [PATCH 051/249] Fix publish-check output (#3953) --- .../tool/lib/src/publish_check_command.dart | 5 ++++- .../tool/test/publish_check_command_test.dart | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 3f90465d4f7..7a080f44207 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -175,7 +175,10 @@ class PublishCheckCommand extends PluginCommand { (List event) { final String output = String.fromCharCodes(event); if (output.isNotEmpty) { - _printImportantStatusMessage(output, isError: true); + // The final result is always printed on stderr, whether success or + // failure. + final bool isError = !output.contains('has 0 warnings'); + _printImportantStatusMessage(output, isError: isError); outputBuffer.write(output); } }, diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 8c34b3313a0..38e8504fa05 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -132,6 +132,26 @@ void main() { expect(runner.run(['publish-check']), throwsA(isA())); }); + test('Success message on stderr is not printed as an error', () async { + createFakePlugin('d'); + + const String publishOutput = 'Package has 0 warnings.'; + + final MockProcess process = MockProcess(); + process.stderrController.add(publishOutput.codeUnits); + process.stdoutController.close(); // ignore: unawaited_futures + process.stderrController.close(); // ignore: unawaited_futures + + process.exitCodeCompleter.complete(0); + + processRunner.processesToReturn.add(process); + + final List output = await runCapturingPrint( + runner, ['publish-check']); + + expect(output, isNot(contains(contains('ERROR:')))); + }); + test( '--machine: Log JSON with status:no-publish and correct human message, if there are no packages need to be published. ', () async { From 544cab7f3917d551a7bbd224dccd85d43d4ddf37 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Mon, 24 May 2021 08:44:06 -0700 Subject: [PATCH 052/249] add a --dart-sdk option to the repo analysis command (#3959) --- script/tool/lib/src/analyze_command.dart | 17 ++++++++++++--- script/tool/test/analyze_command_test.dart | 25 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 0ed6c2caa5c..2892db90bbf 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -19,12 +19,18 @@ class AnalyzeCommand extends PluginCommand { }) : super(packagesDir, fileSystem, processRunner: processRunner) { argParser.addMultiOption(_customAnalysisFlag, help: - 'Directories (comma seperated) that are allowed to have their own analysis options.', + 'Directories (comma separated) that are allowed to have their own analysis options.', defaultsTo: []); + argParser.addOption(_analysisSdk, + valueHelp: 'dart-sdk', + help: 'An optional path to a Dart SDK; this is used to override the ' + 'SDK used to provide analysis.'); } static const String _customAnalysisFlag = 'custom-analysis'; + static const String _analysisSdk = 'analysis-sdk'; + @override final String name = 'analyze'; @@ -62,15 +68,20 @@ class AnalyzeCommand extends PluginCommand { await processRunner.runAndStream('flutter', ['packages', 'get'], workingDir: package, exitOnError: true); } else { - await processRunner.runAndStream('pub', ['get'], + await processRunner.runAndStream('dart', ['pub', 'get'], workingDir: package, exitOnError: true); } } + // Use the Dart SDK override if one was passed in. + final String? dartSdk = argResults![_analysisSdk] as String?; + final String dartBinary = + dartSdk == null ? 'dart' : p.join(dartSdk, 'bin', 'dart'); + final List failingPackages = []; await for (final Directory package in getPlugins()) { final int exitCode = await processRunner.runAndStream( - 'dart', ['analyze', '--fatal-infos'], + dartBinary, ['analyze', '--fatal-infos'], workingDir: package); if (exitCode != 0) { failingPackages.add(p.basename(package.path)); diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 5adebe708a6..5e548cb27a9 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -53,6 +53,31 @@ void main() { ])); }); + test('uses a separate analysis sdk', () async { + final Directory pluginDir = createFakePlugin('a'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze', '--analysis-sdk', 'foo/bar/baz']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['packages', 'get'], + pluginDir.path, + ), + ProcessCall( + 'foo/bar/baz/bin/dart', + const ['analyze', '--fatal-infos'], + pluginDir.path, + ), + ]), + ); + }); + group('verifies analysis settings', () { test('fails analysis_options.yaml', () async { createFakePlugin('foo', withExtraFiles: >[ From b07809952c19c727c6b6f4d2d6341545d55e2def Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 27 May 2021 07:10:14 -0700 Subject: [PATCH 053/249] Allow reverts when checking versions (#3981) If a new version is lower than the current checked-in version, and the transition from the new version to the old version is valid (indicating that it could have been the previous version), allow the change. This prevents the version check from failing when reverting a PR. This is not fool-proof (e.g., it would allow a revert of an already-published PR); if we have issues in practice we can further restrict the check (by checking that the new version is the current pub version, for instance). --- script/tool/CHANGELOG.md | 6 ++++ .../tool/lib/src/version_check_command.dart | 14 ++++++++ script/tool/pubspec.yaml | 2 +- script/tool/test/version_check_test.dart | 34 +++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 79e2de01dea..d7e58dcfba5 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.3 + +- Cosmetic fix to `publish-check` output +- Add a --dart-sdk option to `analyze` +- Allow reverts in `version-check` + ## 0.1.2 - Add `against-pub` flag for version-check, which allows the command to check version with pub. diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index a802c4d425e..c095273a772 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -179,6 +179,20 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} continue; } + // Check for reverts when doing local validation. + if (!getBoolArg(_againstPubFlag) && headVersion < sourceVersion) { + final Map possibleVersionsFromNewVersion = + getAllowedNextVersions(headVersion, sourceVersion); + // Since this skips validation, try to ensure that it really is likely + // to be a revert rather than a typo by checking that the transition + // from the lower version to the new version would have been valid. + if (possibleVersionsFromNewVersion.containsKey(sourceVersion)) { + print('${indentation}New version is lower than previous version. ' + 'This is assumed to be a revert.'); + continue; + } + } + final Map allowedNextVersions = getAllowedNextVersions(sourceVersion, headVersion); diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 13413b50b5d..8ea735f2b67 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.1.2 +version: 0.1.3 dependencies: args: ^2.1.0 diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index cef2ab1cfad..536b885dd57 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -187,6 +187,40 @@ void main() { ); }); + test('allows likely reverts.', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.6.1', + }; + final List output = + await runCapturingPrint(runner, ['version-check']); + + expect( + output, + containsAllInOrder([ + '${indentation}New version is lower than previous version. This is assumed to be a revert.', + ]), + ); + }); + + test('denies lower version that could not be a simple revert', () async { + createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.5.1', + }; + final Future> result = + runCapturingPrint(runner, ['version-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + test('denies invalid version without explicit base-sha', () async { createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; From 6a8e6da0285947c780e21bfc571a4f821d4638d2 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Thu, 27 May 2021 07:38:26 -0700 Subject: [PATCH 054/249] use 'flutter pub get' for both dart and flutter packages (#3973) --- script/tool/lib/src/analyze_command.dart | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 2892db90bbf..b36c43e6b96 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -63,14 +63,10 @@ class AnalyzeCommand extends PluginCommand { throw ToolExit(1); } - await for (final Directory package in getPackages()) { - if (isFlutterPackage(package, fileSystem)) { - await processRunner.runAndStream('flutter', ['packages', 'get'], - workingDir: package, exitOnError: true); - } else { - await processRunner.runAndStream('dart', ['pub', 'get'], - workingDir: package, exitOnError: true); - } + final List packageDirectories = await getPackages().toList(); + for (final Directory package in packageDirectories) { + await processRunner.runAndStream('flutter', ['packages', 'get'], + workingDir: package, exitOnError: true); } // Use the Dart SDK override if one was passed in. @@ -79,7 +75,8 @@ class AnalyzeCommand extends PluginCommand { dartSdk == null ? 'dart' : p.join(dartSdk, 'bin', 'dart'); final List failingPackages = []; - await for (final Directory package in getPlugins()) { + final List pluginDirectories = await getPlugins().toList(); + for (final Directory package in pluginDirectories) { final int exitCode = await processRunner.runAndStream( dartBinary, ['analyze', '--fatal-infos'], workingDir: package); From 93047fff2fa0bd9cdfdac31f492e83a47d95af0e Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Thu, 27 May 2021 11:54:03 -0700 Subject: [PATCH 055/249] [script/tool] speed up the pub get portion of the analyze command (#3982) --- script/tool/lib/src/analyze_command.dart | 10 ++++++ script/tool/test/analyze_command_test.dart | 41 ++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index b36c43e6b96..872645ec16c 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -41,6 +41,7 @@ class AnalyzeCommand extends PluginCommand { @override Future run() async { print('Verifying analysis settings...'); + final List files = packagesDir.listSync(recursive: true); for (final FileSystemEntity file in files) { if (file.basename != 'analysis_options.yaml' && @@ -64,6 +65,14 @@ class AnalyzeCommand extends PluginCommand { } final List packageDirectories = await getPackages().toList(); + final Set packagePaths = + packageDirectories.map((Directory dir) => dir.path).toSet(); + packageDirectories.removeWhere((Directory directory) { + // We remove the 'example' subdirectories - 'flutter pub get' automatically + // runs 'pub get' there as part of handling the parent directory. + return directory.basename == 'example' && + packagePaths.contains(directory.parent.path); + }); for (final Directory package in packageDirectories) { await processRunner.runAndStream('flutter', ['packages', 'get'], workingDir: package, exitOnError: true); @@ -86,6 +95,7 @@ class AnalyzeCommand extends PluginCommand { } print('\n\n'); + if (failingPackages.isNotEmpty) { print('The following packages have analyzer errors (see above):'); for (final String package in failingPackages) { diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 5e548cb27a9..b536c2bb989 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -53,6 +53,47 @@ void main() { ])); }); + test('skips flutter pub get for examples', () async { + final Directory plugin1Dir = createFakePlugin('a', withSingleExample: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin1Dir.path), + ])); + }); + + test('don\'t elide a non-contained example package', () async { + final Directory plugin1Dir = createFakePlugin('a'); + final Directory plugin2Dir = createFakePlugin('example'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['packages', 'get'], plugin2Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin1Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin2Dir.path), + ])); + }); + test('uses a separate analysis sdk', () async { final Directory pluginDir = createFakePlugin('a'); From d21b1d9706232eccb3173f61021100de6f979be7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 27 May 2021 20:54:06 -0700 Subject: [PATCH 056/249] Add pubspec convention checks (#3984) --- script/tool/CHANGELOG.md | 4 + script/tool/lib/src/main.dart | 15 +- .../tool/lib/src/pubspec_check_command.dart | 178 ++++++++++ script/tool/pubspec.yaml | 2 +- .../test/build_examples_command_test.dart | 14 - .../test/drive_examples_command_test.dart | 14 - .../tool/test/pubspec_check_command_test.dart | 334 ++++++++++++++++++ 7 files changed, 529 insertions(+), 32 deletions(-) create mode 100644 script/tool/lib/src/pubspec_check_command.dart create mode 100644 script/tool/test/pubspec_check_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index d7e58dcfba5..6250e2a7273 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4 + +- Add a `pubspec-check` command + ## 0.1.3 - Cosmetic fix to `publish-check` output diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 4b0e85704b2..a3fbc34ae8a 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -9,8 +9,6 @@ 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/publish_check_command.dart'; -import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; import 'package:path/path.dart' as p; import 'analyze_command.dart'; @@ -24,6 +22,9 @@ import 'java_test_command.dart'; import 'license_check_command.dart'; import 'lint_podspecs_command.dart'; import 'list_command.dart'; +import 'publish_check_command.dart'; +import 'publish_plugin_command.dart'; +import 'pubspec_check_command.dart'; import 'test_command.dart'; import 'version_check_command.dart'; import 'xctest_command.dart'; @@ -58,12 +59,20 @@ void main(List args) { ..addCommand(ListCommand(packagesDir, fileSystem)) ..addCommand(PublishCheckCommand(packagesDir, fileSystem)) ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) + ..addCommand(PubspecCheckCommand(packagesDir, fileSystem)) ..addCommand(TestCommand(packagesDir, fileSystem)) ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) ..addCommand(XCTestCommand(packagesDir, fileSystem)); commandRunner.run(args).catchError((Object e) { final ToolExit toolExit = e as ToolExit; - io.exit(toolExit.exitCode); + int exitCode = toolExit.exitCode; + // This should never happen; this check is here to guarantee that a ToolExit + // never accidentally has code 0 thus causing CI to pass. + if (exitCode == 0) { + assert(false); + exitCode = 255; + } + io.exit(exitCode); }, test: (Object e) => e is ToolExit); } diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart new file mode 100644 index 00000000000..fadcfbc56de --- /dev/null +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -0,0 +1,178 @@ +// 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 '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'; + +/// A command to enforce pubspec conventions across the repository. +/// +/// This both ensures that repo best practices for which optional fields are +/// used are followed, and that the structure is consistent to make edits +/// across multiple pubspec files easier. +class PubspecCheckCommand extends PluginCommand { + /// Creates an instance of the version check command. + PubspecCheckCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + GitDir? gitDir, + }) : super(packagesDir, fileSystem, + processRunner: processRunner, gitDir: gitDir); + + // Section order for plugins. Because the 'flutter' section is critical + // information for plugins, and usually small, it goes near the top unlike in + // a normal app or package. + static const List _majorPluginSections = [ + 'environment:', + 'flutter:', + 'dependencies:', + 'dev_dependencies:', + ]; + + static const List _majorPackageSections = [ + 'environment:', + 'dependencies:', + 'dev_dependencies:', + 'flutter:', + ]; + + static const String _expectedIssueLinkFormat = + 'https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A'; + + @override + final String name = 'pubspec-check'; + + @override + final String description = + 'Checks that pubspecs follow repository conventions.'; + + @override + Future run() async { + final List failingPackages = []; + await for (final Directory package in getPackages()) { + final String relativePackagePath = + p.relative(package.path, from: packagesDir.path); + print('Checking $relativePackagePath...'); + final File pubspec = package.childFile('pubspec.yaml'); + final bool passesCheck = !pubspec.existsSync() || + await _checkPubspec(pubspec, packageName: package.basename); + if (!passesCheck) { + failingPackages.add(relativePackagePath); + } + } + + if (failingPackages.isNotEmpty) { + print('The following packages have pubspec issues:'); + for (final String package in failingPackages) { + print(' $package'); + } + throw ToolExit(1); + } + + print('\nNo pubspec issues found!'); + } + + Future _checkPubspec( + File pubspecFile, { + required String packageName, + }) async { + const String indentation = ' '; + final String contents = pubspecFile.readAsStringSync(); + final Pubspec? pubspec = _tryParsePubspec(contents); + if (pubspec == null) { + return false; + } + + final List pubspecLines = contents.split('\n'); + final List sectionOrder = pubspecLines.contains(' plugin:') + ? _majorPluginSections + : _majorPackageSections; + bool passing = _checkSectionOrder(pubspecLines, sectionOrder); + if (!passing) { + print('${indentation}Major sections should follow standard ' + 'repository ordering:'); + final String listIndentation = indentation * 2; + print('$listIndentation${sectionOrder.join('\n$listIndentation')}'); + } + + if (pubspec.publishTo != 'none') { + final List repositoryErrors = + _checkForRepositoryLinkErrors(pubspec, packageName: packageName); + if (repositoryErrors.isNotEmpty) { + for (final String error in repositoryErrors) { + print('$indentation$error'); + } + passing = false; + } + + if (!_checkIssueLink(pubspec)) { + print( + '${indentation}A package should have an "issue_tracker" link to a ' + 'search for open flutter/flutter bugs with the relevant label:\n' + '${indentation * 2}$_expectedIssueLinkFormat'); + passing = false; + } + } + + return passing; + } + + Pubspec? _tryParsePubspec(String pubspecContents) { + try { + return Pubspec.parse(pubspecContents); + } on Exception catch (exception) { + print(' Cannot parse pubspec.yaml: $exception'); + } + return null; + } + + bool _checkSectionOrder( + List pubspecLines, List sectionOrder) { + int previousSectionIndex = 0; + for (final String line in pubspecLines) { + final int index = sectionOrder.indexOf(line); + if (index == -1) { + continue; + } + if (index < previousSectionIndex) { + return false; + } + previousSectionIndex = index; + } + return true; + } + + List _checkForRepositoryLinkErrors( + Pubspec pubspec, { + required String packageName, + }) { + final List errorMessages = []; + if (pubspec.repository == null) { + errorMessages.add('Missing "repository"'); + } else if (!pubspec.repository!.path.endsWith(packageName)) { + errorMessages + .add('The "repository" link should end with the package name.'); + } + + if (pubspec.homepage != null) { + errorMessages + .add('Found a "homepage" entry; only "repository" should be used.'); + } + + return errorMessages; + } + + bool _checkIssueLink(Pubspec pubspec) { + return pubspec.issueTracker + ?.toString() + .startsWith(_expectedIssueLinkFormat) == + true; + } +} diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 8ea735f2b67..ab422daf8ed 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.1.3 +version: 0.1.4 dependencies: args: ^2.1.0 diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 7a8aa85fc8b..44634c25175 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -59,7 +59,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); @@ -96,7 +95,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -142,7 +140,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running build-examples --linux with no // Linux implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); @@ -175,7 +172,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -211,7 +207,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); @@ -245,7 +240,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -280,7 +274,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); @@ -314,7 +307,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -353,7 +345,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); @@ -386,7 +377,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -425,7 +415,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); @@ -462,7 +451,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -492,7 +480,6 @@ void main() { '--enable-experiment=exp1' ]); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -522,7 +509,6 @@ void main() { '--no-macos', '--enable-experiment=exp1' ]); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 6fb8b8d3355..b4d6a25154c 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -63,7 +63,6 @@ void main() { final String deviceTestPath = p.join('test', 'plugin.dart'); final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -109,7 +108,6 @@ void main() { final String deviceTestPath = p.join('test_driver', 'plugin.dart'); final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -197,7 +195,6 @@ void main() { final String driverTestPath = p.join('test_driver', 'integration_test.dart'); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -252,7 +249,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running drive-examples --linux on a non-Linux // plugin is a no-op. expect(processRunner.recordedCalls, []); @@ -287,7 +283,6 @@ void main() { final String deviceTestPath = p.join('test_driver', 'plugin.dart'); final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -332,7 +327,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running drive-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, []); @@ -367,7 +361,6 @@ void main() { final String deviceTestPath = p.join('test_driver', 'plugin.dart'); final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -414,7 +407,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running drive-examples --web on a non-web // plugin is a no-op. expect(processRunner.recordedCalls, []); @@ -449,7 +441,6 @@ void main() { final String deviceTestPath = p.join('test_driver', 'plugin.dart'); final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -498,7 +489,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running drive-examples --windows on a // non-Windows plugin is a no-op. expect(processRunner.recordedCalls, []); @@ -533,7 +523,6 @@ void main() { final String deviceTestPath = p.join('test_driver', 'plugin.dart'); final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -579,7 +568,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running drive-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, []); @@ -600,7 +588,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running drive-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, []); @@ -627,7 +614,6 @@ void main() { final String deviceTestPath = p.join('test', 'plugin.dart'); final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart new file mode 100644 index 00000000000..aee3f7a2031 --- /dev/null +++ b/script/tool/test/pubspec_check_command_test.dart @@ -0,0 +1,334 @@ +// 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: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/pubspec_check_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test pubspec_check_command', () { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + initializeFakePackages(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final PubspecCheckCommand command = PubspecCheckCommand( + packagesDir, fileSystem, + processRunner: processRunner); + + runner = CommandRunner( + 'pubspec_check_command', 'Test for pubspec_check_command'); + runner.addCommand(command); + }); + + String headerSection( + String name, { + bool isPlugin = false, + bool includeRepository = true, + bool includeHomepage = false, + bool includeIssueTracker = true, + }) { + final String repoLink = 'https://github.com/flutter/' + '${isPlugin ? 'plugins' : 'packages'}/tree/master/packages/$name'; + final String issueTrackerLink = + 'https://github.com/flutter/flutter/issues?' + 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; + return ''' +name: $name +${includeRepository ? 'repository: $repoLink' : ''} +${includeHomepage ? 'homepage: $repoLink' : ''} +${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} +version: 1.0.0 +'''; + } + + String environmentSection() { + return ''' +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" +'''; + } + + String flutterSection({bool isPlugin = false}) { + const String pluginEntry = ''' + plugin: + platforms: +'''; + return ''' +flutter: +${isPlugin ? pluginEntry : ''} +'''; + } + + String dependenciesSection() { + return ''' +dependencies: + flutter: + sdk: flutter +'''; + } + + String devDependenciesSection() { + return ''' +dev_dependencies: + flutter_test: + sdk: flutter +'''; + } + + test('passes for a plugin following conventions', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + ]); + + expect( + output, + containsAllInOrder([ + 'Checking plugin...', + 'Checking plugin/example...', + '\nNo pubspec issues found!', + ]), + ); + }); + + test('passes for a Flutter package following conventions', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin')} +${environmentSection()} +${dependenciesSection()} +${devDependenciesSection()} +${flutterSection()} +'''); + + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + ]); + + expect( + output, + containsAllInOrder([ + 'Checking plugin...', + 'Checking plugin/example...', + '\nNo pubspec issues found!', + ]), + ); + }); + + test('passes for a minimal package following conventions', () async { + final Directory packageDirectory = packagesDir.childDirectory('package'); + packageDirectory.createSync(recursive: true); + + packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('package')} +${environmentSection()} +${dependenciesSection()} +'''); + + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + ]); + + expect( + output, + containsAllInOrder([ + 'Checking package...', + '\nNo pubspec issues found!', + ]), + ); + }); + + test('fails when homepage is included', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeHomepage: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when repository is missing', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeRepository: false)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when homepage is given instead of repository', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when issue tracker is missing', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeIssueTracker: false)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when environment section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +${environmentSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when flutter section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${flutterSection(isPlugin: true)} +${environmentSection()} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when dependencies section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${devDependenciesSection()} +${dependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when devDependencies section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', + withSingleExample: true, packagesDirectory: packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${devDependenciesSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + }); +} From 407572bc04f2235c81748884302dd222dcccb239 Mon Sep 17 00:00:00 2001 From: "A.J. Gardner" Date: Tue, 1 Jun 2021 12:17:21 -0400 Subject: [PATCH 057/249] [script/tool] Use 'dart pub' instead of deprecated 'pub' (#3991) --- script/tool/lib/src/test_command.dart | 7 ++++--- script/tool/test/test_command_test.dart | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 7455095effc..fafa0f55a22 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -66,14 +66,15 @@ class TestCommand extends PluginCommand { ); } else { exitCode = await processRunner.runAndStream( - 'pub', - ['get'], + 'dart', + ['pub', 'get'], workingDir: packageDir, ); if (exitCode == 0) { exitCode = await processRunner.runAndStream( - 'pub', + 'dart', [ + 'pub', 'run', if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index dc951f6ea9b..1c7118abc61 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -94,10 +94,10 @@ void main() { 'flutter', const ['test', '--color', '--enable-experiment=exp1'], plugin1Dir.path), - ProcessCall('pub', const ['get'], plugin2Dir.path), + ProcessCall('dart', const ['pub', 'get'], plugin2Dir.path), ProcessCall( - 'pub', - const ['run', '--enable-experiment=exp1', 'test'], + 'dart', + const ['pub', 'run', '--enable-experiment=exp1', 'test'], plugin2Dir.path), ]), ); @@ -149,10 +149,10 @@ void main() { 'flutter', const ['test', '--color', '--enable-experiment=exp1'], plugin1Dir.path), - ProcessCall('pub', const ['get'], plugin2Dir.path), + ProcessCall('dart', const ['pub', 'get'], plugin2Dir.path), ProcessCall( - 'pub', - const ['run', '--enable-experiment=exp1', 'test'], + 'dart', + const ['pub', 'run', '--enable-experiment=exp1', 'test'], plugin2Dir.path), ]), ); From 9c15a2b4ce2d363dad38786c02dbe71944bb5de0 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 2 Jun 2021 17:40:17 +0200 Subject: [PATCH 058/249] Some small documentation fixes (#3999) * Two small documentation fixes * Fix url to plugin_tool format --- script/tool/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/tool/README.md b/script/tool/README.md index c142b66178b..3e9484c3ff0 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -58,7 +58,7 @@ Note that the `plugins` argument, despite the name, applies to any package. ```sh cd -dart run /script/tool/lib/src/main.dart format --plugins plugin_name +dart run ./script/tool/lib/src/main.dart format --plugins plugin_name ``` ### Run the Dart Static Analyzer From 612be3575b7656bda7004207dab6a1795f5023ec Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Fri, 4 Jun 2021 10:24:05 -0700 Subject: [PATCH 059/249] Remove "unnecessary" imports. (#4012) --- script/tool/test/publish_plugin_command_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 357e72efa00..3618cc38bc8 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -14,7 +14,6 @@ import 'package:file/local.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; import 'package:git/git.dart'; -import 'package:matcher/matcher.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; From 533596f798de991334a53ee77a675bf546c480f2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 4 Jun 2021 16:48:31 -0700 Subject: [PATCH 060/249] Enable linting of macOS podspecs (#4015) - Removes the .cirrus.yml workaround that removed macOS podspecs before linting - Deletes the dummy macOS podspecs that are no longer needed due to `flutter` fixes - Adds --use-modular-headers to the lint command to reflect what Flutter Podfiles do - Fix the actual issues in the podspecs --- script/tool/lib/src/lint_podspecs_command.dart | 1 + script/tool/test/lint_podspecs_command_test.dart | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 0bf4c69b24f..5c66373ac6f 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -132,6 +132,7 @@ class LintPodspecsCommand extends PluginCommand { podspecPath, '--configuration=Debug', // Release targets unsupported arm64 simulators. Use Debug to only build against targeted x86_64 simulator devices. '--skip-tests', + '--use-modular-headers', // Flutter sets use_modular_headers! in its templates. if (allowWarnings) '--allow-warnings', if (libraryLint) '--use-libraries' ]; diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 13bb1cc8d32..fad750393a7 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -87,6 +87,7 @@ void main() { p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), '--configuration=Debug', '--skip-tests', + '--use-modular-headers', '--use-libraries' ], mockPackagesDir.path), @@ -98,6 +99,7 @@ void main() { p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), '--configuration=Debug', '--skip-tests', + '--use-modular-headers', ], mockPackagesDir.path), ]), @@ -147,6 +149,7 @@ void main() { p.join(plugin1Dir.path, 'plugin1.podspec'), '--configuration=Debug', '--skip-tests', + '--use-modular-headers', '--allow-warnings', '--use-libraries' ], @@ -159,6 +162,7 @@ void main() { p.join(plugin1Dir.path, 'plugin1.podspec'), '--configuration=Debug', '--skip-tests', + '--use-modular-headers', '--allow-warnings', ], mockPackagesDir.path), From bb0a1ea16131e8835c7c4299aab005bb73184a24 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sat, 5 Jun 2021 10:32:24 -0700 Subject: [PATCH 061/249] [flutter_plugin_tools] Simplify filesystem usage (#4014) - Replaces most explicit use of `fileSystem` with path construction using the `child*` utility methods - Removes explicit passing of a filesystem to the commands; we're already passing a `Directory` for the root where the tool operates, and we should never be using a different filesystem than that directory's filesystem, so passing it was both redundant, and a potential source of test bugs. --- script/tool/lib/src/analyze_command.dart | 5 +- .../tool/lib/src/build_examples_command.dart | 17 +++--- script/tool/lib/src/common.dart | 54 ++++++++----------- .../src/create_all_plugins_app_command.dart | 11 ++-- .../tool/lib/src/drive_examples_command.dart | 40 +++++++------- .../lib/src/firebase_test_lab_command.dart | 28 +++++----- script/tool/lib/src/format_command.dart | 8 +-- script/tool/lib/src/java_test_command.dart | 27 +++++----- .../tool/lib/src/license_check_command.dart | 5 +- .../tool/lib/src/lint_podspecs_command.dart | 5 +- script/tool/lib/src/list_command.dart | 3 +- script/tool/lib/src/main.dart | 39 +++++++------- .../tool/lib/src/publish_check_command.dart | 5 +- .../tool/lib/src/publish_plugin_command.dart | 25 ++++----- .../tool/lib/src/pubspec_check_command.dart | 6 +-- script/tool/lib/src/test_command.dart | 11 ++-- .../tool/lib/src/version_check_command.dart | 8 ++- script/tool/lib/src/xctest_command.dart | 7 ++- script/tool/test/analyze_command_test.dart | 5 +- .../test/build_examples_command_test.dart | 5 +- script/tool/test/common_test.dart | 16 ++---- .../create_all_plugins_app_command_test.dart | 1 - .../test/drive_examples_command_test.dart | 5 +- script/tool/test/firebase_test_lab_test.dart | 2 +- script/tool/test/java_test_command_test.dart | 5 +- .../tool/test/license_check_command_test.dart | 1 - .../tool/test/lint_podspecs_command_test.dart | 1 - script/tool/test/list_command_test.dart | 2 +- .../tool/test/publish_check_command_test.dart | 18 +++---- .../test/publish_plugin_command_test.dart | 5 +- .../tool/test/pubspec_check_command_test.dart | 5 +- script/tool/test/test_command_test.dart | 4 +- script/tool/test/version_check_test.dart | 24 +++------ script/tool/test/xctest_command_test.dart | 5 +- 34 files changed, 177 insertions(+), 231 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 872645ec16c..076c8f69885 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -13,10 +13,9 @@ import 'common.dart'; class AnalyzeCommand extends PluginCommand { /// Creates a analysis command instance. AnalyzeCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { + }) : super(packagesDir, processRunner: processRunner) { argParser.addMultiOption(_customAnalysisFlag, help: 'Directories (comma separated) that are allowed to have their own analysis options.', diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 75bff3b25dc..82fb12e70d4 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -15,10 +15,9 @@ import 'common.dart'; class BuildExamplesCommand extends PluginCommand { /// Creates an instance of the build command. BuildExamplesCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { + }) : super(packagesDir, processRunner: processRunner) { argParser.addFlag(kLinux, defaultsTo: false); argParser.addFlag(kMacos, defaultsTo: false); argParser.addFlag(kWeb, defaultsTo: false); @@ -69,7 +68,7 @@ class BuildExamplesCommand extends PluginCommand { if (getBoolArg(kLinux)) { print('\nBUILDING Linux for $packageName'); - if (isLinuxPlugin(plugin, fileSystem)) { + if (isLinuxPlugin(plugin)) { final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ @@ -89,7 +88,7 @@ class BuildExamplesCommand extends PluginCommand { if (getBoolArg(kMacos)) { print('\nBUILDING macOS for $packageName'); - if (isMacOsPlugin(plugin, fileSystem)) { + if (isMacOsPlugin(plugin)) { final int exitCode = await processRunner.runAndStream( flutterCommand, [ @@ -109,7 +108,7 @@ class BuildExamplesCommand extends PluginCommand { if (getBoolArg(kWeb)) { print('\nBUILDING web for $packageName'); - if (isWebPlugin(plugin, fileSystem)) { + if (isWebPlugin(plugin)) { final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ @@ -129,7 +128,7 @@ class BuildExamplesCommand extends PluginCommand { if (getBoolArg(kWindows)) { print('\nBUILDING Windows for $packageName'); - if (isWindowsPlugin(plugin, fileSystem)) { + if (isWindowsPlugin(plugin)) { final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ @@ -149,7 +148,7 @@ class BuildExamplesCommand extends PluginCommand { if (getBoolArg(kIpa)) { print('\nBUILDING IPA for $packageName'); - if (isIosPlugin(plugin, fileSystem)) { + if (isIosPlugin(plugin)) { final int exitCode = await processRunner.runAndStream( flutterCommand, [ @@ -170,7 +169,7 @@ class BuildExamplesCommand extends PluginCommand { if (getBoolArg(kApk)) { print('\nBUILDING APK for $packageName'); - if (isAndroidPlugin(plugin, fileSystem)) { + if (isAndroidPlugin(plugin)) { final int exitCode = await processRunner.runAndStream( flutterCommand, [ diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index 8b16559d458..f58290b5e07 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -48,14 +48,13 @@ const String kApk = 'apk'; const String kEnableExperiment = 'enable-experiment'; /// Returns whether the given directory contains a Flutter package. -bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { +bool isFlutterPackage(FileSystemEntity entity) { if (entity is! Directory) { return false; } try { - final File pubspecFile = - fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final File pubspecFile = entity.childFile('pubspec.yaml'); final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; final YamlMap? dependencies = pubspecYaml['dependencies'] as YamlMap?; @@ -78,8 +77,7 @@ bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { /// plugin: /// platforms: /// [platform]: -bool pluginSupportsPlatform( - String platform, FileSystemEntity entity, FileSystem fileSystem) { +bool pluginSupportsPlatform(String platform, FileSystemEntity entity) { assert(platform == kIos || platform == kAndroid || platform == kWeb || @@ -91,8 +89,7 @@ bool pluginSupportsPlatform( } try { - final File pubspecFile = - fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final File pubspecFile = entity.childFile('pubspec.yaml'); final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; @@ -120,33 +117,33 @@ bool pluginSupportsPlatform( } /// Returns whether the given directory contains a Flutter Android plugin. -bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kAndroid, entity, fileSystem); +bool isAndroidPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kAndroid, entity); } /// Returns whether the given directory contains a Flutter iOS plugin. -bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kIos, entity, fileSystem); +bool isIosPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kIos, entity); } /// Returns whether the given directory contains a Flutter web plugin. -bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kWeb, entity, fileSystem); +bool isWebPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kWeb, entity); } /// Returns whether the given directory contains a Flutter Windows plugin. -bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kWindows, entity, fileSystem); +bool isWindowsPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kWindows, entity); } /// Returns whether the given directory contains a Flutter macOS plugin. -bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kMacos, entity, fileSystem); +bool isMacOsPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kMacos, entity); } /// Returns whether the given directory contains a Flutter linux plugin. -bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { - return pluginSupportsPlatform(kLinux, entity, fileSystem); +bool isLinuxPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kLinux, entity); } /// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red. @@ -169,8 +166,7 @@ class ToolExit extends Error { abstract class PluginCommand extends Command { /// Creates a command to operate on [packagesDir] with the given environment. PluginCommand( - this.packagesDir, - this.fileSystem, { + this.packagesDir, { this.processRunner = const ProcessRunner(), this.gitDir, }) { @@ -223,11 +219,6 @@ abstract class PluginCommand extends Command { /// The directory containing the plugin packages. final Directory packagesDir; - /// The file system. - /// - /// This can be overridden for testing. - final FileSystem fileSystem; - /// The process runner. /// /// This can be overridden for testing. @@ -414,19 +405,17 @@ abstract class PluginCommand extends Command { /// Returns whether the specified entity is a directory containing a /// `pubspec.yaml` file. bool _isDartPackage(FileSystemEntity entity) { - return entity is Directory && - fileSystem.file(p.join(entity.path, 'pubspec.yaml')).existsSync(); + 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 = - fileSystem.directory(p.join(plugin.path, 'example')); + final Directory exampleFolder = plugin.childDirectory('example'); if (!exampleFolder.existsSync()) { return []; } - if (isFlutterPackage(exampleFolder, fileSystem)) { + if (isFlutterPackage(exampleFolder)) { return [exampleFolder]; } // Only look at the subdirectories of the example directory if the example @@ -434,8 +423,7 @@ abstract class PluginCommand extends Command { // example directory for other dart packages. return exampleFolder .listSync() - .where( - (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) + .where((FileSystemEntity entity) => isFlutterPackage(entity)) .cast(); } 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 e02ebb70f50..9de7f1b904a 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,6 @@ import 'dart:async'; import 'dart:io' as io; import 'package:file/file.dart'; -import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; @@ -18,11 +17,10 @@ import 'common.dart'; class CreateAllPluginsAppCommand extends PluginCommand { /// Creates an instance of the builder command. CreateAllPluginsAppCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { this.pluginsRoot, - }) : super(packagesDir, fileSystem) { - pluginsRoot ??= fileSystem.currentDirectory; + }) : super(packagesDir) { + pluginsRoot ??= packagesDir.fileSystem.currentDirectory; appDirectory = pluginsRoot.childDirectory('all_plugins'); } @@ -161,8 +159,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { await for (final Directory package in getPlugins()) { final String pluginName = package.path.split('/').last; - final File pubspecFile = - fileSystem.file(p.join(package.path, 'pubspec.yaml')); + final File pubspecFile = package.childFile('pubspec.yaml'); final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec.publishTo != 'none') { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 15f465f5359..4678a9de3f1 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -12,10 +12,9 @@ import 'common.dart'; class DriveExamplesCommand extends PluginCommand { /// Creates an instance of the drive command. DriveExamplesCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { + }) : super(packagesDir, processRunner: processRunner) { argParser.addFlag(kAndroid, help: 'Runs the Android implementation of the examples'); argParser.addFlag(kIos, @@ -67,7 +66,7 @@ class DriveExamplesCommand extends PluginCommand { continue; } print('\n==========\nChecking $pluginName...'); - if (!(await _pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { + if (!(await _pluginSupportedOnCurrentPlatform(plugin))) { print('Not supported for the target platform; skipping.'); continue; } @@ -79,8 +78,7 @@ class DriveExamplesCommand extends PluginCommand { ++examplesFound; final String packageName = p.relative(example.path, from: packagesDir.path); - final Directory driverTests = - fileSystem.directory(p.join(example.path, 'test_driver')); + final Directory driverTests = example.childDirectory('test_driver'); if (!driverTests.existsSync()) { print('No driver tests found for $packageName'); continue; @@ -98,7 +96,7 @@ class DriveExamplesCommand extends PluginCommand { '.dart', ); String deviceTestPath = p.join('test', deviceTestName); - if (!fileSystem + if (!example.fileSystem .file(p.join(example.path, deviceTestPath)) .existsSync()) { // If the app isn't in test/ folder, look in test_driver/ instead. @@ -106,13 +104,13 @@ class DriveExamplesCommand extends PluginCommand { } final List targetPaths = []; - if (fileSystem + if (example.fileSystem .file(p.join(example.path, deviceTestPath)) .existsSync()) { targetPaths.add(deviceTestPath); } else { final Directory integrationTests = - fileSystem.directory(p.join(example.path, 'integration_test')); + example.childDirectory('integration_test'); if (await integrationTests.exists()) { await for (final FileSystemEntity integrationTest @@ -145,19 +143,19 @@ Tried searching for the following: driveArgs.add('--enable-experiment=$enableExperiment'); } - if (isLinux && isLinuxPlugin(plugin, fileSystem)) { + if (isLinux && isLinuxPlugin(plugin)) { driveArgs.addAll([ '-d', 'linux', ]); } - if (isMacos && isMacOsPlugin(plugin, fileSystem)) { + if (isMacos && isMacOsPlugin(plugin)) { driveArgs.addAll([ '-d', 'macos', ]); } - if (isWeb && isWebPlugin(plugin, fileSystem)) { + if (isWeb && isWebPlugin(plugin)) { driveArgs.addAll([ '-d', 'web-server', @@ -165,7 +163,7 @@ Tried searching for the following: '--browser-name=chrome', ]); } - if (isWindows && isWindowsPlugin(plugin, fileSystem)) { + if (isWindows && isWindowsPlugin(plugin)) { driveArgs.addAll([ '-d', 'windows', @@ -220,7 +218,7 @@ Tried searching for the following: } Future _pluginSupportedOnCurrentPlatform( - FileSystemEntity plugin, FileSystem fileSystem) async { + FileSystemEntity plugin) async { final bool isAndroid = getBoolArg(kAndroid); final bool isIOS = getBoolArg(kIos); final bool isLinux = getBoolArg(kLinux); @@ -228,27 +226,27 @@ Tried searching for the following: final bool isWeb = getBoolArg(kWeb); final bool isWindows = getBoolArg(kWindows); if (isAndroid) { - return isAndroidPlugin(plugin, fileSystem); + return isAndroidPlugin(plugin); } if (isIOS) { - return isIosPlugin(plugin, fileSystem); + return isIosPlugin(plugin); } if (isLinux) { - return isLinuxPlugin(plugin, fileSystem); + return isLinuxPlugin(plugin); } if (isMacos) { - return isMacOsPlugin(plugin, fileSystem); + return isMacOsPlugin(plugin); } if (isWeb) { - return isWebPlugin(plugin, fileSystem); + return isWebPlugin(plugin); } if (isWindows) { - return isWindowsPlugin(plugin, fileSystem); + return isWindowsPlugin(plugin); } // When we are here, no flags are specified. Only return true if the plugin // supports Android for legacy command support. // TODO(cyanglaz): Make Android flag also required like other platforms // (breaking change). https://github.com/flutter/flutter/issues/58285 - return isAndroidPlugin(plugin, fileSystem); + return isAndroidPlugin(plugin); } } diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 2d91deffa2c..6db0d629e59 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -17,12 +17,11 @@ import 'common.dart'; class FirebaseTestLabCommand extends PluginCommand { /// Creates an instance of the test runner command. FirebaseTestLabCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Print print = print, }) : _print = print, - super(packagesDir, fileSystem, processRunner: processRunner) { + super(packagesDir, processRunner: processRunner) { argParser.addOption( 'project', defaultsTo: 'flutter-infra', @@ -105,10 +104,13 @@ class FirebaseTestLabCommand extends PluginCommand { Future run() async { final Stream packagesWithTests = getPackages().where( (Directory d) => - isFlutterPackage(d, fileSystem) && - fileSystem - .directory(p.join( - d.path, 'example', 'android', 'app', 'src', 'androidTest')) + isFlutterPackage(d) && + d + .childDirectory('example') + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('androidTest') .existsSync()); final List failingPackages = []; @@ -118,23 +120,20 @@ class FirebaseTestLabCommand extends PluginCommand { await for (final Directory package in packagesWithTests) { // See https://github.com/flutter/flutter/issues/38983 - final Directory exampleDirectory = - fileSystem.directory(p.join(package.path, 'example')); + final Directory exampleDirectory = package.childDirectory('example'); final String packageName = p.relative(package.path, from: packagesDir.path); _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); final Directory androidDirectory = - fileSystem.directory(p.join(exampleDirectory.path, 'android')); + exampleDirectory.childDirectory('android'); final String enableExperiment = getStringArg(kEnableExperiment); final String encodedEnableExperiment = Uri.encodeComponent('--enable-experiment=$enableExperiment'); // Ensures that gradle wrapper exists - if (!fileSystem - .file(p.join(androidDirectory.path, _gradleWrapper)) - .existsSync()) { + if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { final int exitCode = await processRunner.runAndStream( 'flutter', [ @@ -181,8 +180,7 @@ class FirebaseTestLabCommand extends PluginCommand { final List testDirs = package.listSync().where(isTestDir).cast().toList(); - final Directory example = - fileSystem.directory(p.join(package.path, 'example')); + final Directory example = package.childDirectory('example'); testDirs.addAll( example.listSync().where(isTestDir).cast().toList()); for (final Directory testDir in testDirs) { diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 51ab7957d88..1ef41f82bb2 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -20,10 +20,9 @@ final Uri _googleFormatterUrl = Uri.https('github.com', class FormatCommand extends PluginCommand { /// Creates an instance of the format command. FormatCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { + }) : super(packagesDir, processRunner: processRunner) { argParser.addFlag('fail-on-change', hide: true); argParser.addOption('clang-format', defaultsTo: 'clang-format', @@ -144,7 +143,8 @@ class FormatCommand extends PluginCommand { final String javaFormatterPath = p.join( p.dirname(p.fromUri(io.Platform.script)), 'google-java-format-1.3-all-deps.jar'); - final File javaFormatterFile = fileSystem.file(javaFormatterPath); + final File javaFormatterFile = + packagesDir.fileSystem.file(javaFormatterPath); if (!javaFormatterFile.existsSync()) { print('Downloading Google Java Format...'); diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index 5df97627cec..d1366ea7636 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -13,10 +13,9 @@ import 'common.dart'; class JavaTestCommand extends PluginCommand { /// Creates an instance of the test runner. JavaTestCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner); + }) : super(packagesDir, processRunner: processRunner); @override final String name = 'java-test'; @@ -32,12 +31,17 @@ class JavaTestCommand extends PluginCommand { Future run() async { final Stream examplesWithTests = getExamples().where( (Directory d) => - isFlutterPackage(d, fileSystem) && - (fileSystem - .directory(p.join(d.path, 'android', 'app', 'src', 'test')) + isFlutterPackage(d) && + (d + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('test') .existsSync() || - fileSystem - .directory(p.join(d.path, '..', 'android', 'src', 'test')) + d.parent + .childDirectory('android') + .childDirectory('src') + .childDirectory('test') .existsSync())); final List failingPackages = []; @@ -47,11 +51,8 @@ class JavaTestCommand extends PluginCommand { p.relative(example.path, from: packagesDir.path); print('\nRUNNING JAVA TESTS for $packageName'); - final Directory androidDirectory = - fileSystem.directory(p.join(example.path, 'android')); - if (!fileSystem - .file(p.join(androidDirectory.path, _gradleWrapper)) - .existsSync()) { + final Directory androidDirectory = example.childDirectory('android'); + if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { print('ERROR: Run "flutter build apk" on example app of $packageName' 'before executing tests.'); missingFlutterBuild.add(packageName); diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index a6528640b82..805c3ab9f90 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -98,11 +98,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. class LicenseCheckCommand extends PluginCommand { /// Creates a new license check command for [packagesDir]. LicenseCheckCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { Print print = print, }) : _print = print, - super(packagesDir, fileSystem); + super(packagesDir); final Print _print; diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 5c66373ac6f..72bb6af3f64 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -20,14 +20,13 @@ import 'common.dart'; class LintPodspecsCommand extends PluginCommand { /// Creates an instance of the linter command. LintPodspecsCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), Print print = print, }) : _platform = platform, _print = print, - super(packagesDir, fileSystem, processRunner: processRunner) { + super(packagesDir, processRunner: processRunner) { argParser.addMultiOption('skip', help: 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index f834c8aa502..f6b186e7ba2 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -12,8 +12,7 @@ import 'common.dart'; class ListCommand extends PluginCommand { /// Creates an instance of the list command, whose behavior depends on the /// 'type' argument it provides. - ListCommand(Directory packagesDir, FileSystem fileSystem) - : super(packagesDir, fileSystem) { + ListCommand(Directory packagesDir) : super(packagesDir) { argParser.addOption( _type, defaultsTo: _plugin, diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index a3fbc34ae8a..9f1a1d5bf24 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -9,7 +9,6 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; -import 'package:path/path.dart' as p; import 'analyze_command.dart'; import 'build_examples_command.dart'; @@ -32,11 +31,11 @@ import 'xctest_command.dart'; void main(List args) { const FileSystem fileSystem = LocalFileSystem(); - Directory packagesDir = fileSystem - .directory(p.join(fileSystem.currentDirectory.path, 'packages')); + Directory packagesDir = + fileSystem.currentDirectory.childDirectory('packages'); if (!packagesDir.existsSync()) { - if (p.basename(fileSystem.currentDirectory.path) == 'packages') { + if (fileSystem.currentDirectory.basename == 'packages') { packagesDir = fileSystem.currentDirectory; } else { print('Error: Cannot find a "packages" sub-directory'); @@ -47,22 +46,22 @@ void main(List args) { final CommandRunner commandRunner = CommandRunner( 'pub global run flutter_plugin_tools', 'Productivity utils for hosting multiple plugins within one repository.') - ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) - ..addCommand(BuildExamplesCommand(packagesDir, fileSystem)) - ..addCommand(CreateAllPluginsAppCommand(packagesDir, fileSystem)) - ..addCommand(DriveExamplesCommand(packagesDir, fileSystem)) - ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) - ..addCommand(FormatCommand(packagesDir, fileSystem)) - ..addCommand(JavaTestCommand(packagesDir, fileSystem)) - ..addCommand(LicenseCheckCommand(packagesDir, fileSystem)) - ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) - ..addCommand(ListCommand(packagesDir, fileSystem)) - ..addCommand(PublishCheckCommand(packagesDir, fileSystem)) - ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) - ..addCommand(PubspecCheckCommand(packagesDir, fileSystem)) - ..addCommand(TestCommand(packagesDir, fileSystem)) - ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) - ..addCommand(XCTestCommand(packagesDir, fileSystem)); + ..addCommand(AnalyzeCommand(packagesDir)) + ..addCommand(BuildExamplesCommand(packagesDir)) + ..addCommand(CreateAllPluginsAppCommand(packagesDir)) + ..addCommand(DriveExamplesCommand(packagesDir)) + ..addCommand(FirebaseTestLabCommand(packagesDir)) + ..addCommand(FormatCommand(packagesDir)) + ..addCommand(JavaTestCommand(packagesDir)) + ..addCommand(LicenseCheckCommand(packagesDir)) + ..addCommand(LintPodspecsCommand(packagesDir)) + ..addCommand(ListCommand(packagesDir)) + ..addCommand(PublishCheckCommand(packagesDir)) + ..addCommand(PublishPluginCommand(packagesDir)) + ..addCommand(PubspecCheckCommand(packagesDir)) + ..addCommand(TestCommand(packagesDir)) + ..addCommand(VersionCheckCommand(packagesDir)) + ..addCommand(XCTestCommand(packagesDir)); commandRunner.run(args).catchError((Object e) { final ToolExit toolExit = e as ToolExit; diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 7a080f44207..fa229cabefc 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -21,13 +21,12 @@ import 'common.dart'; class PublishCheckCommand extends PluginCommand { /// Creates an instance of the publish command. PublishCheckCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), this.httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), - super(packagesDir, fileSystem, processRunner: processRunner) { + super(packagesDir, processRunner: processRunner) { argParser.addFlag( _allowPrereleaseFlag, help: 'Allows the pre-release SDK warning to pass.\n' diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index cb72c189721..6b837cb3f4f 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -32,16 +32,14 @@ import 'common.dart'; class PublishPluginCommand extends PluginCommand { /// Creates an instance of the publish command. PublishPluginCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Print print = print, io.Stdin stdinput, GitDir gitDir, }) : _print = print, _stdin = stdinput ?? io.stdin, - super(packagesDir, fileSystem, - processRunner: processRunner, gitDir: gitDir) { + super(packagesDir, processRunner: processRunner, gitDir: gitDir) { argParser.addOption( _packageOption, help: 'The package to publish.' @@ -133,12 +131,15 @@ class PublishPluginCommand extends PluginCommand { } _print('Checking local repo...'); - if (!await GitDir.isGitDir(packagesDir.path)) { - _print('$packagesDir is not a valid Git repository.'); + // Ensure there are no symlinks in the path, as it can break + // GitDir's allowSubdirectory:true. + final String packagesPath = packagesDir.resolveSymbolicLinksSync(); + if (!await GitDir.isGitDir(packagesPath)) { + _print('$packagesPath is not a valid Git repository.'); throw ToolExit(1); } final GitDir baseGitDir = - await GitDir.fromExisting(packagesDir.path, allowSubdirectory: true); + await GitDir.fromExisting(packagesPath, allowSubdirectory: true); final bool shouldPushTag = getBoolArg(_pushTagsOption); final String remote = getStringArg(_remoteOption); @@ -194,8 +195,9 @@ class PublishPluginCommand extends PluginCommand { final List packagesFailed = []; for (final String pubspecPath in changedPubspecs) { - final File pubspecFile = - fileSystem.directory(baseGitDir.path).childFile(pubspecPath); + final File pubspecFile = packagesDir.fileSystem + .directory(baseGitDir.path) + .childFile(pubspecPath); final _CheckNeedsReleaseResult result = await _checkNeedsRelease( pubspecFile: pubspecFile, gitVersionFinder: gitVersionFinder, @@ -453,8 +455,7 @@ Safe to ignore if the package is deleted in this commit. } String _getTag(Directory packageDir) { - final File pubspecFile = - fileSystem.file(p.join(packageDir.path, 'pubspec.yaml')); + final File pubspecFile = packageDir.childFile('pubspec.yaml'); final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; final String name = pubspecYaml['name'] as String; @@ -499,7 +500,7 @@ Safe to ignore if the package is deleted in this commit. } void _ensureValidPubCredential() { - final File credentialFile = fileSystem.file(_credentialsPath); + final File credentialFile = packagesDir.fileSystem.file(_credentialsPath); if (credentialFile.existsSync() && credentialFile.readAsStringSync().isNotEmpty) { return; diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index fadcfbc56de..878b683dbbb 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -19,12 +19,10 @@ import 'common.dart'; class PubspecCheckCommand extends PluginCommand { /// Creates an instance of the version check command. PubspecCheckCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), GitDir? gitDir, - }) : super(packagesDir, fileSystem, - processRunner: processRunner, gitDir: gitDir); + }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); // Section order for plugins. Because the 'flutter' section is critical // information for plugins, and usually small, it goes near the top unlike in diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index fafa0f55a22..0174b986eb6 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -13,10 +13,9 @@ import 'common.dart'; class TestCommand extends PluginCommand { /// Creates an instance of the test command. TestCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { + }) : super(packagesDir, processRunner: processRunner) { argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -37,7 +36,7 @@ class TestCommand extends PluginCommand { await for (final Directory packageDir in getPackages()) { final String packageName = p.relative(packageDir.path, from: packagesDir.path); - if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { + if (!packageDir.childDirectory('test').existsSync()) { print('SKIPPING $packageName - no test subdirectory'); continue; } @@ -48,7 +47,7 @@ class TestCommand extends PluginCommand { // `flutter test` automatically gets packages. `pub run test` does not. :( int exitCode = 0; - if (isFlutterPackage(packageDir, fileSystem)) { + if (isFlutterPackage(packageDir)) { final List args = [ 'test', '--color', @@ -56,7 +55,7 @@ class TestCommand extends PluginCommand { '--enable-experiment=$enableExperiment', ]; - if (isWebPlugin(packageDir, fileSystem)) { + if (isWebPlugin(packageDir)) { args.add('--platform=chrome'); } exitCode = await processRunner.runAndStream( diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index c095273a772..d74c7dad3c5 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -73,15 +73,13 @@ Map getAllowedNextVersions( class VersionCheckCommand extends PluginCommand { /// Creates an instance of the version check command. VersionCheckCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), GitDir gitDir, this.httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), - super(packagesDir, fileSystem, - processRunner: processRunner, gitDir: gitDir) { + super(packagesDir, processRunner: processRunner, gitDir: gitDir) { argParser.addFlag( _againstPubFlag, help: 'Whether the version check should run against the version on pub.\n' @@ -117,7 +115,7 @@ class VersionCheckCommand extends PluginCommand { const String indentation = ' '; for (final String pubspecPath in changedPubspecs) { print('Checking versions for $pubspecPath...'); - final File pubspecFile = fileSystem.file(pubspecPath); + final File pubspecFile = packagesDir.fileSystem.file(pubspecPath); if (!pubspecFile.existsSync()) { print('${indentation}Deleted; skipping.'); continue; diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 2bd8639cd85..e41164e3ed8 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -26,10 +26,9 @@ const String _kFoundNoSimulatorsMessage = class XCTestCommand extends PluginCommand { /// Creates an instance of the test command. XCTestCommand( - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, fileSystem, processRunner: processRunner) { + }) : super(packagesDir, processRunner: processRunner) { argParser.addOption( _kiOSDestination, help: @@ -68,7 +67,7 @@ class XCTestCommand extends PluginCommand { final String packageName = p.relative(plugin.path, from: packagesDir.path); print('Start running for $packageName ...'); - if (!isIosPlugin(plugin, fileSystem)) { + if (!isIosPlugin(plugin)) { print('iOS is not supported by this plugin.'); print('\n\n'); continue; diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index b536c2bb989..28cfeaaf393 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -18,9 +18,8 @@ void main() { setUp(() { initializeFakePackages(); processRunner = RecordingProcessRunner(); - final AnalyzeCommand analyzeCommand = AnalyzeCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); + final AnalyzeCommand analyzeCommand = + AnalyzeCommand(mockPackagesDir, processRunner: processRunner); runner = CommandRunner('analyze_command', 'Test for analyze_command'); runner.addCommand(analyzeCommand); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 44634c25175..d162806ab2d 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -21,9 +21,8 @@ void main() { setUp(() { initializeFakePackages(); processRunner = RecordingProcessRunner(); - final BuildExamplesCommand command = BuildExamplesCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); + final BuildExamplesCommand command = + BuildExamplesCommand(mockPackagesDir, processRunner: processRunner); runner = CommandRunner( 'build_examples_command', 'Test for build_example_command'); diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index faf04faaeec..53fd0ec4721 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -58,7 +58,6 @@ void main() { final SamplePluginCommand samplePluginCommand = SamplePluginCommand( plugins, packagesDir, - fileSystem, processRunner: processRunner, gitDir: gitDir, ); @@ -156,8 +155,7 @@ void main() { expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); - test('all plugins should be tested if .cirrus.yml changes.', - () async { + test('all plugins should be tested if .cirrus.yml changes.', () async { gitDiffResponse = ''' .cirrus.yml packages/plugin1/CHANGELOG @@ -172,8 +170,7 @@ packages/plugin1/CHANGELOG expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); - test('all plugins should be tested if .ci.yaml changes', - () async { + test('all plugins should be tested if .ci.yaml changes', () async { gitDiffResponse = ''' .ci.yaml packages/plugin1/CHANGELOG @@ -188,8 +185,7 @@ packages/plugin1/CHANGELOG expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); - test('all plugins should be tested if anything in .ci/ changes', - () async { + test('all plugins should be tested if anything in .ci/ changes', () async { gitDiffResponse = ''' .ci/Dockerfile packages/plugin1/CHANGELOG @@ -520,12 +516,10 @@ file2/file2.cc class SamplePluginCommand extends PluginCommand { SamplePluginCommand( this._plugins, - Directory packagesDir, - FileSystem fileSystem, { + Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), GitDir? gitDir, - }) : super(packagesDir, fileSystem, - processRunner: processRunner, gitDir: gitDir); + }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); final List _plugins; diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index caf4218dc16..e9da3cb1ef8 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -30,7 +30,6 @@ void main() { final CreateAllPluginsAppCommand command = CreateAllPluginsAppCommand( packagesDir, - fileSystem, pluginsRoot: testRoot, ); appDir = command.appDirectory; diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index b4d6a25154c..85bd4f019a4 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -21,9 +21,8 @@ void main() { setUp(() { initializeFakePackages(); processRunner = RecordingProcessRunner(); - final DriveExamplesCommand command = DriveExamplesCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); + final DriveExamplesCommand command = + DriveExamplesCommand(mockPackagesDir, processRunner: processRunner); runner = CommandRunner( 'drive_examples_command', 'Test for drive_example_command'); diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index b4e5b5b8c72..f8ddc9fa471 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -24,7 +24,7 @@ void main() { initializeFakePackages(); processRunner = RecordingProcessRunner(); final FirebaseTestLabCommand command = FirebaseTestLabCommand( - mockPackagesDir, mockFileSystem, + mockPackagesDir, processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString())); diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index 2ce231651b8..24e85429c1e 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.dart @@ -17,9 +17,8 @@ void main() { setUp(() { initializeFakePackages(); - final JavaTestCommand command = JavaTestCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); + final JavaTestCommand command = + JavaTestCommand(mockPackagesDir, processRunner: processRunner); runner = CommandRunner('java_test_test', 'Test for $JavaTestCommand'); diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 7b49fa8e9ed..a874d7db17b 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -25,7 +25,6 @@ void main() { printedMessages = []; final LicenseCheckCommand command = LicenseCheckCommand( packagesDir, - fileSystem, print: (Object? message) => printedMessages.add(message.toString()), ); runner = diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index fad750393a7..4cb416f0bab 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -30,7 +30,6 @@ void main() { when(mockPlatform.isMacOS).thenReturn(true); final LintPodspecsCommand command = LintPodspecsCommand( mockPackagesDir, - mockFileSystem, processRunner: processRunner, platform: mockPlatform, print: (Object message) => printedMessages.add(message.toString()), diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index e97474dedfe..ca0dbc614e9 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -15,7 +15,7 @@ void main() { setUp(() { initializeFakePackages(); - final ListCommand command = ListCommand(mockPackagesDir, mockFileSystem); + final ListCommand command = ListCommand(mockPackagesDir); runner = CommandRunner('list_test', 'Test for $ListCommand'); runner.addCommand(command); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 38e8504fa05..cccff19de5e 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -27,9 +27,8 @@ void main() { setUp(() { initializeFakePackages(); processRunner = PublishCheckProcessRunner(); - final PublishCheckCommand publishCheckCommand = PublishCheckCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); + final PublishCheckCommand publishCheckCommand = + PublishCheckCommand(mockPackagesDir, processRunner: processRunner); runner = CommandRunner( 'publish_check_command', @@ -146,8 +145,8 @@ void main() { processRunner.processesToReturn.add(process); - final List output = await runCapturingPrint( - runner, ['publish-check']); + final List output = + await runCapturingPrint(runner, ['publish-check']); expect(output, isNot(contains(contains('ERROR:')))); }); @@ -180,8 +179,7 @@ void main() { } return null; }); - final PublishCheckCommand command = PublishCheckCommand( - mockPackagesDir, mockFileSystem, + final PublishCheckCommand command = PublishCheckCommand(mockPackagesDir, processRunner: processRunner, httpClient: mockClient); runner = CommandRunner( @@ -247,8 +245,7 @@ void main() { } return null; }); - final PublishCheckCommand command = PublishCheckCommand( - mockPackagesDir, mockFileSystem, + final PublishCheckCommand command = PublishCheckCommand(mockPackagesDir, processRunner: processRunner, httpClient: mockClient); runner = CommandRunner( @@ -317,8 +314,7 @@ void main() { } return null; }); - final PublishCheckCommand command = PublishCheckCommand( - mockPackagesDir, mockFileSystem, + final PublishCheckCommand command = PublishCheckCommand(mockPackagesDir, processRunner: processRunner, httpClient: mockClient); runner = CommandRunner( diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 3618cc38bc8..1bf6ab7bbe7 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -45,6 +45,9 @@ void main() { setUp(() async { parentDir = fileSystem.systemTempDirectory .createTempSync('publish_plugin_command_test-'); + // The temp directory can have symbolic links, which won't match git output; + // use a fully resolved version to avoid potential path comparison issues. + parentDir = fileSystem.directory(parentDir.resolveSymbolicLinksSync()); initializeFakePackages(parentDir: parentDir); pluginDir = createFakePlugin(testPluginName, withSingleExample: false, packagesDirectory: parentDir); @@ -58,7 +61,7 @@ void main() { processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand(parentDir, fileSystem, + ..addCommand(PublishPluginCommand(parentDir, processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString()), stdinput: mockStdin, diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index aee3f7a2031..e1b1d364978 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -23,9 +23,8 @@ void main() { packagesDir = fileSystem.currentDirectory.childDirectory('packages'); initializeFakePackages(parentDir: packagesDir.parent); processRunner = RecordingProcessRunner(); - final PubspecCheckCommand command = PubspecCheckCommand( - packagesDir, fileSystem, - processRunner: processRunner); + final PubspecCheckCommand command = + PubspecCheckCommand(packagesDir, processRunner: processRunner); runner = CommandRunner( 'pubspec_check_command', 'Test for pubspec_check_command'); diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 1c7118abc61..61c06e0af1b 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -16,8 +16,8 @@ void main() { setUp(() { initializeFakePackages(); - final TestCommand command = TestCommand(mockPackagesDir, mockFileSystem, - processRunner: processRunner); + final TestCommand command = + TestCommand(mockPackagesDir, processRunner: processRunner); runner = CommandRunner('test_test', 'Test for $TestCommand'); runner.addCommand(command); diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index 536b885dd57..600d9a08c3f 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -88,8 +88,7 @@ void main() { }); initializeFakePackages(); processRunner = RecordingProcessRunner(); - final VersionCheckCommand command = VersionCheckCommand( - mockPackagesDir, mockFileSystem, + final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, processRunner: processRunner, gitDir: gitDir); runner = CommandRunner( @@ -238,13 +237,10 @@ void main() { }); test('gracefully handles missing pubspec.yaml', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + final Directory pluginDir = createFakePlugin('plugin', + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; - mockFileSystem.currentDirectory - .childDirectory('packages') - .childDirectory('plugin') - .childFile('pubspec.yaml') - .deleteSync(); + pluginDir.childFile('pubspec.yaml').deleteSync(); final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); @@ -600,8 +596,7 @@ The first version listed in CHANGELOG.md is 1.0.0. final MockClient mockClient = MockClient((http.Request request) async { return http.Response(json.encode(httpResponse), 200); }); - final VersionCheckCommand command = VersionCheckCommand( - mockPackagesDir, mockFileSystem, + final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); runner = CommandRunner( @@ -637,8 +632,7 @@ The first version listed in CHANGELOG.md is 1.0.0. final MockClient mockClient = MockClient((http.Request request) async { return http.Response(json.encode(httpResponse), 200); }); - final VersionCheckCommand command = VersionCheckCommand( - mockPackagesDir, mockFileSystem, + final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); runner = CommandRunner( @@ -682,8 +676,7 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N final MockClient mockClient = MockClient((http.Request request) async { return http.Response('xx', 400); }); - final VersionCheckCommand command = VersionCheckCommand( - mockPackagesDir, mockFileSystem, + final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); runner = CommandRunner( @@ -726,8 +719,7 @@ ${indentation}HTTP response: xx final MockClient mockClient = MockClient((http.Request request) async { return http.Response('xx', 404); }); - final VersionCheckCommand command = VersionCheckCommand( - mockPackagesDir, mockFileSystem, + final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); runner = CommandRunner( diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 505730f37a6..0b25a5b015c 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -91,9 +91,8 @@ void main() { setUp(() { initializeFakePackages(); processRunner = RecordingProcessRunner(); - final XCTestCommand command = XCTestCommand( - mockPackagesDir, mockFileSystem, - processRunner: processRunner); + final XCTestCommand command = + XCTestCommand(mockPackagesDir, processRunner: processRunner); runner = CommandRunner('xctest_command', 'Test for xctest_command'); runner.addCommand(command); From 74d03857f862d340d1fc9b910986f16e02b69af1 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 7 Jun 2021 10:04:43 -0700 Subject: [PATCH 062/249] [flutter_plugin_tools] Remove global state from tests (#4018) Eliminates the global test filesystem and global test packages directory, in favor of local versions. This guarantees that each test runs with a clean filesystem state, rather than relying on cleanup. It also simplifies understanding the tests, since everything is done via params and return values instead of needing to know about the magic global variables and which methods mutate them. --- script/tool/test/analyze_command_test.dart | 33 +++-- .../test/build_examples_command_test.dart | 113 +++++++-------- script/tool/test/common_test.dart | 137 +++++++----------- .../create_all_plugins_app_command_test.dart | 14 +- .../test/drive_examples_command_test.dart | 82 ++++++----- script/tool/test/firebase_test_lab_test.dart | 33 ++--- script/tool/test/java_test_command_test.dart | 18 ++- .../tool/test/lint_podspecs_command_test.dart | 50 +++---- script/tool/test/list_command_test.dart | 52 +++---- .../tool/test/publish_check_command_test.dart | 44 +++--- .../test/publish_plugin_command_test.dart | 115 +++++++-------- .../tool/test/pubspec_check_command_test.dart | 42 +++--- script/tool/test/test_command_test.dart | 59 ++++---- script/tool/test/util.dart | 47 +++--- script/tool/test/version_check_test.dart | 101 +++++++------ script/tool/test/xctest_command_test.dart | 53 ++++--- 16 files changed, 456 insertions(+), 537 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 28cfeaaf393..ec627f25864 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -4,6 +4,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:test/test.dart'; @@ -12,26 +13,25 @@ import 'mocks.dart'; import 'util.dart'; void main() { + late FileSystem fileSystem; + late Directory packagesDir; late RecordingProcessRunner processRunner; late CommandRunner runner; setUp(() { - initializeFakePackages(); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); final AnalyzeCommand analyzeCommand = - AnalyzeCommand(mockPackagesDir, processRunner: processRunner); + AnalyzeCommand(packagesDir, processRunner: processRunner); runner = CommandRunner('analyze_command', 'Test for analyze_command'); runner.addCommand(analyzeCommand); }); - tearDown(() { - mockPackagesDir.deleteSync(recursive: true); - }); - test('analyzes all packages', () async { - final Directory plugin1Dir = createFakePlugin('a'); - final Directory plugin2Dir = createFakePlugin('b'); + final Directory plugin1Dir = createFakePlugin('a', packagesDir); + final Directory plugin2Dir = createFakePlugin('b', packagesDir); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -53,7 +53,8 @@ void main() { }); test('skips flutter pub get for examples', () async { - final Directory plugin1Dir = createFakePlugin('a', withSingleExample: true); + final Directory plugin1Dir = + createFakePlugin('a', packagesDir, withSingleExample: true); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -71,8 +72,8 @@ void main() { }); test('don\'t elide a non-contained example package', () async { - final Directory plugin1Dir = createFakePlugin('a'); - final Directory plugin2Dir = createFakePlugin('example'); + final Directory plugin1Dir = createFakePlugin('a', packagesDir); + final Directory plugin2Dir = createFakePlugin('example', packagesDir); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -94,7 +95,7 @@ void main() { }); test('uses a separate analysis sdk', () async { - final Directory pluginDir = createFakePlugin('a'); + final Directory pluginDir = createFakePlugin('a', packagesDir); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -120,7 +121,7 @@ void main() { group('verifies analysis settings', () { test('fails analysis_options.yaml', () async { - createFakePlugin('foo', withExtraFiles: >[ + createFakePlugin('foo', packagesDir, withExtraFiles: >[ ['analysis_options.yaml'] ]); @@ -129,7 +130,7 @@ void main() { }); test('fails .analysis_options', () async { - createFakePlugin('foo', withExtraFiles: >[ + createFakePlugin('foo', packagesDir, withExtraFiles: >[ ['.analysis_options'] ]); @@ -139,7 +140,7 @@ void main() { test('takes an allow list', () async { final Directory pluginDir = - createFakePlugin('foo', withExtraFiles: >[ + createFakePlugin('foo', packagesDir, withExtraFiles: >[ ['analysis_options.yaml'] ]); @@ -160,7 +161,7 @@ void main() { // See: https://github.com/flutter/flutter/issues/78994 test('takes an empty allow list', () async { - createFakePlugin('foo', withExtraFiles: >[ + createFakePlugin('foo', packagesDir, withExtraFiles: >[ ['analysis_options.yaml'] ]); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index d162806ab2d..2ad17b374ba 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -4,6 +4,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; +import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/build_examples_command.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; @@ -13,40 +14,42 @@ import 'util.dart'; void main() { group('test build_example_command', () { + late FileSystem fileSystem; + late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; final String flutterCommand = const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; setUp(() { - initializeFakePackages(); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); final BuildExamplesCommand command = - BuildExamplesCommand(mockPackagesDir, processRunner: processRunner); + BuildExamplesCommand(packagesDir, processRunner: processRunner); runner = CommandRunner( 'build_examples_command', 'Test for build_example_command'); runner.addCommand(command); - cleanupPackages(); }); test('building for iOS when plugin is not set up for iOS results in no-op', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isLinuxPlugin: false); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--ipa', '--no-macos']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -61,18 +64,17 @@ void main() { // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); }); test('building for ios', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isIosPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -83,7 +85,7 @@ void main() { '--enable-experiment=exp1' ]); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -107,27 +109,26 @@ void main() { ], pluginExampleDirectory.path), ])); - cleanupPackages(); }); test( 'building for Linux when plugin is not set up for Linux results in no-op', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isLinuxPlugin: false); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--linux']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -142,25 +143,24 @@ void main() { // Output should be empty since running build-examples --linux with no // Linux implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); }); test('building for Linux', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isLinuxPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--linux']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -177,24 +177,24 @@ void main() { ProcessCall(flutterCommand, const ['build', 'linux'], pluginExampleDirectory.path), ])); - cleanupPackages(); }); test('building for macos with no implementation results in no-op', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['example', 'test'], - ]); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ]); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--macos']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -209,11 +209,10 @@ void main() { // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); }); test('building for macos', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ['example', 'macos', 'macos.swift'], @@ -221,14 +220,14 @@ void main() { isMacOsPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--macos']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -245,23 +244,23 @@ void main() { ProcessCall(flutterCommand, const ['build', 'macos'], pluginExampleDirectory.path), ])); - cleanupPackages(); }); test('building for web with no implementation results in no-op', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['example', 'test'], - ]); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ]); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--web']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -276,11 +275,10 @@ void main() { // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); }); test('building for web', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ['example', 'web', 'index.html'], @@ -288,14 +286,14 @@ void main() { isWebPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--web']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -312,27 +310,26 @@ void main() { ProcessCall(flutterCommand, const ['build', 'web'], pluginExampleDirectory.path), ])); - cleanupPackages(); }); test( 'building for Windows when plugin is not set up for Windows results in no-op', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isWindowsPlugin: false); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--windows']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -347,25 +344,24 @@ void main() { // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); }); test('building for windows', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isWindowsPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--windows']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -382,27 +378,26 @@ void main() { ProcessCall(flutterCommand, const ['build', 'windows'], pluginExampleDirectory.path), ])); - cleanupPackages(); }); test( 'building for Android when plugin is not set up for Android results in no-op', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isLinuxPlugin: false); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); final List output = await runCapturingPrint( runner, ['build-examples', '--apk', '--no-ipa']); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -417,18 +412,17 @@ void main() { // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); - cleanupPackages(); }); test('building for android', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isAndroidPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -439,7 +433,7 @@ void main() { '--no-macos', ]); final String packageName = - p.relative(pluginExampleDirectory.path, from: mockPackagesDir.path); + p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, @@ -456,18 +450,17 @@ void main() { ProcessCall(flutterCommand, const ['build', 'apk'], pluginExampleDirectory.path), ])); - cleanupPackages(); }); test('enable-experiment flag for Android', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isAndroidPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -487,18 +480,17 @@ void main() { const ['build', 'apk', '--enable-experiment=exp1'], pluginExampleDirectory.path), ])); - cleanupPackages(); }); test('enable-experiment flag for ios', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isIosPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -521,7 +513,6 @@ void main() { ], pluginExampleDirectory.path), ])); - cleanupPackages(); }); }); } diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index 53fd0ec4721..2f497963d22 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -34,7 +34,7 @@ void main() { setUp(() { fileSystem = MemoryFileSystem(); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); thirdPartyPackagesDir = packagesDir.parent .childDirectory('third_party') .childDirectory('packages'); @@ -52,7 +52,6 @@ void main() { } return Future.value(mockProcessResult); }); - initializeFakePackages(parentDir: packagesDir.parent); processRunner = RecordingProcessRunner(); plugins = []; final SamplePluginCommand samplePluginCommand = SamplePluginCommand( @@ -67,47 +66,40 @@ void main() { }); test('all plugins from file system', () async { - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run(['sample']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins includes third_party/packages', () async { - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); final Directory plugin3 = - createFakePlugin('plugin3', packagesDirectory: thirdPartyPackagesDir); + createFakePlugin('plugin3', thirdPartyPackagesDir); await runner.run(['sample']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); }); test('exclude plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); expect(plugins, unorderedEquals([plugin2.path])); }); test('exclude plugins when plugins flag isn\'t specified', () async { - createFakePlugin('plugin1', packagesDirectory: packagesDir); - createFakePlugin('plugin2', packagesDirectory: packagesDir); + createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); await runner.run(['sample', '--exclude=plugin1,plugin2']); expect(plugins, unorderedEquals([])); }); test('exclude federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', - parentDirectoryName: 'federated', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + createFakePlugin('plugin1', packagesDir, parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run([ 'sample', '--plugins=federated/plugin1,plugin2', @@ -118,10 +110,8 @@ void main() { test('exclude entire federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', - parentDirectoryName: 'federated', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + createFakePlugin('plugin1', packagesDir, parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run([ 'sample', '--plugins=federated/plugin1,plugin2', @@ -132,10 +122,8 @@ void main() { group('test run-on-changed-packages', () { test('all plugins should be tested if there are no changes.', () async { - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -145,10 +133,8 @@ void main() { test('all plugins should be tested if there are no plugin related changes.', () async { gitDiffResponse = 'AUTHORS'; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -160,10 +146,8 @@ void main() { .cirrus.yml packages/plugin1/CHANGELOG '''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -175,10 +159,8 @@ packages/plugin1/CHANGELOG .ci.yaml packages/plugin1/CHANGELOG '''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -190,10 +172,8 @@ packages/plugin1/CHANGELOG .ci/Dockerfile packages/plugin1/CHANGELOG '''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -206,10 +186,8 @@ packages/plugin1/CHANGELOG script/tool_runner.sh packages/plugin1/CHANGELOG '''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -222,10 +200,8 @@ packages/plugin1/CHANGELOG analysis_options.yaml packages/plugin1/CHANGELOG '''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -238,10 +214,8 @@ packages/plugin1/CHANGELOG .clang-format packages/plugin1/CHANGELOG '''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -250,9 +224,8 @@ packages/plugin1/CHANGELOG test('Only changed plugin should be tested.', () async { gitDiffResponse = 'packages/plugin1/plugin1.dart'; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -264,9 +237,8 @@ packages/plugin1/CHANGELOG packages/plugin1/plugin1.dart packages/plugin1/ios/plugin1.m '''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - createFakePlugin('plugin2', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -279,11 +251,9 @@ packages/plugin1/ios/plugin1.m packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m '''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); - createFakePlugin('plugin3', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -298,10 +268,10 @@ packages/plugin1/plugin1/plugin1.dart packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart packages/plugin1/plugin1_web/plugin1_web.dart '''; - final Directory plugin1 = createFakePlugin('plugin1', - parentDirectoryName: 'plugin1', packagesDirectory: packagesDir); - createFakePlugin('plugin2', packagesDirectory: packagesDir); - createFakePlugin('plugin3', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); await runner.run( ['sample', '--base-sha=master', '--run-on-changed-packages']); @@ -315,11 +285,10 @@ packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; - final Directory plugin1 = createFakePlugin('plugin1', - parentDirectoryName: 'plugin1', packagesDirectory: packagesDir); - final Directory plugin2 = - createFakePlugin('plugin2', packagesDirectory: packagesDir); - createFakePlugin('plugin3', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'plugin1'); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); await runner.run([ 'sample', '--plugins=plugin1,plugin2', @@ -336,10 +305,10 @@ packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; - final Directory plugin1 = createFakePlugin('plugin1', - parentDirectoryName: 'plugin1', packagesDirectory: packagesDir); - createFakePlugin('plugin2', packagesDirectory: packagesDir); - createFakePlugin('plugin3', packagesDirectory: packagesDir); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); await runner.run([ 'sample', '--exclude=plugin2,plugin3', @@ -352,12 +321,15 @@ 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(); @@ -374,14 +346,9 @@ packages/plugin3/plugin3.dart } return Future.value(mockProcessResult); }); - initializeFakePackages(); processRunner = RecordingProcessRunner(); }); - tearDown(() { - cleanupPackages(); - }); - test('No git diff should result no files changed', () async { final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); final List changedFiles = await finder.getChangedFiles(); diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index e9da3cb1ef8..b3cbd592a63 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -43,9 +43,9 @@ void main() { }); test('pubspec includes all plugins', () async { - createFakePlugin('plugina', packagesDirectory: packagesDir); - createFakePlugin('pluginb', packagesDirectory: packagesDir); - createFakePlugin('pluginc', packagesDirectory: packagesDir); + createFakePlugin('plugina', packagesDir); + createFakePlugin('pluginb', packagesDir); + createFakePlugin('pluginc', packagesDir); await runner.run(['all-plugins-app']); final List pubspec = @@ -61,9 +61,9 @@ void main() { }); test('pubspec has overrides for all plugins', () async { - createFakePlugin('plugina', packagesDirectory: packagesDir); - createFakePlugin('pluginb', packagesDirectory: packagesDir); - createFakePlugin('pluginc', packagesDirectory: packagesDir); + createFakePlugin('plugina', packagesDir); + createFakePlugin('pluginb', packagesDir); + createFakePlugin('pluginc', packagesDir); await runner.run(['all-plugins-app']); final List pubspec = @@ -80,7 +80,7 @@ void main() { }); test('pubspec is compatible with null-safe app code', () async { - createFakePlugin('plugina', packagesDirectory: packagesDir); + createFakePlugin('plugina', packagesDir); await runner.run(['all-plugins-app']); final String pubspec = diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 85bd4f019a4..c9a8b9d90a8 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -4,6 +4,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/drive_examples_command.dart'; import 'package:path/path.dart' as p; @@ -14,27 +15,27 @@ import 'util.dart'; void main() { group('test drive_example_command', () { + late FileSystem fileSystem; + late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; final String flutterCommand = const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + setUp(() { - initializeFakePackages(); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); final DriveExamplesCommand command = - DriveExamplesCommand(mockPackagesDir, processRunner: processRunner); + DriveExamplesCommand(packagesDir, processRunner: processRunner); runner = CommandRunner( 'drive_examples_command', 'Test for drive_example_command'); runner.addCommand(command); }); - tearDown(() { - cleanupPackages(); - }); - test('driving under folder "test"', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test', 'plugin.dart'], @@ -43,7 +44,7 @@ void main() { isAndroidPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -79,7 +80,7 @@ void main() { }); test('driving under folder "test_driver"', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], @@ -88,7 +89,7 @@ void main() { isIosPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -125,7 +126,7 @@ void main() { test('driving under folder "test_driver" when test files are missing"', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ], @@ -133,7 +134,7 @@ void main() { isIosPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -144,7 +145,7 @@ void main() { test('a plugin without any integration test files is reported as an error', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'lib', 'main.dart'], ], @@ -152,7 +153,7 @@ void main() { isIosPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -164,7 +165,7 @@ void main() { test( 'driving under folder "test_driver" when targets are under "integration_test"', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'integration_test.dart'], ['example', 'integration_test', 'bar_test.dart'], @@ -175,7 +176,7 @@ void main() { isIosPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -221,7 +222,7 @@ void main() { }); test('driving when plugin does not support Linux is a no-op', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], @@ -229,7 +230,7 @@ void main() { isMacOsPlugin: false); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -254,7 +255,7 @@ void main() { }); test('driving on a Linux plugin', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], @@ -262,7 +263,7 @@ void main() { isLinuxPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -301,13 +302,14 @@ void main() { }); test('driving when plugin does not suppport macOS is a no-op', () async { - createFakePlugin('plugin', withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ]); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -331,7 +333,7 @@ void main() { expect(processRunner.recordedCalls, []); }); test('driving on a macOS plugin', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], @@ -340,7 +342,7 @@ void main() { isMacOsPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -379,7 +381,7 @@ void main() { }); test('driving when plugin does not suppport web is a no-op', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], @@ -387,7 +389,7 @@ void main() { isWebPlugin: false); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -412,7 +414,7 @@ void main() { }); test('driving a web plugin', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], @@ -420,7 +422,7 @@ void main() { isWebPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -461,7 +463,7 @@ void main() { }); test('driving when plugin does not suppport Windows is a no-op', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], @@ -469,7 +471,7 @@ void main() { isWindowsPlugin: false); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -494,7 +496,7 @@ void main() { }); test('driving on a Windows plugin', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], @@ -502,7 +504,7 @@ void main() { isWindowsPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -541,7 +543,7 @@ void main() { }); test('driving when plugin does not support mobile is no-op', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test_driver', 'plugin.dart'], @@ -549,7 +551,7 @@ void main() { isMacOsPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -573,7 +575,7 @@ void main() { }); test('platform interface plugins are silently skipped', () async { - createFakePlugin('aplugin_platform_interface'); + createFakePlugin('aplugin_platform_interface', packagesDir); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -593,7 +595,7 @@ void main() { }); test('enable-experiment flag', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test_driver', 'plugin_test.dart'], ['example', 'test', 'plugin.dart'], @@ -602,7 +604,7 @@ void main() { isAndroidPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index f8ddc9fa471..74809007c29 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -7,6 +7,8 @@ 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/firebase_test_lab_command.dart'; import 'package:test/test.dart'; @@ -16,15 +18,18 @@ import 'util.dart'; void main() { group('$FirebaseTestLabCommand', () { - final List printedMessages = []; + FileSystem fileSystem; + Directory packagesDir; + List printedMessages; CommandRunner runner; RecordingProcessRunner processRunner; setUp(() { - initializeFakePackages(); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + printedMessages = []; processRunner = RecordingProcessRunner(); - final FirebaseTestLabCommand command = FirebaseTestLabCommand( - mockPackagesDir, + final FirebaseTestLabCommand command = FirebaseTestLabCommand(packagesDir, processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString())); @@ -33,15 +38,11 @@ void main() { runner.addCommand(command); }); - tearDown(() { - printedMessages.clear(); - }); - test('retries gcloud set', () async { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(1); processRunner.processToReturn = mockProcess; - createFakePlugin('plugin', withExtraFiles: >[ + createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['lib/test/should_not_run_e2e.dart'], ['example', 'test_driver', 'plugin_e2e.dart'], ['example', 'test_driver', 'plugin_e2e_test.dart'], @@ -66,7 +67,7 @@ void main() { }); test('runs e2e tests', () async { - createFakePlugin('plugin', withExtraFiles: >[ + createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['test', 'plugin_test.dart'], ['test', 'plugin_e2e.dart'], ['should_not_run_e2e.dart'], @@ -134,7 +135,7 @@ void main() { '/packages/plugin/example'), ProcessCall( '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' .split(' '), '/packages/plugin/example/android'), ProcessCall( @@ -144,7 +145,7 @@ void main() { '/packages/plugin/example'), ProcessCall( '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' .split(' '), '/packages/plugin/example/android'), ProcessCall( @@ -167,7 +168,7 @@ void main() { }); test('experimental flag', () async { - createFakePlugin('plugin', withExtraFiles: >[ + createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['test', 'plugin_test.dart'], ['test', 'plugin_e2e.dart'], ['should_not_run_e2e.dart'], @@ -225,7 +226,7 @@ void main() { '/packages/plugin/example'), ProcessCall( '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' .split(' '), '/packages/plugin/example/android'), ProcessCall( @@ -235,7 +236,7 @@ void main() { '/packages/plugin/example'), ProcessCall( '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' .split(' '), '/packages/plugin/example/android'), ProcessCall( @@ -255,8 +256,6 @@ void main() { '/packages/plugin/example'), ]), ); - - cleanupPackages(); }); }); } diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index 24e85429c1e..a1c2d3b864c 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.dart @@ -4,6 +4,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; +import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/java_test_command.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -12,27 +13,27 @@ import 'util.dart'; void main() { group('$JavaTestCommand', () { + late FileSystem fileSystem; + late Directory packagesDir; late CommandRunner runner; - final RecordingProcessRunner processRunner = RecordingProcessRunner(); + late RecordingProcessRunner processRunner; setUp(() { - initializeFakePackages(); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); final JavaTestCommand command = - JavaTestCommand(mockPackagesDir, processRunner: processRunner); + JavaTestCommand(packagesDir, processRunner: processRunner); runner = CommandRunner('java_test_test', 'Test for $JavaTestCommand'); runner.addCommand(command); }); - tearDown(() { - cleanupPackages(); - processRunner.recordedCalls.clear(); - }); - test('Should run Java tests in Android implementation folder', () async { final Directory plugin = createFakePlugin( 'plugin1', + packagesDir, isAndroidPlugin: true, isFlutter: true, withSingleExample: true, @@ -59,6 +60,7 @@ void main() { test('Should run Java tests in example folder', () async { final Directory plugin = createFakePlugin( 'plugin1', + packagesDir, isAndroidPlugin: true, isFlutter: true, withSingleExample: true, diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 4cb416f0bab..349607b0ca7 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -6,6 +6,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; +import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; import 'package:mockito/mockito.dart'; import 'package:path/path.dart' as p; @@ -17,19 +18,22 @@ import 'util.dart'; void main() { group('$LintPodspecsCommand', () { + FileSystem fileSystem; + Directory packagesDir; CommandRunner runner; MockPlatform mockPlatform; final RecordingProcessRunner processRunner = RecordingProcessRunner(); List printedMessages; setUp(() { - initializeFakePackages(); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); printedMessages = []; mockPlatform = MockPlatform(); when(mockPlatform.isMacOS).thenReturn(true); final LintPodspecsCommand command = LintPodspecsCommand( - mockPackagesDir, + packagesDir, processRunner: processRunner, platform: mockPlatform, print: (Object message) => printedMessages.add(message.toString()), @@ -44,12 +48,8 @@ void main() { processRunner.recordedCalls.clear(); }); - tearDown(() { - cleanupPackages(); - }); - test('only runs on macOS', () async { - createFakePlugin('plugin1', withExtraFiles: >[ + createFakePlugin('plugin1', packagesDir, withExtraFiles: >[ ['plugin1.podspec'], ]); @@ -63,11 +63,11 @@ void main() { }); test('runs pod lib lint on a podspec', () async { - final Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['ios', 'plugin1.podspec'], - ['bogus.dart'], // Ignore non-podspecs. - ]); + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + withExtraFiles: >[ + ['ios', 'plugin1.podspec'], + ['bogus.dart'], // Ignore non-podspecs. + ]); processRunner.resultStdout = 'Foo'; processRunner.resultStderr = 'Bar'; @@ -77,7 +77,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('which', const ['pod'], mockPackagesDir.path), + ProcessCall('which', const ['pod'], packagesDir.path), ProcessCall( 'pod', [ @@ -89,7 +89,7 @@ void main() { '--use-modular-headers', '--use-libraries' ], - mockPackagesDir.path), + packagesDir.path), ProcessCall( 'pod', [ @@ -100,7 +100,7 @@ void main() { '--skip-tests', '--use-modular-headers', ], - mockPackagesDir.path), + packagesDir.path), ]), ); @@ -110,10 +110,10 @@ void main() { }); test('skips podspecs with known issues', () async { - createFakePlugin('plugin1', withExtraFiles: >[ + createFakePlugin('plugin1', packagesDir, withExtraFiles: >[ ['plugin1.podspec'] ]); - createFakePlugin('plugin2', withExtraFiles: >[ + createFakePlugin('plugin2', packagesDir, withExtraFiles: >[ ['plugin2.podspec'] ]); @@ -123,23 +123,23 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('which', const ['pod'], mockPackagesDir.path), + ProcessCall('which', const ['pod'], packagesDir.path), ]), ); }); test('allow warnings for podspecs with known warnings', () async { - final Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['plugin1.podspec'], - ]); + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + withExtraFiles: >[ + ['plugin1.podspec'], + ]); await runner.run(['podspecs', '--ignore-warnings=plugin1']); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('which', const ['pod'], mockPackagesDir.path), + ProcessCall('which', const ['pod'], packagesDir.path), ProcessCall( 'pod', [ @@ -152,7 +152,7 @@ void main() { '--allow-warnings', '--use-libraries' ], - mockPackagesDir.path), + packagesDir.path), ProcessCall( 'pod', [ @@ -164,7 +164,7 @@ void main() { '--use-modular-headers', '--allow-warnings', ], - mockPackagesDir.path), + packagesDir.path), ]), ); diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index ca0dbc614e9..02b898c5c3f 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -4,6 +4,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; +import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/list_command.dart'; import 'package:test/test.dart'; @@ -11,19 +12,22 @@ import 'util.dart'; void main() { group('$ListCommand', () { + late FileSystem fileSystem; + late Directory packagesDir; late CommandRunner runner; setUp(() { - initializeFakePackages(); - final ListCommand command = ListCommand(mockPackagesDir); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + final ListCommand command = ListCommand(packagesDir); runner = CommandRunner('list_test', 'Test for $ListCommand'); runner.addCommand(command); }); test('lists plugins', () async { - createFakePlugin('plugin1'); - createFakePlugin('plugin2'); + createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); final List plugins = await runCapturingPrint(runner, ['list', '--type=plugin']); @@ -35,15 +39,13 @@ void main() { '/packages/plugin2', ]), ); - - cleanupPackages(); }); test('lists examples', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin2', packagesDir, withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); + createFakePlugin('plugin3', packagesDir); final List examples = await runCapturingPrint(runner, ['list', '--type=example']); @@ -56,15 +58,13 @@ void main() { '/packages/plugin2/example/example2', ]), ); - - cleanupPackages(); }); test('lists packages', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin2', packagesDir, withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); + createFakePlugin('plugin3', packagesDir); final List packages = await runCapturingPrint(runner, ['list', '--type=package']); @@ -80,15 +80,13 @@ void main() { '/packages/plugin3', ]), ); - - cleanupPackages(); }); test('lists files', () async { - createFakePlugin('plugin1', withSingleExample: true); - createFakePlugin('plugin2', + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin2', packagesDir, withExamples: ['example1', 'example2']); - createFakePlugin('plugin3'); + createFakePlugin('plugin3', packagesDir); final List examples = await runCapturingPrint(runner, ['list', '--type=file']); @@ -104,17 +102,15 @@ void main() { '/packages/plugin3/pubspec.yaml', ]), ); - - cleanupPackages(); }); test('lists plugins using federated plugin layout', () async { - createFakePlugin('plugin1'); + createFakePlugin('plugin1', packagesDir); // Create a federated plugin by creating a directory under the packages // directory with several packages underneath. - final Directory federatedPlugin = - mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory federatedPlugin = packagesDir.childDirectory('my_plugin') + ..createSync(); final Directory clientLibrary = federatedPlugin.childDirectory('my_plugin')..createSync(); createFakePubspec(clientLibrary); @@ -138,17 +134,15 @@ void main() { '/packages/my_plugin/my_plugin_macos', ]), ); - - cleanupPackages(); }); test('can filter plugins with the --plugins argument', () async { - createFakePlugin('plugin1'); + createFakePlugin('plugin1', packagesDir); // Create a federated plugin by creating a directory under the packages // directory with several packages underneath. - final Directory federatedPlugin = - mockPackagesDir.childDirectory('my_plugin')..createSync(); + final Directory federatedPlugin = packagesDir.childDirectory('my_plugin') + ..createSync(); final Directory clientLibrary = federatedPlugin.childDirectory('my_plugin')..createSync(); createFakePubspec(clientLibrary); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index cccff19de5e..6d36031a264 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -10,6 +10,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/publish_check_command.dart'; import 'package:http/http.dart' as http; @@ -21,14 +22,17 @@ import 'util.dart'; void main() { group('$PublishCheckProcessRunner tests', () { + FileSystem fileSystem; + Directory packagesDir; PublishCheckProcessRunner processRunner; CommandRunner runner; setUp(() { - initializeFakePackages(); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = PublishCheckProcessRunner(); final PublishCheckCommand publishCheckCommand = - PublishCheckCommand(mockPackagesDir, processRunner: processRunner); + PublishCheckCommand(packagesDir, processRunner: processRunner); runner = CommandRunner( 'publish_check_command', @@ -37,13 +41,9 @@ void main() { runner.addCommand(publishCheckCommand); }); - tearDown(() { - mockPackagesDir.deleteSync(recursive: true); - }); - test('publish check all packages', () async { - final Directory plugin1Dir = createFakePlugin('a'); - final Directory plugin2Dir = createFakePlugin('b'); + final Directory plugin1Dir = createFakePlugin('a', packagesDir); + final Directory plugin2Dir = createFakePlugin('b', packagesDir); processRunner.processesToReturn.add( MockProcess()..exitCodeCompleter.complete(0), @@ -68,7 +68,7 @@ void main() { }); test('fail on negative test', () async { - createFakePlugin('a'); + createFakePlugin('a', packagesDir); final MockProcess process = MockProcess(); process.stdoutController.close(); // ignore: unawaited_futures @@ -84,7 +84,7 @@ void main() { }); test('fail on bad pubspec', () async { - final Directory dir = createFakePlugin('c'); + final Directory dir = createFakePlugin('c', packagesDir); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); final MockProcess process = MockProcess(); @@ -95,7 +95,7 @@ void main() { }); test('pass on prerelease if --allow-pre-release flag is on', () async { - createFakePlugin('d'); + createFakePlugin('d', packagesDir); const String preReleaseOutput = 'Package has 1 warning.' 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; @@ -114,7 +114,7 @@ void main() { }); test('fail on prerelease if --allow-pre-release flag is off', () async { - createFakePlugin('d'); + createFakePlugin('d', packagesDir); const String preReleaseOutput = 'Package has 1 warning.' 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; @@ -132,7 +132,7 @@ void main() { }); test('Success message on stderr is not printed as an error', () async { - createFakePlugin('d'); + createFakePlugin('d', packagesDir); const String publishOutput = 'Package has 0 warnings.'; @@ -179,7 +179,7 @@ void main() { } return null; }); - final PublishCheckCommand command = PublishCheckCommand(mockPackagesDir, + final PublishCheckCommand command = PublishCheckCommand(packagesDir, processRunner: processRunner, httpClient: mockClient); runner = CommandRunner( @@ -189,9 +189,9 @@ void main() { runner.addCommand(command); final Directory plugin1Dir = - createFakePlugin('no_publish_a', includeVersion: true); + createFakePlugin('no_publish_a', packagesDir, includeVersion: true); final Directory plugin2Dir = - createFakePlugin('no_publish_b', includeVersion: true); + createFakePlugin('no_publish_b', packagesDir, includeVersion: true); createFakePubspec(plugin1Dir, name: 'no_publish_a', includeVersion: true, version: '0.1.0'); @@ -245,7 +245,7 @@ void main() { } return null; }); - final PublishCheckCommand command = PublishCheckCommand(mockPackagesDir, + final PublishCheckCommand command = PublishCheckCommand(packagesDir, processRunner: processRunner, httpClient: mockClient); runner = CommandRunner( @@ -255,9 +255,9 @@ void main() { runner.addCommand(command); final Directory plugin1Dir = - createFakePlugin('no_publish_a', includeVersion: true); + createFakePlugin('no_publish_a', packagesDir, includeVersion: true); final Directory plugin2Dir = - createFakePlugin('no_publish_b', includeVersion: true); + createFakePlugin('no_publish_b', packagesDir, includeVersion: true); createFakePubspec(plugin1Dir, name: 'no_publish_a', includeVersion: true, version: '0.1.0'); @@ -314,7 +314,7 @@ void main() { } return null; }); - final PublishCheckCommand command = PublishCheckCommand(mockPackagesDir, + final PublishCheckCommand command = PublishCheckCommand(packagesDir, processRunner: processRunner, httpClient: mockClient); runner = CommandRunner( @@ -324,9 +324,9 @@ void main() { runner.addCommand(command); final Directory plugin1Dir = - createFakePlugin('no_publish_a', includeVersion: true); + createFakePlugin('no_publish_a', packagesDir, includeVersion: true); final Directory plugin2Dir = - createFakePlugin('no_publish_b', includeVersion: true); + createFakePlugin('no_publish_b', packagesDir, includeVersion: true); createFakePubspec(plugin1Dir, name: 'no_publish_a', includeVersion: true, version: '0.1.0'); diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 1bf6ab7bbe7..570ceb234a8 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -22,9 +22,10 @@ import 'util.dart'; void main() { const String testPluginName = 'foo'; - final List printedMessages = []; + List printedMessages; - Directory parentDir; + Directory testRoot; + Directory packagesDir; Directory pluginDir; GitDir gitDir; TestProcessRunner processRunner; @@ -43,34 +44,34 @@ void main() { } setUp(() async { - parentDir = fileSystem.systemTempDirectory + testRoot = fileSystem.systemTempDirectory .createTempSync('publish_plugin_command_test-'); // The temp directory can have symbolic links, which won't match git output; // use a fully resolved version to avoid potential path comparison issues. - parentDir = fileSystem.directory(parentDir.resolveSymbolicLinksSync()); - initializeFakePackages(parentDir: parentDir); - pluginDir = createFakePlugin(testPluginName, - withSingleExample: false, packagesDirectory: parentDir); + testRoot = fileSystem.directory(testRoot.resolveSymbolicLinksSync()); + packagesDir = createPackagesDirectory(parentDir: testRoot); + pluginDir = + createFakePlugin(testPluginName, packagesDir, withSingleExample: false); assert(pluginDir != null && pluginDir.existsSync()); createFakePubspec(pluginDir, includeVersion: true); io.Process.runSync('git', ['init'], - workingDirectory: parentDir.path); - gitDir = await GitDir.fromExisting(parentDir.path); + workingDirectory: testRoot.path); + gitDir = await GitDir.fromExisting(testRoot.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Initial commit']); processRunner = TestProcessRunner(); mockStdin = MockStdin(); + printedMessages = []; commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand(parentDir, + ..addCommand(PublishPluginCommand(packagesDir, processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString()), stdinput: mockStdin, - gitDir: await GitDir.fromExisting(parentDir.path))); + gitDir: gitDir)); }); tearDown(() { - parentDir.deleteSync(recursive: true); - printedMessages.clear(); + testRoot.deleteSync(recursive: true); }); group('Initial validation', () { @@ -109,7 +110,7 @@ void main() { expect( printedMessages, containsAllInOrder([ - 'There are files in the package directory that haven\'t been saved in git. Refusing to publish these files:\n\n?? foo/tmp\n\nIf the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.', + 'There are files in the package directory that haven\'t been saved in git. Refusing to publish these files:\n\n?? packages/foo/tmp\n\nIf the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.', 'Failed, see above for details.', ])); }); @@ -140,8 +141,8 @@ void main() { test('can publish non-flutter package', () async { createFakePubspec(pluginDir, includeVersion: true, isFlutter: false); io.Process.runSync('git', ['init'], - workingDirectory: parentDir.path); - gitDir = await GitDir.fromExisting(parentDir.path); + workingDirectory: testRoot.path); + gitDir = await GitDir.fromExisting(testRoot.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. @@ -420,21 +421,19 @@ void main() { group('Auto release (all-changed flag)', () { setUp(() async { io.Process.runSync('git', ['init'], - workingDirectory: parentDir.path); - gitDir = await GitDir.fromExisting(parentDir.path); + workingDirectory: testRoot.path); + gitDir = await GitDir.fromExisting(testRoot.path); await gitDir.runCommand( ['remote', 'add', 'upstream', 'http://localhost:8000']); }); test('can release newly created plugins', () async { // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: parentDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', - withSingleExample: true, - parentDirectoryName: 'plugin2', - packagesDirectory: parentDir); + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -475,8 +474,8 @@ void main() { test('can release newly created plugins, while there are existing plugins', () async { // Prepare an exiting plugin and tag it - final Directory pluginDir0 = createFakePlugin('plugin0', - withSingleExample: true, packagesDirectory: parentDir); + final Directory pluginDir0 = + createFakePlugin('plugin0', packagesDir, withSingleExample: true); createFakePubspec(pluginDir0, name: 'plugin0', includeVersion: true, @@ -492,13 +491,11 @@ void main() { processRunner.pushTagsArgs.clear(); // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: parentDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', - withSingleExample: true, - parentDirectoryName: 'plugin2', - packagesDirectory: parentDir); + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -536,13 +533,11 @@ void main() { test('can release newly created plugins, dry run', () async { // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: parentDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', - withSingleExample: true, - parentDirectoryName: 'plugin2', - packagesDirectory: parentDir); + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -585,13 +580,11 @@ void main() { test('version change triggers releases.', () async { // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: parentDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', - withSingleExample: true, - parentDirectoryName: 'plugin2', - packagesDirectory: parentDir); + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -676,13 +669,11 @@ void main() { 'delete package will not trigger publish but exit the command successfully.', () async { // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: parentDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', - withSingleExample: true, - parentDirectoryName: 'plugin2', - packagesDirectory: parentDir); + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -764,13 +755,11 @@ void main() { 'versions revert do not trigger releases. Also prints out warning message.', () async { // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: parentDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', - withSingleExample: true, - parentDirectoryName: 'plugin2', - packagesDirectory: parentDir); + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -846,13 +835,11 @@ void main() { test('No version change does not release any plugins', () async { // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: parentDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', - withSingleExample: true, - parentDirectoryName: 'plugin2', - packagesDirectory: parentDir); + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -865,8 +852,8 @@ void main() { version: '0.0.1'); io.Process.runSync('git', ['init'], - workingDirectory: parentDir.path); - gitDir = await GitDir.fromExisting(parentDir.path); + workingDirectory: testRoot.path); + gitDir = await GitDir.fromExisting(testRoot.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index e1b1d364978..576060d23a9 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -21,7 +21,7 @@ void main() { setUp(() { fileSystem = MemoryFileSystem(); packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - initializeFakePackages(parentDir: packagesDir.parent); + createPackagesDirectory(parentDir: packagesDir.parent); processRunner = RecordingProcessRunner(); final PubspecCheckCommand command = PubspecCheckCommand(packagesDir, processRunner: processRunner); @@ -88,8 +88,8 @@ dev_dependencies: } test('passes for a plugin following conventions', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} @@ -114,8 +114,8 @@ ${devDependenciesSection()} }); test('passes for a Flutter package following conventions', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin')} @@ -163,8 +163,8 @@ ${dependenciesSection()} }); test('fails when homepage is included', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true, includeHomepage: true)} @@ -184,8 +184,8 @@ ${devDependenciesSection()} }); test('fails when repository is missing', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true, includeRepository: false)} @@ -205,8 +205,8 @@ ${devDependenciesSection()} }); test('fails when homepage is given instead of repository', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} @@ -226,8 +226,8 @@ ${devDependenciesSection()} }); test('fails when issue tracker is missing', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true, includeIssueTracker: false)} @@ -247,8 +247,8 @@ ${devDependenciesSection()} }); test('fails when environment section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} @@ -268,8 +268,8 @@ ${environmentSection()} }); test('fails when flutter section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} @@ -289,8 +289,8 @@ ${devDependenciesSection()} }); test('fails when dependencies section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} @@ -310,8 +310,8 @@ ${dependenciesSection()} }); test('fails when devDependencies section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', - withSingleExample: true, packagesDirectory: packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 61c06e0af1b..5cbbdf5b8d4 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -4,6 +4,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; +import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/test_command.dart'; import 'package:test/test.dart'; @@ -11,32 +12,31 @@ import 'util.dart'; void main() { group('$TestCommand', () { + late FileSystem fileSystem; + late Directory packagesDir; late CommandRunner runner; - final RecordingProcessRunner processRunner = RecordingProcessRunner(); + late RecordingProcessRunner processRunner; setUp(() { - initializeFakePackages(); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); final TestCommand command = - TestCommand(mockPackagesDir, processRunner: processRunner); + TestCommand(packagesDir, processRunner: processRunner); runner = CommandRunner('test_test', 'Test for $TestCommand'); runner.addCommand(command); }); - tearDown(() { - cleanupPackages(); - processRunner.recordedCalls.clear(); - }); - test('runs flutter test on each plugin', () async { - final Directory plugin1Dir = - createFakePlugin('plugin1', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = - createFakePlugin('plugin2', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); await runner.run(['test']); @@ -49,16 +49,14 @@ void main() { 'flutter', const ['test', '--color'], plugin2Dir.path), ]), ); - - cleanupPackages(); }); test('skips testing plugins without test directory', () async { - createFakePlugin('plugin1'); - final Directory plugin2Dir = - createFakePlugin('plugin2', withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); + createFakePlugin('plugin1', packagesDir); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); await runner.run(['test']); @@ -69,17 +67,15 @@ void main() { 'flutter', const ['test', '--color'], plugin2Dir.path), ]), ); - - cleanupPackages(); }); test('runs pub run test on non-Flutter packages', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, isFlutter: true, withExtraFiles: >[ ['test', 'empty_test.dart'], ]); - final Directory plugin2Dir = createFakePlugin('plugin2', + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, isFlutter: false, withExtraFiles: >[ ['test', 'empty_test.dart'], @@ -101,13 +97,12 @@ void main() { plugin2Dir.path), ]), ); - - cleanupPackages(); }); test('runs on Chrome for web plugins', () async { final Directory pluginDir = createFakePlugin( 'plugin', + packagesDir, withExtraFiles: >[ ['test', 'empty_test.dart'], ], @@ -129,12 +124,12 @@ void main() { }); test('enable-experiment flag', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, isFlutter: true, withExtraFiles: >[ ['test', 'empty_test.dart'], ]); - final Directory plugin2Dir = createFakePlugin('plugin2', + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, isFlutter: false, withExtraFiles: >[ ['test', 'empty_test.dart'], @@ -156,8 +151,6 @@ void main() { plugin2Dir.path), ]), ); - - cleanupPackages(); }); }); } diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 7d4278f68cc..a0a316f95df 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -11,32 +11,29 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:meta/meta.dart'; -import 'package:platform/platform.dart'; import 'package:quiver/collection.dart'; -// TODO(stuartmorgan): Eliminate this in favor of setting up a clean filesystem -// for each test, to eliminate the chance of files from one test interfering -// with another test. -FileSystem mockFileSystem = MemoryFileSystem( - style: const LocalPlatform().isWindows - ? FileSystemStyle.windows - : FileSystemStyle.posix); -late Directory mockPackagesDir; - -/// Creates a mock packages directory in the mock file system. +/// Creates a packages directory in the given location. /// -/// If [parentDir] is set the mock packages dir will be creates as a child of -/// it. If not [mockFileSystem] will be used instead. -void initializeFakePackages({Directory? parentDir}) { - mockPackagesDir = - (parentDir ?? mockFileSystem.currentDirectory).childDirectory('packages'); - mockPackagesDir.createSync(); +/// If [parentDir] is set the packages directory will be created there, +/// otherwise [fileSystem] must be provided and it will be created an arbitrary +/// location in that filesystem. +Directory createPackagesDirectory( + {Directory? parentDir, FileSystem? fileSystem}) { + assert(parentDir != null || fileSystem != null, + 'One of parentDir or fileSystem must be provided'); + assert(fileSystem == null || fileSystem is MemoryFileSystem, + 'If using a real filesystem, parentDir must be provided'); + final Directory packagesDir = + (parentDir ?? fileSystem!.currentDirectory).childDirectory('packages'); + packagesDir.createSync(); + return packagesDir; } -/// Creates a plugin package with the given [name] in [packagesDirectory], -/// defaulting to [mockPackagesDir]. +/// Creates a plugin package with the given [name] in [packagesDirectory]. Directory createFakePlugin( - String name, { + String name, + Directory packagesDirectory, { bool withSingleExample = false, List withExamples = const [], List> withExtraFiles = const >[], @@ -51,12 +48,11 @@ Directory createFakePlugin( bool includeVersion = false, String version = '0.0.1', String parentDirectoryName = '', - Directory? packagesDirectory, }) { assert(!(withSingleExample && withExamples.isNotEmpty), 'cannot pass withSingleExample and withExamples simultaneously'); - Directory parentDirectory = packagesDirectory ?? mockPackagesDir; + Directory parentDirectory = packagesDirectory; if (parentDirectoryName != '') { parentDirectory = parentDirectory.childDirectory(parentDirectoryName); } @@ -198,13 +194,6 @@ publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being parent.childFile('pubspec.yaml').writeAsStringSync(yaml); } -/// Cleans up the mock packages directory, making it an empty directory again. -void cleanupPackages() { - mockPackagesDir.listSync().forEach((FileSystemEntity entity) { - entity.deleteSync(recursive: true); - }); -} - typedef _ErrorHandler = void Function(Error error); /// Run the command [runner] with the given [args] and return diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index 600d9a08c3f..ec76ceba8e7 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -10,6 +10,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/version_check_command.dart'; import 'package:git/git.dart'; @@ -55,6 +56,8 @@ String _redColorString(String string) { void main() { const String indentation = ' '; group('$VersionCheckCommand', () { + FileSystem fileSystem; + Directory packagesDir; CommandRunner runner; RecordingProcessRunner processRunner; List> gitDirCommands; @@ -63,6 +66,8 @@ void main() { MockGitDir gitDir; setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); gitDirCommands = >[]; gitDiffResponse = ''; gitShowResponses = {}; @@ -86,9 +91,8 @@ void main() { } return Future.value(mockProcessResult); }); - initializeFakePackages(); processRunner = RecordingProcessRunner(); - final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, + final VersionCheckCommand command = VersionCheckCommand(packagesDir, processRunner: processRunner, gitDir: gitDir); runner = CommandRunner( @@ -96,12 +100,9 @@ void main() { runner.addCommand(command); }); - tearDown(() { - cleanupPackages(); - }); - test('allows valid version', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', @@ -127,7 +128,8 @@ void main() { }); test('denies invalid version', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', @@ -151,7 +153,8 @@ void main() { }); test('allows valid version without explicit base-sha', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 1.0.0', @@ -169,7 +172,8 @@ void main() { }); test('allows valid version for new package.', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'HEAD:packages/plugin/pubspec.yaml': 'version: 1.0.0', @@ -187,7 +191,8 @@ void main() { }); test('allows likely reverts.', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', @@ -205,7 +210,8 @@ void main() { }); test('denies lower version that could not be a simple revert', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', @@ -221,7 +227,8 @@ void main() { }); test('denies invalid version without explicit base-sha', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 0.0.1', @@ -237,7 +244,7 @@ void main() { }); test('gracefully handles missing pubspec.yaml', () async { - final Directory pluginDir = createFakePlugin('plugin', + final Directory pluginDir = createFakePlugin('plugin', packagesDir, includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; pluginDir.childFile('pubspec.yaml').deleteSync(); @@ -259,7 +266,7 @@ void main() { }); test('allows minor changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface', + createFakePlugin('plugin_platform_interface', packagesDir, includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; gitShowResponses = { @@ -293,7 +300,7 @@ void main() { }); test('disallows breaking changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface', + createFakePlugin('plugin_platform_interface', packagesDir, includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; gitShowResponses = { @@ -326,10 +333,8 @@ void main() { test('Allow empty lines in front of the first version in CHANGELOG', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - - final Directory pluginDirectory = - mockPackagesDir.childDirectory('plugin'); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.1'); @@ -355,10 +360,8 @@ void main() { }); test('Throws if versions in changelog and pubspec do not match', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - - final Directory pluginDirectory = - mockPackagesDir.childDirectory('plugin'); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.1'); @@ -392,10 +395,8 @@ The first version listed in CHANGELOG.md is 1.0.2. }); test('Success if CHANGELOG and pubspec versions match', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - - final Directory pluginDirectory = - mockPackagesDir.childDirectory('plugin'); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.1'); @@ -420,10 +421,8 @@ The first version listed in CHANGELOG.md is 1.0.2. test( 'Fail if pubspec version only matches an older version listed in CHANGELOG', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - - final Directory pluginDirectory = - mockPackagesDir.childDirectory('plugin'); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.0'); @@ -464,10 +463,8 @@ The first version listed in CHANGELOG.md is 1.0.1. test('Allow NEXT as a placeholder for gathering CHANGELOG entries', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - - final Directory pluginDirectory = - mockPackagesDir.childDirectory('plugin'); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.0'); @@ -495,10 +492,8 @@ The first version listed in CHANGELOG.md is 1.0.1. test('Fail if NEXT is left in the CHANGELOG when adding a version bump', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - - final Directory pluginDirectory = - mockPackagesDir.childDirectory('plugin'); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.1'); @@ -541,10 +536,8 @@ into the new version's release notes. }); test('Fail if the version changes without replacing NEXT', () async { - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); - - final Directory pluginDirectory = - mockPackagesDir.childDirectory('plugin'); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); createFakePubspec(pluginDirectory, isFlutter: true, includeVersion: true, version: '1.0.1'); @@ -596,14 +589,15 @@ The first version listed in CHANGELOG.md is 1.0.0. final MockClient mockClient = MockClient((http.Request request) async { return http.Response(json.encode(httpResponse), 200); }); - final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, + final VersionCheckCommand command = VersionCheckCommand(packagesDir, processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); runner = CommandRunner( 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', @@ -632,14 +626,15 @@ The first version listed in CHANGELOG.md is 1.0.0. final MockClient mockClient = MockClient((http.Request request) async { return http.Response(json.encode(httpResponse), 200); }); - final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, + final VersionCheckCommand command = VersionCheckCommand(packagesDir, processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); runner = CommandRunner( 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', @@ -676,14 +671,15 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N final MockClient mockClient = MockClient((http.Request request) async { return http.Response('xx', 400); }); - final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, + final VersionCheckCommand command = VersionCheckCommand(packagesDir, processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); runner = CommandRunner( 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', @@ -719,14 +715,15 @@ ${indentation}HTTP response: xx final MockClient mockClient = MockClient((http.Request request) async { return http.Response('xx', 404); }); - final VersionCheckCommand command = VersionCheckCommand(mockPackagesDir, + final VersionCheckCommand command = VersionCheckCommand(packagesDir, processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); runner = CommandRunner( 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - createFakePlugin('plugin', includeChangeLog: true, includeVersion: true); + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 0b25a5b015c..174dba1d5a4 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -8,6 +8,7 @@ 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/xctest_command.dart'; import 'package:test/test.dart'; @@ -85,31 +86,31 @@ void main() { const String _kSkip = '--skip'; group('test xctest_command', () { + FileSystem fileSystem; + Directory packagesDir; CommandRunner runner; RecordingProcessRunner processRunner; setUp(() { - initializeFakePackages(); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); final XCTestCommand command = - XCTestCommand(mockPackagesDir, processRunner: processRunner); + XCTestCommand(packagesDir, processRunner: processRunner); runner = CommandRunner('xctest_command', 'Test for xctest_command'); runner.addCommand(command); - cleanupPackages(); }); test('skip if ios is not supported', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isIosPlugin: false); - final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); + createFakePubspec(pluginDirectory.childDirectory('example'), + isFlutter: true); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -118,27 +119,27 @@ void main() { runner, ['xctest', _kDestination, 'foo_destination']); expect(output, contains('iOS is not supported by this plugin.')); expect(processRunner.recordedCalls, orderedEquals([])); - - cleanupPackages(); }); test('running with correct destination, skip 1 plugin', () async { - createFakePlugin('plugin1', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - createFakePlugin('plugin2', - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); + final Directory pluginDirectory1 = + createFakePlugin('plugin1', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + final Directory pluginDirectory2 = + createFakePlugin('plugin2', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); final Directory pluginExampleDirectory1 = - mockPackagesDir.childDirectory('plugin1').childDirectory('example'); + pluginDirectory1.childDirectory('example'); createFakePubspec(pluginExampleDirectory1, isFlutter: true); final Directory pluginExampleDirectory2 = - mockPackagesDir.childDirectory('plugin2').childDirectory('example'); + pluginDirectory2.childDirectory('example'); createFakePubspec(pluginExampleDirectory2, isFlutter: true); final MockProcess mockProcess = MockProcess(); @@ -178,20 +179,18 @@ void main() { ], pluginExampleDirectory2.path), ])); - - cleanupPackages(); }); test('Not specifying --ios-destination assigns an available simulator', () async { - createFakePlugin('plugin', + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, withExtraFiles: >[ ['example', 'test'], ], isIosPlugin: true); final Directory pluginExampleDirectory = - mockPackagesDir.childDirectory('plugin').childDirectory('example'); + pluginDirectory.childDirectory('example'); createFakePubspec(pluginExampleDirectory, isFlutter: true); @@ -234,8 +233,6 @@ void main() { ], pluginExampleDirectory.path), ])); - - cleanupPackages(); }); }); } From d491b95e695a7adbc65c52d31945ba95823426d6 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 7 Jun 2021 11:29:07 -0700 Subject: [PATCH 063/249] [flutter_plugin_tools] Remove xctest's --skip (#4022) --- script/tool/CHANGELOG.md | 4 ++++ script/tool/README.md | 4 ++-- script/tool/lib/src/xctest_command.dart | 10 ---------- script/tool/pubspec.yaml | 2 +- script/tool/test/xctest_command_test.dart | 7 +++---- 5 files changed, 10 insertions(+), 17 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 6250e2a7273..bd0875c2dbb 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.0 + +- Remove `xctest`'s `--skip`, which is redundant with `--ignore`. + ## 0.1.4 - Add a `pubspec-check` command diff --git a/script/tool/README.md b/script/tool/README.md index 3e9484c3ff0..8ff33a807b8 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -46,7 +46,7 @@ dart pub global run flutter_plugin_tools ## Commands Run with `--help` for a full list of commands and arguments, but the -following shows a number of common commands. +following shows a number of common commands being run for a specific plugin. All examples assume running from source; see above for running the published version instead. @@ -79,7 +79,7 @@ dart run ./script/tool/lib/src/main.dart test --plugins plugin_name ```sh cd -dart run ./script/tool/lib/src/main.dart xctest --target RunnerUITests --skip +dart run ./script/tool/lib/src/main.dart xctest --plugins plugin_name ``` ### Publish a Release diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index e41164e3ed8..c8775307bf2 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -14,7 +14,6 @@ import 'package:path/path.dart' as p; import 'common.dart'; const String _kiOSDestination = 'ios-destination'; -const String _kSkip = 'skip'; const String _kXcodeBuildCommand = 'xcodebuild'; const String _kXCRunCommand = 'xcrun'; const String _kFoundNoSimulatorsMessage = @@ -36,8 +35,6 @@ class XCTestCommand extends PluginCommand { 'this is passed to the `-destination` argument in xcodebuild command.\n' 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', ); - argParser.addMultiOption(_kSkip, - help: 'Plugins to skip while running this command. \n'); } @override @@ -59,8 +56,6 @@ class XCTestCommand extends PluginCommand { destination = 'id=$simulatorId'; } - final List skipped = getStringListArg(_kSkip); - final List failingPackages = []; await for (final Directory plugin in getPlugins()) { // Start running for package. @@ -72,11 +67,6 @@ class XCTestCommand extends PluginCommand { print('\n\n'); continue; } - if (skipped.contains(packageName)) { - print('$packageName was skipped with the --skip flag.'); - print('\n\n'); - continue; - } for (final Directory example in getExamplesForPlugin(plugin)) { // Running tests and static analyzer. print('Running tests and analyzer for $packageName ...'); diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index ab422daf8ed..5d2200abcdb 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.1.4 +version: 0.2.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 174dba1d5a4..ede23113477 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -83,7 +83,6 @@ final Map _kDeviceListMap = { void main() { const String _kDestination = '--ios-destination'; - const String _kSkip = '--skip'; group('test xctest_command', () { FileSystem fileSystem; @@ -121,7 +120,7 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('running with correct destination, skip 1 plugin', () async { + test('running with correct destination, exclude 1 plugin', () async { final Directory pluginDirectory1 = createFakePlugin('plugin1', packagesDir, withExtraFiles: >[ @@ -151,11 +150,11 @@ void main() { 'xctest', _kDestination, 'foo_destination', - _kSkip, + '--exclude', 'plugin1' ]); - expect(output, contains('plugin1 was skipped with the --skip flag.')); + expect(output, isNot(contains('Successfully ran xctest for plugin1'))); expect(output, contains('Successfully ran xctest for plugin2')); expect( From 181fe18e27f5866483acd851dcf639e76347b095 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 7 Jun 2021 11:44:05 -0700 Subject: [PATCH 064/249] [flutter_plugin_tools] Migrate xctest command to NNBD (#4024) --- script/tool/lib/src/xctest_command.dart | 40 ++++++++++++++--------- script/tool/test/mocks.dart | 3 ++ script/tool/test/xctest_command_test.dart | 13 ++++---- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index c8775307bf2..1b157ce1ae2 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_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. -// @dart=2.9 - import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -48,7 +46,7 @@ class XCTestCommand extends PluginCommand { Future run() async { String destination = getStringArg(_kiOSDestination); if (destination.isEmpty) { - final String simulatorId = await _findAvailableIphoneSimulator(); + final String? simulatorId = await _findAvailableIphoneSimulator(); if (simulatorId == null) { print(_kFoundNoSimulatorsMessage); throw ToolExit(1); @@ -119,7 +117,7 @@ class XCTestCommand extends PluginCommand { workingDir: example, exitOnError: false); } - Future _findAvailableIphoneSimulator() async { + Future _findAvailableIphoneSimulator() async { // Find the first available destination if not specified. final List findSimulatorsArguments = [ 'simctl', @@ -143,30 +141,40 @@ class XCTestCommand extends PluginCommand { final List> runtimes = (simulatorListJson['runtimes'] as List) .cast>(); - final Map devices = - simulatorListJson['devices'] as Map; + final Map devices = + (simulatorListJson['devices'] as Map) + .cast(); if (runtimes.isEmpty || devices.isEmpty) { return null; } - String id; + String? id; // Looking for runtimes, trying to find one with highest OS version. - for (final Map runtimeMap in runtimes.reversed) { - if (!(runtimeMap['name'] as String).contains('iOS')) { + for (final Map rawRuntimeMap in runtimes.reversed) { + final Map runtimeMap = + rawRuntimeMap.cast(); + if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { + continue; + } + final String? runtimeID = runtimeMap['identifier'] as String?; + if (runtimeID == null) { continue; } - final String runtimeID = runtimeMap['identifier'] as String; - final List> devicesForRuntime = - (devices[runtimeID] as List).cast>(); - if (devicesForRuntime.isEmpty) { + final List>? devicesForRuntime = + (devices[runtimeID] as List?)?.cast>(); + if (devicesForRuntime == null || devicesForRuntime.isEmpty) { continue; } // Looking for runtimes, trying to find latest version of device. - for (final Map device in devicesForRuntime.reversed) { + for (final Map rawDevice in devicesForRuntime.reversed) { + final Map device = rawDevice.cast(); if (device['availabilityError'] != null || - (device['isAvailable'] as bool == false)) { + (device['isAvailable'] as bool?) == false) { + continue; + } + id = device['udid'] as String?; + if (id == null) { continue; } - id = device['udid'] as String; print('device selected: $device'); return id; } diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index b984247af9a..66267ec5825 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -16,6 +16,9 @@ class MockProcess extends Mock implements io.Process { StreamController>(); final MockIOSink stdinMock = MockIOSink(); + @override + int get pid => 99; + @override Future get exitCode => exitCodeCompleter.future; diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index ede23113477..ffe9bf4267a 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.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. -// @dart=2.9 - import 'dart:convert'; import 'package:args/command_runner.dart'; @@ -15,6 +13,9 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; +// Note: This uses `dynamic` deliberately, and should not be updated to Object, +// in order to ensure that the code correctly handles this return type from +// JSON decoding. final Map _kDeviceListMap = { 'runtimes': >[ { @@ -85,10 +86,10 @@ void main() { const String _kDestination = '--ios-destination'; group('test xctest_command', () { - FileSystem fileSystem; - Directory packagesDir; - CommandRunner runner; - RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); From b98034dd76a4bff411011e28349f1bf2739f9ed2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 9 Jun 2021 11:00:04 -0700 Subject: [PATCH 065/249] [flutter_plugin_tools] Migrate more commands to NNBD (#4026) Migrates: - `all_plugins_app` - `podspecs` - `firebase-test-lab` Minor functional changes to `firebase-test-lab` based on issues highlighted by the migration: - The build ID used in the path is now a) passable, and b) given a fallback value in the path that isn't "null" - Flag setup will no longer assume that `$HOME` must be set in the environment. - Adds a --build-id flag to `firebase-test-lab` instead of hard-coding the use of `CIRRUS_BUILD_ID`. The default is still `CIRRUS_BUILD_ID` so no CI changes are needed. Part of https://github.com/flutter/flutter/issues/81912 --- script/tool/CHANGELOG.md | 6 ++ .../src/create_all_plugins_app_command.dart | 15 ++-- .../lib/src/firebase_test_lab_command.dart | 73 +++++++++++-------- .../tool/lib/src/lint_podspecs_command.dart | 5 +- .../create_all_plugins_app_command_test.dart | 10 +-- script/tool/test/firebase_test_lab_test.dart | 32 ++++---- .../tool/test/lint_podspecs_command_test.dart | 24 +++--- script/tool/test/mocks.dart | 8 ++ script/tool/test/util.dart | 10 +-- 9 files changed, 99 insertions(+), 84 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index bd0875c2dbb..2ada2cc30cb 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## NEXT + +- Add a --build-id flag to `firebase-test-lab` instead of hard-coding the use of + `CIRRUS_BUILD_ID`. `CIRRUS_BUILD_ID` is the default value for that flag, for backward + compatibility. + ## 0.2.0 - Remove `xctest`'s `--skip`, which is redundant with `--ignore`. 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 9de7f1b904a..cd5b85e45ac 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart=2.9 - -import 'dart:async'; import 'dart:io' as io; import 'package:file/file.dart'; @@ -18,17 +15,17 @@ class CreateAllPluginsAppCommand extends PluginCommand { /// Creates an instance of the builder command. CreateAllPluginsAppCommand( Directory packagesDir, { - this.pluginsRoot, - }) : super(packagesDir) { - pluginsRoot ??= packagesDir.fileSystem.currentDirectory; - appDirectory = pluginsRoot.childDirectory('all_plugins'); + Directory? pluginsRoot, + }) : pluginsRoot = pluginsRoot ?? packagesDir.fileSystem.currentDirectory, + super(packagesDir) { + appDirectory = this.pluginsRoot.childDirectory('all_plugins'); } /// The root directory of the plugin repository. Directory pluginsRoot; /// The location of the synthesized app project. - Directory appDirectory; + late Directory appDirectory; @override String get description => @@ -177,7 +174,7 @@ description: ${pubspec.description} version: ${pubspec.version} -environment:${_pubspecMapString(pubspec.environment)} +environment:${_pubspecMapString(pubspec.environment!)} dependencies:${_pubspecMapString(pubspec.dependencies)} diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 6db0d629e59..741d8569322 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_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. -// @dart=2.9 - import 'dart:async'; import 'dart:io' as io; @@ -27,15 +25,25 @@ class FirebaseTestLabCommand extends PluginCommand { defaultsTo: 'flutter-infra', help: 'The Firebase project name.', ); + final String? homeDir = io.Platform.environment['HOME']; argParser.addOption('service-key', defaultsTo: - p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); + homeDir == null ? null : p.join(homeDir, 'gcloud-service-key.json'), + help: 'The path to the service key for gcloud authentication.\n' + r'If not provided, \$HOME/gcloud-service-key.json will be ' + r'assumed if $HOME is set.'); argParser.addOption('test-run-id', defaultsTo: const Uuid().v4(), help: 'Optional string to append to the results path, to avoid conflicts. ' 'Randomly chosen on each invocation if none is provided. ' 'The default shown here is just an example.'); + argParser.addOption('build-id', + defaultsTo: + io.Platform.environment['CIRRUS_BUILD_ID'] ?? 'unknown_build', + help: + 'Optional string to append to the results path, to avoid conflicts. ' + r'Defaults to $CIRRUS_BUILD_ID if that is set.'); argParser.addMultiOption('device', splitCommas: false, defaultsTo: [ @@ -66,38 +74,43 @@ class FirebaseTestLabCommand extends PluginCommand { final Print _print; - Completer _firebaseProjectConfigured; + Completer? _firebaseProjectConfigured; Future _configureFirebaseProject() async { if (_firebaseProjectConfigured != null) { - return _firebaseProjectConfigured.future; - } else { - _firebaseProjectConfigured = Completer(); + return _firebaseProjectConfigured!.future; } - await processRunner.run( - 'gcloud', - [ - 'auth', - 'activate-service-account', - '--key-file=${getStringArg('service-key')}', - ], - exitOnError: true, - logOnError: true, - ); - final int exitCode = await processRunner.runAndStream('gcloud', [ - 'config', - 'set', - 'project', - getStringArg('project'), - ]); - if (exitCode == 0) { - _print('\nFirebase project configured.'); - return; + _firebaseProjectConfigured = Completer(); + + final String serviceKey = getStringArg('service-key'); + if (serviceKey.isEmpty) { + _print('No --service-key provided; skipping gcloud authorization'); } else { - _print( - '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + await processRunner.run( + 'gcloud', + [ + 'auth', + 'activate-service-account', + '--key-file=$serviceKey', + ], + exitOnError: true, + logOnError: true, + ); + final int exitCode = await processRunner.runAndStream('gcloud', [ + 'config', + 'set', + 'project', + getStringArg('project'), + ]); + if (exitCode == 0) { + _print('\nFirebase project configured.'); + return; + } else { + _print( + '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + } } - _firebaseProjectConfigured.complete(null); + _firebaseProjectConfigured!.complete(null); } @override @@ -212,7 +225,7 @@ class FirebaseTestLabCommand extends PluginCommand { failingPackages.add(packageName); continue; } - final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; + final String buildId = getStringArg('build-id'); final String testRunId = getStringArg('test-run-id'); final String resultsDir = 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 72bb6af3f64..364653bd13b 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart=2.9 - -import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -122,7 +119,7 @@ class LintPodspecsCommand extends PluginCommand { } Future _runPodLint(String podspecPath, - {bool libraryLint}) async { + {required bool libraryLint}) async { final bool allowWarnings = (getStringListArg('ignore-warnings')) .contains(p.basenameWithoutExtension(podspecPath)); final List arguments = [ diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index b3cbd592a63..5bde5e0dc00 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.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. -// @dart=2.9 - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; @@ -14,11 +12,11 @@ import 'util.dart'; void main() { group('$CreateAllPluginsAppCommand', () { - CommandRunner runner; + late CommandRunner runner; FileSystem fileSystem; - Directory testRoot; - Directory packagesDir; - Directory appDir; + late Directory testRoot; + late Directory packagesDir; + late Directory appDir; setUp(() { // Since the core of this command is a call to 'flutter create', the test diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index 74809007c29..aa8be17d679 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.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. -// @dart=2.9 - import 'dart:io'; import 'package:args/command_runner.dart'; @@ -19,10 +17,10 @@ import 'util.dart'; void main() { group('$FirebaseTestLabCommand', () { FileSystem fileSystem; - Directory packagesDir; - List printedMessages; - CommandRunner runner; - RecordingProcessRunner processRunner; + late Directory packagesDir; + late List printedMessages; + late CommandRunner runner; + late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); @@ -31,7 +29,7 @@ void main() { processRunner = RecordingProcessRunner(); final FirebaseTestLabCommand command = FirebaseTestLabCommand(packagesDir, processRunner: processRunner, - print: (Object message) => printedMessages.add(message.toString())); + print: (Object? message) => printedMessages.add(message.toString())); runner = CommandRunner( 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); @@ -97,6 +95,8 @@ void main() { 'model=seoul,version=26', '--test-run-id', 'testRunId', + '--build-id', + 'buildId', ]); expect( @@ -130,7 +130,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -140,7 +140,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -150,7 +150,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -160,7 +160,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -196,6 +196,8 @@ void main() { 'model=flame,version=29', '--test-run-id', 'testRunId', + '--build-id', + 'buildId', '--enable-experiment=exp1', ]); @@ -221,7 +223,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -231,7 +233,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -241,7 +243,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/2/ --device model=flame,version=29' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -251,7 +253,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/3/ --device model=flame,version=29' .split(' '), '/packages/plugin/example'), ]), diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 349607b0ca7..0183704f72c 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -2,15 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart=2.9 - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; -import 'package:mockito/mockito.dart'; import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -19,24 +15,24 @@ import 'util.dart'; void main() { group('$LintPodspecsCommand', () { FileSystem fileSystem; - Directory packagesDir; - CommandRunner runner; - MockPlatform mockPlatform; - final RecordingProcessRunner processRunner = RecordingProcessRunner(); - List printedMessages; + late Directory packagesDir; + late CommandRunner runner; + late MockPlatform mockPlatform; + late RecordingProcessRunner processRunner; + late List printedMessages; setUp(() { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); printedMessages = []; - mockPlatform = MockPlatform(); - when(mockPlatform.isMacOS).thenReturn(true); + mockPlatform = MockPlatform(isMacOS: true); + processRunner = RecordingProcessRunner(); final LintPodspecsCommand command = LintPodspecsCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, - print: (Object message) => printedMessages.add(message.toString()), + print: (Object? message) => printedMessages.add(message.toString()), ); runner = @@ -53,7 +49,7 @@ void main() { ['plugin1.podspec'], ]); - when(mockPlatform.isMacOS).thenReturn(false); + mockPlatform.isMacOS = false; await runner.run(['podspecs']); expect( @@ -172,5 +168,3 @@ void main() { }); }); } - -class MockPlatform extends Mock implements Platform {} diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 66267ec5825..ba6a03da7bc 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -7,6 +7,14 @@ import 'dart:io' as io; import 'package:file/file.dart'; import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; + +class MockPlatform extends Mock implements Platform { + MockPlatform({this.isMacOS = false}); + + @override + bool isMacOS; +} class MockProcess extends Mock implements io.Process { final Completer exitCodeCompleter = Completer(); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index a0a316f95df..c9d4ed23d08 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -257,13 +257,13 @@ class RecordingProcessRunner extends ProcessRunner { Encoding stderrEncoding = io.systemEncoding, }) async { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - io.ProcessResult? result; final io.Process? process = processToReturn; - if (process != null) { - result = io.ProcessResult(process.pid, await process.exitCode, - resultStdout ?? process.stdout, resultStderr ?? process.stderr); - } + final io.ProcessResult result = process == null + ? io.ProcessResult(1, 1, '', '') + : io.ProcessResult(process.pid, await process.exitCode, + resultStdout ?? process.stdout, resultStderr ?? process.stderr); + return Future.value(result); } From cb92e5d416427f2667d77fa8c844bc243bbd1d84 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 10 Jun 2021 14:50:20 -0700 Subject: [PATCH 066/249] Enable macOS XCTest support (#4043) - Adds macOS support to the `xctest` tool command - Adds logic to the tool to check for packages that delegate their implementations to another package, so they can be skipped when running native unit tests - Updates the tool's unit test utility for writing pubspecs to be able to make delegated federated implementation references to test it - Adds initial unit tests to the non-deprecated macOS plugins - Enables macOS XCTesting in CI macOS portion of https://github.com/flutter/flutter/issues/82445 --- script/tool/CHANGELOG.md | 2 + script/tool/README.md | 5 +- .../tool/lib/src/build_examples_command.dart | 38 +- script/tool/lib/src/common.dart | 75 ++- .../tool/lib/src/drive_examples_command.dart | 32 +- script/tool/lib/src/xctest_command.dart | 119 +++- script/tool/test/common_test.dart | 621 ++++++++++++------ .../tool/test/publish_check_command_test.dart | 18 +- .../test/publish_plugin_command_test.dart | 79 +-- script/tool/test/util.dart | 134 ++-- script/tool/test/version_check_test.dart | 21 +- script/tool/test/xctest_command_test.dart | 377 +++++++---- 12 files changed, 964 insertions(+), 557 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2ada2cc30cb..6b3d96b4f35 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -3,6 +3,8 @@ - Add a --build-id flag to `firebase-test-lab` instead of hard-coding the use of `CIRRUS_BUILD_ID`. `CIRRUS_BUILD_ID` is the default value for that flag, for backward compatibility. +- `xctest` now supports running macOS tests in addition to iOS + - **Breaking change**: it now requires an `--ios` and/or `--macos` flag. ## 0.2.0 diff --git a/script/tool/README.md b/script/tool/README.md index 8ff33a807b8..c0bcd7c5e10 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -79,7 +79,10 @@ dart run ./script/tool/lib/src/main.dart test --plugins plugin_name ```sh cd -dart run ./script/tool/lib/src/main.dart xctest --plugins plugin_name +# For iOS: +dart run ./script/tool/lib/src/main.dart xctest --ios --plugins plugin_name +# For macOS: +dart run ./script/tool/lib/src/main.dart xctest --macos --plugins plugin_name ``` ### Publish a Release diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 82fb12e70d4..9590aecef98 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -11,6 +11,12 @@ import 'package:platform/platform.dart'; import 'common.dart'; +/// Key for IPA. +const String kIpa = 'ipa'; + +/// Key for APK. +const String kApk = 'apk'; + /// A command to build the example applications for packages. class BuildExamplesCommand extends PluginCommand { /// Creates an instance of the build command. @@ -18,10 +24,10 @@ class BuildExamplesCommand extends PluginCommand { Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), }) : super(packagesDir, processRunner: processRunner) { - argParser.addFlag(kLinux, defaultsTo: false); - argParser.addFlag(kMacos, defaultsTo: false); - argParser.addFlag(kWeb, defaultsTo: false); - argParser.addFlag(kWindows, defaultsTo: false); + argParser.addFlag(kPlatformFlagLinux, defaultsTo: false); + argParser.addFlag(kPlatformFlagMacos, defaultsTo: false); + argParser.addFlag(kPlatformFlagWeb, defaultsTo: false); + argParser.addFlag(kPlatformFlagWindows, defaultsTo: false); argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); argParser.addFlag(kApk); argParser.addOption( @@ -44,10 +50,10 @@ class BuildExamplesCommand extends PluginCommand { final List platformSwitches = [ kApk, kIpa, - kLinux, - kMacos, - kWeb, - kWindows, + kPlatformFlagLinux, + kPlatformFlagMacos, + kPlatformFlagWeb, + kPlatformFlagWindows, ]; if (!platformSwitches.any((String platform) => getBoolArg(platform))) { print( @@ -66,14 +72,14 @@ class BuildExamplesCommand extends PluginCommand { final String packageName = p.relative(example.path, from: packagesDir.path); - if (getBoolArg(kLinux)) { + if (getBoolArg(kPlatformFlagLinux)) { print('\nBUILDING Linux for $packageName'); if (isLinuxPlugin(plugin)) { final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ 'build', - kLinux, + kPlatformFlagLinux, if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], @@ -86,14 +92,14 @@ class BuildExamplesCommand extends PluginCommand { } } - if (getBoolArg(kMacos)) { + if (getBoolArg(kPlatformFlagMacos)) { print('\nBUILDING macOS for $packageName'); if (isMacOsPlugin(plugin)) { final int exitCode = await processRunner.runAndStream( flutterCommand, [ 'build', - kMacos, + kPlatformFlagMacos, if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], @@ -106,14 +112,14 @@ class BuildExamplesCommand extends PluginCommand { } } - if (getBoolArg(kWeb)) { + if (getBoolArg(kPlatformFlagWeb)) { print('\nBUILDING web for $packageName'); if (isWebPlugin(plugin)) { final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ 'build', - kWeb, + kPlatformFlagWeb, if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], @@ -126,14 +132,14 @@ class BuildExamplesCommand extends PluginCommand { } } - if (getBoolArg(kWindows)) { + if (getBoolArg(kPlatformFlagWindows)) { print('\nBUILDING Windows for $packageName'); if (isWindowsPlugin(plugin)) { final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ 'build', - kWindows, + kPlatformFlagWindows, if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index f58290b5e07..20e8479e6d4 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -21,28 +21,22 @@ import 'package:yaml/yaml.dart'; typedef Print = void Function(Object? object); /// Key for windows platform. -const String kWindows = 'windows'; +const String kPlatformFlagWindows = 'windows'; /// Key for macos platform. -const String kMacos = 'macos'; +const String kPlatformFlagMacos = 'macos'; /// Key for linux platform. -const String kLinux = 'linux'; +const String kPlatformFlagLinux = 'linux'; /// Key for IPA (iOS) platform. -const String kIos = 'ios'; +const String kPlatformFlagIos = 'ios'; /// Key for APK (Android) platform. -const String kAndroid = 'android'; +const String kPlatformFlagAndroid = 'android'; /// Key for Web platform. -const String kWeb = 'web'; - -/// Key for IPA. -const String kIpa = 'ipa'; - -/// Key for APK. -const String kApk = 'apk'; +const String kPlatformFlagWeb = 'web'; /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; @@ -69,6 +63,15 @@ bool isFlutterPackage(FileSystemEntity entity) { } } +/// 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: @@ -77,13 +80,17 @@ bool isFlutterPackage(FileSystemEntity entity) { /// plugin: /// platforms: /// [platform]: -bool pluginSupportsPlatform(String platform, FileSystemEntity entity) { - assert(platform == kIos || - platform == kAndroid || - platform == kWeb || - platform == kMacos || - platform == kWindows || - platform == kLinux); +/// +/// 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; } @@ -102,13 +109,25 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity) { } final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; if (platforms == null) { - // Legacy plugin specs are assumed to support iOS and Android. + // 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 == kIos || platform == kAndroid; + return platform == kPlatformFlagIos || platform == kPlatformFlagAndroid; } return false; } - return platforms.containsKey(platform); + 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 { @@ -118,32 +137,32 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity) { /// Returns whether the given directory contains a Flutter Android plugin. bool isAndroidPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kAndroid, entity); + return pluginSupportsPlatform(kPlatformFlagAndroid, entity); } /// Returns whether the given directory contains a Flutter iOS plugin. bool isIosPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kIos, entity); + return pluginSupportsPlatform(kPlatformFlagIos, entity); } /// Returns whether the given directory contains a Flutter web plugin. bool isWebPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kWeb, entity); + return pluginSupportsPlatform(kPlatformFlagWeb, entity); } /// Returns whether the given directory contains a Flutter Windows plugin. bool isWindowsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kWindows, entity); + return pluginSupportsPlatform(kPlatformFlagWindows, entity); } /// Returns whether the given directory contains a Flutter macOS plugin. bool isMacOsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kMacos, entity); + return pluginSupportsPlatform(kPlatformFlagMacos, entity); } /// Returns whether the given directory contains a Flutter linux plugin. bool isLinuxPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kLinux, entity); + return pluginSupportsPlatform(kPlatformFlagLinux, entity); } /// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red. diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 4678a9de3f1..14dfede5b2f 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -15,17 +15,17 @@ class DriveExamplesCommand extends PluginCommand { Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), }) : super(packagesDir, processRunner: processRunner) { - argParser.addFlag(kAndroid, + argParser.addFlag(kPlatformFlagAndroid, help: 'Runs the Android implementation of the examples'); - argParser.addFlag(kIos, + argParser.addFlag(kPlatformFlagIos, help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(kLinux, + argParser.addFlag(kPlatformFlagLinux, help: 'Runs the Linux implementation of the examples'); - argParser.addFlag(kMacos, + argParser.addFlag(kPlatformFlagMacos, help: 'Runs the macOS implementation of the examples'); - argParser.addFlag(kWeb, + argParser.addFlag(kPlatformFlagWeb, help: 'Runs the web implementation of the examples'); - argParser.addFlag(kWindows, + argParser.addFlag(kPlatformFlagWindows, help: 'Runs the Windows implementation of the examples'); argParser.addOption( kEnableExperiment, @@ -52,10 +52,10 @@ class DriveExamplesCommand extends PluginCommand { Future run() async { final List failingTests = []; final List pluginsWithoutTests = []; - final bool isLinux = getBoolArg(kLinux); - final bool isMacos = getBoolArg(kMacos); - final bool isWeb = getBoolArg(kWeb); - final bool isWindows = getBoolArg(kWindows); + final bool isLinux = getBoolArg(kPlatformFlagLinux); + final bool isMacos = getBoolArg(kPlatformFlagMacos); + final bool isWeb = getBoolArg(kPlatformFlagWeb); + final bool isWindows = getBoolArg(kPlatformFlagWindows); await for (final Directory plugin in getPlugins()) { final String pluginName = plugin.basename; if (pluginName.endsWith('_platform_interface') && @@ -219,12 +219,12 @@ Tried searching for the following: Future _pluginSupportedOnCurrentPlatform( FileSystemEntity plugin) async { - final bool isAndroid = getBoolArg(kAndroid); - final bool isIOS = getBoolArg(kIos); - final bool isLinux = getBoolArg(kLinux); - final bool isMacos = getBoolArg(kMacos); - final bool isWeb = getBoolArg(kWeb); - final bool isWindows = getBoolArg(kWindows); + final bool isAndroid = getBoolArg(kPlatformFlagAndroid); + final bool isIOS = getBoolArg(kPlatformFlagIos); + final bool isLinux = getBoolArg(kPlatformFlagLinux); + final bool isMacos = getBoolArg(kPlatformFlagMacos); + final bool isWeb = getBoolArg(kPlatformFlagWeb); + final bool isWindows = getBoolArg(kPlatformFlagWindows); if (isAndroid) { return isAndroidPlugin(plugin); } diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 1b157ce1ae2..288851ca7ed 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -17,8 +17,10 @@ const String _kXCRunCommand = 'xcrun'; const String _kFoundNoSimulatorsMessage = 'Cannot find any available simulators, tests failed'; -/// The command to run iOS XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. -/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcworkspace". +/// The command to run XCTests (XCUnitTest and XCUITest) in plugins. +/// The tests target have to be added to the Xcode project of the example app, +/// usually at "example/{ios,macos}/Runner.xcworkspace". +/// /// The static analyzer is also run. class XCTestCommand extends PluginCommand { /// Creates an instance of the test command. @@ -33,52 +35,61 @@ class XCTestCommand extends PluginCommand { 'this is passed to the `-destination` argument in xcodebuild command.\n' 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', ); + argParser.addFlag(kPlatformFlagIos, help: 'Runs the iOS tests'); + argParser.addFlag(kPlatformFlagMacos, help: 'Runs the macOS tests'); } @override final String name = 'xctest'; @override - final String description = 'Runs the xctests in the iOS example apps.\n\n' + final String description = + 'Runs the xctests in the iOS and/or macOS example apps.\n\n' 'This command requires "flutter" and "xcrun" to be in your path.'; @override Future run() async { - String destination = getStringArg(_kiOSDestination); - if (destination.isEmpty) { - final String? simulatorId = await _findAvailableIphoneSimulator(); - if (simulatorId == null) { - print(_kFoundNoSimulatorsMessage); - throw ToolExit(1); + final bool testIos = getBoolArg(kPlatformFlagIos); + final bool testMacos = getBoolArg(kPlatformFlagMacos); + + if (!(testIos || testMacos)) { + print('At least one platform flag must be provided.'); + throw ToolExit(2); + } + + List iosDestinationFlags = []; + if (testIos) { + String destination = getStringArg(_kiOSDestination); + if (destination.isEmpty) { + final String? simulatorId = await _findAvailableIphoneSimulator(); + if (simulatorId == null) { + print(_kFoundNoSimulatorsMessage); + throw ToolExit(1); + } + destination = 'id=$simulatorId'; } - destination = 'id=$simulatorId'; + iosDestinationFlags = [ + '-destination', + destination, + ]; } final List failingPackages = []; await for (final Directory plugin in getPlugins()) { - // Start running for package. final String packageName = p.relative(plugin.path, from: packagesDir.path); - print('Start running for $packageName ...'); - if (!isIosPlugin(plugin)) { - print('iOS is not supported by this plugin.'); - print('\n\n'); - continue; + print('============================================================'); + print('Start running for $packageName...'); + bool passed = true; + if (testIos) { + passed &= await _testPlugin(plugin, 'iOS', + extraXcrunFlags: iosDestinationFlags); } - for (final Directory example in getExamplesForPlugin(plugin)) { - // Running tests and static analyzer. - print('Running tests and analyzer for $packageName ...'); - int exitCode = await _runTests(true, destination, example); - // 66 = there is no test target (this fails fast). Try again with just the analyzer. - if (exitCode == 66) { - print('Tests not found for $packageName, running analyzer only...'); - exitCode = await _runTests(false, destination, example); - } - if (exitCode == 0) { - print('Successfully ran xctest for $packageName'); - } else { - failingPackages.add(packageName); - } + if (testMacos) { + passed &= await _testPlugin(plugin, 'macOS'); + } + if (!passed) { + failingPackages.add(packageName); } } @@ -95,19 +106,59 @@ class XCTestCommand extends PluginCommand { } } - Future _runTests(bool runTests, String destination, Directory example) { + /// Runs all applicable tests for [plugin], printing status and returning + /// success if the tests passed (or did not exist). + Future _testPlugin( + Directory plugin, + String platform, { + List extraXcrunFlags = const [], + }) async { + if (!pluginSupportsPlatform(platform.toLowerCase(), plugin, + requiredMode: PlatformSupport.inline)) { + print('$platform is not implemented by this plugin package.'); + print('\n'); + return true; + } + bool passing = true; + for (final Directory example in getExamplesForPlugin(plugin)) { + // Running tests and static analyzer. + final String examplePath = + p.relative(example.path, from: plugin.parent.path); + print('Running $platform tests and analyzer for $examplePath...'); + int exitCode = + await _runTests(true, example, platform, extraFlags: extraXcrunFlags); + // 66 = there is no test target (this fails fast). Try again with just the analyzer. + if (exitCode == 66) { + print('Tests not found for $examplePath, running analyzer only...'); + exitCode = await _runTests(false, example, platform, + extraFlags: extraXcrunFlags); + } + if (exitCode == 0) { + print('Successfully ran $platform xctest for $examplePath'); + } else { + passing = false; + } + } + return passing; + } + + Future _runTests( + bool runTests, + Directory example, + String platform, { + List extraFlags = const [], + }) { final List xctestArgs = [ _kXcodeBuildCommand, if (runTests) 'test', 'analyze', '-workspace', - 'ios/Runner.xcworkspace', + '${platform.toLowerCase()}/Runner.xcworkspace', '-configuration', 'Debug', '-scheme', 'Runner', - '-destination', - destination, + ...extraFlags, 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ]; final String completeTestCommand = diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index 2f497963d22..0516fab1da3 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -65,258 +65,301 @@ void main() { runner.addCommand(samplePluginCommand); }); - test('all plugins from file system', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run(['sample']); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); - - test('all plugins includes third_party/packages', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - final Directory plugin3 = - createFakePlugin('plugin3', thirdPartyPackagesDir); - await runner.run(['sample']); - expect(plugins, - unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); - }); - - test('exclude plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); - expect(plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude plugins when plugins flag isn\'t specified', () async { - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - await runner.run(['sample', '--exclude=plugin1,plugin2']); - expect(plugins, unorderedEquals([])); - }); - - test('exclude federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', packagesDir, parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ - 'sample', - '--plugins=federated/plugin1,plugin2', - '--exclude=federated/plugin1' - ]); - expect(plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude entire federated plugins when plugins flag is specified', - () async { - createFakePlugin('plugin1', packagesDir, parentDirectoryName: 'federated'); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ - 'sample', - '--plugins=federated/plugin1,plugin2', - '--exclude=federated' - ]); - expect(plugins, unorderedEquals([plugin2.path])); - }); - - group('test run-on-changed-packages', () { - test('all plugins should be tested if there are no changes.', () async { + group('plugin iteration', () { + test('all plugins from file system', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); - + await runner.run(['sample']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); - test('all plugins should be tested if there are no plugin related changes.', - () async { - gitDiffResponse = 'AUTHORS'; + test('all plugins includes third_party/packages', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final Directory plugin3 = + createFakePlugin('plugin3', thirdPartyPackagesDir); + await runner.run(['sample']); + expect(plugins, + unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); + }); + + test('exclude plugins when plugins flag is specified', () async { + createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); + ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); + expect(plugins, unorderedEquals([plugin2.path])); + }); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + test('exclude plugins when plugins flag isn\'t specified', () async { + createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + await runner.run(['sample', '--exclude=plugin1,plugin2']); + expect(plugins, unorderedEquals([])); }); - test('all plugins should be tested if .cirrus.yml changes.', () async { - gitDiffResponse = ''' -.cirrus.yml -packages/plugin1/CHANGELOG -'''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + test('exclude federated plugins when plugins flag is specified', () async { + createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'federated'); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated/plugin1' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + test('exclude entire federated plugins when plugins flag is specified', + () async { + createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated' + ]); + expect(plugins, unorderedEquals([plugin2.path])); }); - test('all plugins should be tested if .ci.yaml changes', () async { - gitDiffResponse = ''' -.ci.yaml + group('test run-on-changed-packages', () { + test('all plugins should be tested if there are no changes.', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test( + 'all plugins should be tested if there are no plugin related changes.', + () async { + gitDiffResponse = 'AUTHORS'; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if .cirrus.yml changes.', () async { + gitDiffResponse = ''' +.cirrus.yml packages/plugin1/CHANGELOG '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); + test('all plugins should be tested if .ci.yaml changes', () async { + gitDiffResponse = ''' +.ci.yaml +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); - test('all plugins should be tested if anything in .ci/ changes', () async { - gitDiffResponse = ''' + test('all plugins should be tested if anything in .ci/ changes', + () async { + gitDiffResponse = ''' .ci/Dockerfile packages/plugin1/CHANGELOG '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); - - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); - test('all plugins should be tested if anything in script changes.', - () async { - gitDiffResponse = ''' + test('all plugins should be tested if anything in script changes.', + () async { + gitDiffResponse = ''' script/tool_runner.sh packages/plugin1/CHANGELOG '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); - - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); - test('all plugins should be tested if the root analysis options change.', - () async { - gitDiffResponse = ''' + test('all plugins should be tested if the root analysis options change.', + () async { + gitDiffResponse = ''' analysis_options.yaml packages/plugin1/CHANGELOG '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); - - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); - test('all plugins should be tested if formatting options change.', - () async { - gitDiffResponse = ''' + test('all plugins should be tested if formatting options change.', + () async { + gitDiffResponse = ''' .clang-format packages/plugin1/CHANGELOG '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); - - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); - - test('Only changed plugin should be tested.', () async { - gitDiffResponse = 'packages/plugin1/plugin1.dart'; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); - expect(plugins, unorderedEquals([plugin1.path])); - }); + test('Only changed plugin should be tested.', () async { + gitDiffResponse = 'packages/plugin1/plugin1.dart'; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); - test('multiple files in one plugin should also test the plugin', () async { - gitDiffResponse = ''' + test('multiple files in one plugin should also test the plugin', + () async { + gitDiffResponse = ''' packages/plugin1/plugin1.dart packages/plugin1/ios/plugin1.m '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); - - expect(plugins, unorderedEquals([plugin1.path])); - }); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); - test('multiple plugins changed should test all the changed plugins', - () async { - gitDiffResponse = ''' + test('multiple plugins changed should test all the changed plugins', + () async { + gitDiffResponse = ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); - - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); - test( - 'multiple plugins inside the same plugin group changed should output the plugin group name', - () async { - gitDiffResponse = ''' + test( + 'multiple plugins inside the same plugin group changed should output the plugin group name', + () async { + gitDiffResponse = ''' packages/plugin1/plugin1/plugin1.dart packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart packages/plugin1/plugin1_web/plugin1_web.dart '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir, - parentDirectoryName: 'plugin1'); - createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runner.run( - ['sample', '--base-sha=master', '--run-on-changed-packages']); - - expect(plugins, unorderedEquals([plugin1.path])); - }); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); - test('--plugins flag overrides the behavior of --run-on-changed-packages', - () async { - gitDiffResponse = ''' + test('--plugins flag overrides the behavior of --run-on-changed-packages', + () async { + gitDiffResponse = ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir, - parentDirectoryName: 'plugin1'); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runner.run([ - 'sample', - '--plugins=plugin1,plugin2', - '--base-sha=master', - '--run-on-changed-packages' - ]); - - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); - }); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'plugin1'); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runner.run([ + 'sample', + '--plugins=plugin1,plugin2', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); - test('--exclude flag works with --run-on-changed-packages', () async { - gitDiffResponse = ''' + test('--exclude flag works with --run-on-changed-packages', () async { + gitDiffResponse = ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir, - parentDirectoryName: 'plugin1'); - createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runner.run([ - 'sample', - '--exclude=plugin2,plugin3', - '--base-sha=master', - '--run-on-changed-packages' - ]); - - expect(plugins, unorderedEquals([plugin1.path])); + final Directory plugin1 = createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runner.run([ + 'sample', + '--exclude=plugin2,plugin3', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); }); }); @@ -478,6 +521,196 @@ file2/file2.cc 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/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 6d36031a264..11d703177d5 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -193,10 +193,8 @@ void main() { final Directory plugin2Dir = createFakePlugin('no_publish_b', packagesDir, includeVersion: true); - createFakePubspec(plugin1Dir, - name: 'no_publish_a', includeVersion: true, version: '0.1.0'); - createFakePubspec(plugin2Dir, - name: 'no_publish_b', includeVersion: true, version: '0.2.0'); + createFakePubspec(plugin1Dir, name: 'no_publish_a', version: '0.1.0'); + createFakePubspec(plugin2Dir, name: 'no_publish_b', version: '0.2.0'); processRunner.processesToReturn.add( MockProcess()..exitCodeCompleter.complete(0), @@ -259,10 +257,8 @@ void main() { final Directory plugin2Dir = createFakePlugin('no_publish_b', packagesDir, includeVersion: true); - createFakePubspec(plugin1Dir, - name: 'no_publish_a', includeVersion: true, version: '0.1.0'); - createFakePubspec(plugin2Dir, - name: 'no_publish_b', includeVersion: true, version: '0.2.0'); + createFakePubspec(plugin1Dir, name: 'no_publish_a', version: '0.1.0'); + createFakePubspec(plugin2Dir, name: 'no_publish_b', version: '0.2.0'); processRunner.processesToReturn.add( MockProcess()..exitCodeCompleter.complete(0), @@ -328,10 +324,8 @@ void main() { final Directory plugin2Dir = createFakePlugin('no_publish_b', packagesDir, includeVersion: true); - createFakePubspec(plugin1Dir, - name: 'no_publish_a', includeVersion: true, version: '0.1.0'); - createFakePubspec(plugin2Dir, - name: 'no_publish_b', includeVersion: true, version: '0.2.0'); + createFakePubspec(plugin1Dir, name: 'no_publish_a', version: '0.1.0'); + createFakePubspec(plugin2Dir, name: 'no_publish_b', version: '0.2.0'); await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); processRunner.processesToReturn.add( diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 570ceb234a8..14987d47a40 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -53,7 +53,7 @@ void main() { pluginDir = createFakePlugin(testPluginName, packagesDir, withSingleExample: false); assert(pluginDir != null && pluginDir.existsSync()); - createFakePubspec(pluginDir, includeVersion: true); + createFakePubspec(pluginDir, version: '0.0.1'); io.Process.runSync('git', ['init'], workingDirectory: testRoot.path); gitDir = await GitDir.fromExisting(testRoot.path); @@ -139,7 +139,7 @@ void main() { }); test('can publish non-flutter package', () async { - createFakePubspec(pluginDir, includeVersion: true, isFlutter: false); + createFakePubspec(pluginDir, version: '0.0.1', isFlutter: false); io.Process.runSync('git', ['init'], workingDirectory: testRoot.path); gitDir = await GitDir.fromExisting(testRoot.path); @@ -435,15 +435,9 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, - name: 'plugin1', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin1', isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, - name: 'plugin2', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin2', isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -477,10 +471,7 @@ void main() { final Directory pluginDir0 = createFakePlugin('plugin0', packagesDir, withSingleExample: true); createFakePubspec(pluginDir0, - name: 'plugin0', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin0', isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -497,15 +488,9 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, - name: 'plugin1', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin1', isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, - name: 'plugin2', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin2', isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -539,15 +524,9 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, - name: 'plugin1', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin1', isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, - name: 'plugin2', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin2', isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. @@ -586,15 +565,9 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, - name: 'plugin1', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin1', isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, - name: 'plugin2', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin2', isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -675,15 +648,9 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, - name: 'plugin1', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin1', isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, - name: 'plugin2', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin2', isFlutter: false, version: '0.0.1'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -761,15 +728,9 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, - name: 'plugin1', - includeVersion: true, - isFlutter: false, - version: '0.0.2'); + name: 'plugin1', isFlutter: false, version: '0.0.2'); createFakePubspec(pluginDir2, - name: 'plugin2', - includeVersion: true, - isFlutter: false, - version: '0.0.2'); + name: 'plugin2', isFlutter: false, version: '0.0.2'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -841,15 +802,9 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, withSingleExample: true, parentDirectoryName: 'plugin2'); createFakePubspec(pluginDir1, - name: 'plugin1', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin1', isFlutter: false, version: '0.0.1'); createFakePubspec(pluginDir2, - name: 'plugin2', - includeVersion: true, - isFlutter: false, - version: '0.0.1'); + name: 'plugin2', isFlutter: false, version: '0.0.1'); io.Process.runSync('git', ['init'], workingDirectory: testRoot.path); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index c9d4ed23d08..c590d8a4bb0 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -38,6 +38,7 @@ Directory createFakePlugin( List withExamples = const [], List> withExtraFiles = const >[], bool isFlutter = true, + // TODO(stuartmorgan): Change these platform switches to support type enums. bool isAndroidPlugin = false, bool isIosPlugin = false, bool isWebPlugin = false, @@ -62,14 +63,13 @@ Directory createFakePlugin( createFakePubspec(pluginDirectory, name: name, isFlutter: isFlutter, - isAndroidPlugin: isAndroidPlugin, - isIosPlugin: isIosPlugin, - isWebPlugin: isWebPlugin, - isLinuxPlugin: isLinuxPlugin, - isMacOsPlugin: isMacOsPlugin, - isWindowsPlugin: isWindowsPlugin, - includeVersion: includeVersion, - version: version); + androidSupport: isAndroidPlugin ? PlatformSupport.inline : null, + iosSupport: isIosPlugin ? PlatformSupport.inline : null, + webSupport: isWebPlugin ? PlatformSupport.inline : null, + linuxSupport: isLinuxPlugin ? PlatformSupport.inline : null, + macosSupport: isMacOsPlugin ? PlatformSupport.inline : null, + windowsSupport: isWindowsPlugin ? PlatformSupport.inline : null, + version: includeVersion ? version : null); if (includeChangeLog) { createFakeCHANGELOG(pluginDirectory, ''' ## 0.0.1 @@ -81,10 +81,7 @@ Directory createFakePlugin( final Directory exampleDir = pluginDirectory.childDirectory('example') ..createSync(); createFakePubspec(exampleDir, - name: '${name}_example', - isFlutter: isFlutter, - includeVersion: false, - publishTo: 'none'); + name: '${name}_example', isFlutter: isFlutter, publishTo: 'none'); } else if (withExamples.isNotEmpty) { final Directory exampleDir = pluginDirectory.childDirectory('example') ..createSync(); @@ -92,10 +89,7 @@ Directory createFakePlugin( final Directory currentExample = exampleDir.childDirectory(example) ..createSync(); createFakePubspec(currentExample, - name: example, - isFlutter: isFlutter, - includeVersion: false, - publishTo: 'none'); + name: example, isFlutter: isFlutter, publishTo: 'none'); } } @@ -119,15 +113,14 @@ void createFakePubspec( Directory parent, { String name = 'fake_package', bool isFlutter = true, - bool includeVersion = false, - bool isAndroidPlugin = false, - bool isIosPlugin = false, - bool isWebPlugin = false, - bool isLinuxPlugin = false, - bool isMacOsPlugin = false, - bool isWindowsPlugin = false, + PlatformSupport? androidSupport, + PlatformSupport? iosSupport, + PlatformSupport? linuxSupport, + PlatformSupport? macosSupport, + PlatformSupport? webSupport, + PlatformSupport? windowsSupport, String publishTo = 'http://no_pub_server.com', - String version = '0.0.1', + String? version, }) { parent.childFile('pubspec.yaml').createSync(); String yaml = ''' @@ -136,43 +129,23 @@ flutter: plugin: platforms: '''; - if (isAndroidPlugin) { - yaml += ''' - android: - package: io.flutter.plugins.fake - pluginClass: FakePlugin -'''; + if (androidSupport != null) { + yaml += _pluginPlatformSection('android', androidSupport, name); } - if (isIosPlugin) { - yaml += ''' - ios: - pluginClass: FLTFakePlugin -'''; + if (iosSupport != null) { + yaml += _pluginPlatformSection('ios', iosSupport, name); } - if (isWebPlugin) { - yaml += ''' - web: - pluginClass: FakePlugin - fileName: ${name}_web.dart -'''; + if (webSupport != null) { + yaml += _pluginPlatformSection('web', webSupport, name); } - if (isLinuxPlugin) { - yaml += ''' - linux: - pluginClass: FakePlugin -'''; + if (linuxSupport != null) { + yaml += _pluginPlatformSection('linux', linuxSupport, name); } - if (isMacOsPlugin) { - yaml += ''' - macos: - pluginClass: FakePlugin -'''; + if (macosSupport != null) { + yaml += _pluginPlatformSection('macos', macosSupport, name); } - if (isWindowsPlugin) { - yaml += ''' - windows: - pluginClass: FakePlugin -'''; + if (windowsSupport != null) { + yaml += _pluginPlatformSection('windows', windowsSupport, name); } if (isFlutter) { yaml += ''' @@ -181,7 +154,7 @@ dependencies: sdk: flutter '''; } - if (includeVersion) { + if (version != null) { yaml += ''' version: $version '''; @@ -194,6 +167,53 @@ publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being parent.childFile('pubspec.yaml').writeAsStringSync(yaml); } +String _pluginPlatformSection( + String platform, PlatformSupport type, String packageName) { + if (type == PlatformSupport.federated) { + return ''' + $platform: + default_package: ${packageName}_$platform +'''; + } + switch (platform) { + case 'android': + return ''' + android: + package: io.flutter.plugins.fake + pluginClass: FakePlugin +'''; + case 'ios': + return ''' + ios: + pluginClass: FLTFakePlugin +'''; + case 'linux': + return ''' + linux: + pluginClass: FakePlugin +'''; + case 'macos': + return ''' + macos: + pluginClass: FakePlugin +'''; + case 'web': + return ''' + web: + pluginClass: FakePlugin + fileName: ${packageName}_web.dart +'''; + case 'windows': + return ''' + windows: + pluginClass: FakePlugin +'''; + default: + assert(false); + return ''; + } +} + typedef _ErrorHandler = void Function(Error error); /// Run the command [runner] with the given [args] and return diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index ec76ceba8e7..fd33c21b2d0 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -336,8 +336,7 @@ void main() { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, includeChangeLog: true, includeVersion: true); - createFakePubspec(pluginDirectory, - isFlutter: true, includeVersion: true, version: '1.0.1'); + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); const String changelog = ''' @@ -363,8 +362,7 @@ void main() { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, includeChangeLog: true, includeVersion: true); - createFakePubspec(pluginDirectory, - isFlutter: true, includeVersion: true, version: '1.0.1'); + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); const String changelog = ''' ## 1.0.2 @@ -398,8 +396,7 @@ The first version listed in CHANGELOG.md is 1.0.2. final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, includeChangeLog: true, includeVersion: true); - createFakePubspec(pluginDirectory, - isFlutter: true, includeVersion: true, version: '1.0.1'); + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); const String changelog = ''' ## 1.0.1 @@ -424,8 +421,7 @@ The first version listed in CHANGELOG.md is 1.0.2. final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, includeChangeLog: true, includeVersion: true); - createFakePubspec(pluginDirectory, - isFlutter: true, includeVersion: true, version: '1.0.0'); + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.0'); const String changelog = ''' ## 1.0.1 @@ -466,8 +462,7 @@ The first version listed in CHANGELOG.md is 1.0.1. final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, includeChangeLog: true, includeVersion: true); - createFakePubspec(pluginDirectory, - isFlutter: true, includeVersion: true, version: '1.0.0'); + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.0'); const String changelog = ''' ## NEXT @@ -495,8 +490,7 @@ The first version listed in CHANGELOG.md is 1.0.1. final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, includeChangeLog: true, includeVersion: true); - createFakePubspec(pluginDirectory, - isFlutter: true, includeVersion: true, version: '1.0.1'); + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); const String changelog = ''' ## 1.0.1 @@ -539,8 +533,7 @@ into the new version's release notes. final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, includeChangeLog: true, includeVersion: true); - createFakePubspec(pluginDirectory, - isFlutter: true, includeVersion: true, version: '1.0.1'); + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); const String changelog = ''' ## NEXT diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index ffe9bf4267a..8ed8144562c 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -7,6 +7,7 @@ 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/xctest_command.dart'; import 'package:test/test.dart'; @@ -102,137 +103,267 @@ void main() { runner.addCommand(command); }); - test('skip if ios is not supported', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: false); - - createFakePubspec(pluginDirectory.childDirectory('example'), - isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - final List output = await runCapturingPrint( - runner, ['xctest', _kDestination, 'foo_destination']); - expect(output, contains('iOS is not supported by this plugin.')); - expect(processRunner.recordedCalls, orderedEquals([])); + test('Fails if no platforms are provided', () async { + expect( + () => runner.run(['xctest']), + throwsA(isA()), + ); }); - test('running with correct destination, exclude 1 plugin', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin1', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - final Directory pluginDirectory2 = - createFakePlugin('plugin2', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory1 = - pluginDirectory1.childDirectory('example'); - createFakePubspec(pluginExampleDirectory1, isFlutter: true); - final Directory pluginExampleDirectory2 = - pluginDirectory2.childDirectory('example'); - createFakePubspec(pluginExampleDirectory2, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; - final List output = await runCapturingPrint(runner, [ - 'xctest', - _kDestination, - 'foo_destination', - '--exclude', - 'plugin1' - ]); - - expect(output, isNot(contains('Successfully ran xctest for plugin1'))); - expect(output, contains('Successfully ran xctest for plugin2')); + group('iOS', () { + test('skip if iOS is not supported', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: false, + isMacOsPlugin: true); + + createFakePubspec(pluginDirectory.childDirectory('example'), + isFlutter: true); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', - '-scheme', - 'Runner', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, + ['xctest', '--ios', _kDestination, 'foo_destination']); + expect( + output, contains('iOS is not implemented by this plugin package.')); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if iOS is implemented in a federated package', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + createFakePubspec(pluginDirectory, + iosSupport: PlatformSupport.federated); + + createFakePubspec(pluginDirectory.childDirectory('example'), + isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, + ['xctest', '--ios', _kDestination, 'foo_destination']); + expect( + output, contains('iOS is not implemented by this plugin package.')); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('running with correct destination, exclude 1 plugin', () async { + final Directory pluginDirectory1 = + createFakePlugin('plugin1', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + final Directory pluginDirectory2 = + createFakePlugin('plugin2', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory1 = + pluginDirectory1.childDirectory('example'); + createFakePubspec(pluginExampleDirectory1, isFlutter: true); + final Directory pluginExampleDirectory2 = + pluginDirectory2.childDirectory('example'); + createFakePubspec(pluginExampleDirectory2, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--ios', + _kDestination, + 'foo_destination', + '--exclude', + 'plugin1' + ]); + + expect(output, isNot(contains('Start running for plugin1...'))); + expect(output, contains('Start running for plugin2...')); + expect(output, + contains('Successfully ran iOS xctest for plugin2/example')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory2.path), + ])); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], ], - pluginExampleDirectory2.path), - ])); + isIosPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final Map schemeCommandResult = { + 'project': { + 'targets': ['bar_scheme', 'foo_scheme'] + } + }; + // For simplicity of the test, we combine all the mock results into a single mock result, each internal command + // will get this result and they should still be able to parse them correctly. + processRunner.resultStdout = + jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + await runner.run(['xctest', '--ios']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + '-destination', + 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); }); - test('Not specifying --ios-destination assigns an available simulator', - () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); - - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - final Map schemeCommandResult = { - 'project': { - 'targets': ['bar_scheme', 'foo_scheme'] - } - }; - // For simplicity of the test, we combine all the mock results into a single mock result, each internal command - // will get this result and they should still be able to parse them correctly. - processRunner.resultStdout = - jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); - await runner.run([ - 'xctest', - ]); + group('macOS', () { + test('skip if macOS is not supported', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true, + isMacOsPlugin: false); - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall( - 'xcrun', ['simctl', 'list', '--json'], null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', - '-scheme', - 'Runner', - '-destination', - 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + createFakePubspec(pluginDirectory.childDirectory('example'), + isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, + ['xctest', '--macos', _kDestination, 'foo_destination']); + expect(output, + contains('macOS is not implemented by this plugin package.')); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if macOS is implemented in a federated package', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], ], - pluginExampleDirectory.path), - ])); + isMacOsPlugin: true); + createFakePubspec(pluginDirectory, + macosSupport: PlatformSupport.federated); + + createFakePubspec(pluginDirectory.childDirectory('example'), + isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, + ['xctest', '--macos', _kDestination, 'foo_destination']); + expect(output, + contains('macOS is not implemented by this plugin package.')); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for macOS plugin', () async { + final Directory pluginDirectory1 = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + ]); + + expect(output, + contains('Successfully ran macOS xctest for plugin/example')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); }); }); } From 038c1796b08485a569daf28d4536365080e997c3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 11 Jun 2021 16:10:41 -0700 Subject: [PATCH 067/249] [flutter_plugin_tools] Migrate publish and version checks to NNBD (#4037) Migrates publish-check and version-check commands to NNBD. Reworks the version-check flow so that it's more consistent with the other commands: instead of immediately exiting on failure, it checks all plugins, gathers failures, and summarizes all failures at the end. This ensures that we don't have failures in one package temporarily masked by failures in another, so PRs don't need to go through as many check cycles. Part of https://github.com/flutter/flutter/issues/81912 --- script/tool/lib/src/common.dart | 33 ++--- .../tool/lib/src/publish_check_command.dart | 29 ++-- .../tool/lib/src/publish_plugin_command.dart | 3 +- .../tool/lib/src/version_check_command.dart | 126 +++++++++++------- script/tool/test/common_test.dart | 16 +-- .../tool/test/publish_check_command_test.dart | 14 +- script/tool/test/version_check_test.dart | 31 +++-- 7 files changed, 134 insertions(+), 118 deletions(-) diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart index 20e8479e6d4..5d653ad0ed2 100644 --- a/script/tool/lib/src/common.dart +++ b/script/tool/lib/src/common.dart @@ -165,11 +165,10 @@ bool isLinuxPlugin(FileSystemEntity entity) { return pluginSupportsPlatform(kPlatformFlagLinux, entity); } -/// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red. -void printErrorAndExit({required String errorMessage, int exitCode = 1}) { +/// Prints `errorMessage` in red. +void printError(String errorMessage) { final Colorize redError = Colorize(errorMessage)..red(); print(redError); - throw ToolExit(exitCode); } /// Error thrown when a command needs to exit with a non-zero exit code. @@ -456,9 +455,10 @@ abstract class PluginCommand extends Command { GitDir? baseGitDir = gitDir; if (baseGitDir == null) { if (!await GitDir.isGitDir(rootDir)) { - printErrorAndExit( - errorMessage: '$rootDir is not a valid Git repository.', - exitCode: 2); + printError( + '$rootDir is not a valid Git repository.', + ); + throw ToolExit(2); } baseGitDir = await GitDir.fromExisting(rootDir); } @@ -599,7 +599,7 @@ class ProcessRunner { /// passing [workingDir]. /// /// Returns the started [io.Process]. - Future start(String executable, List args, + Future start(String executable, List args, {Directory? workingDirectory}) async { final io.Process process = await io.Process.start(executable, args, workingDirectory: workingDirectory?.path); @@ -641,12 +641,12 @@ class PubVersionFinder { if (response.statusCode == 404) { return PubVersionFinderResponse( - versions: null, + versions: [], result: PubVersionFinderResult.noPackageFound, httpResponse: response); } else if (response.statusCode != 200) { return PubVersionFinderResponse( - versions: null, + versions: [], result: PubVersionFinderResult.fail, httpResponse: response); } @@ -666,9 +666,12 @@ class PubVersionFinder { /// Represents a response for [PubVersionFinder]. class PubVersionFinderResponse { /// Constructor. - PubVersionFinderResponse({this.versions, this.result, this.httpResponse}) { - if (versions != null && versions!.isNotEmpty) { - versions!.sort((Version a, Version b) { + 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); @@ -680,13 +683,13 @@ class PubVersionFinderResponse { /// /// 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; + final List versions; /// The result of the version finder. - final PubVersionFinderResult? result; + final PubVersionFinderResult result; /// The response object of the http request. - final http.Response? httpResponse; + final http.Response httpResponse; } /// An enum representing the result of [PubVersionFinder]. diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index fa229cabefc..b77eceecbf4 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_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. -// @dart=2.9 - import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -11,7 +9,6 @@ import 'dart:io' as io; import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; @@ -23,7 +20,7 @@ class PublishCheckCommand extends PluginCommand { PublishCheckCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - this.httpClient, + http.Client? httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), super(packagesDir, processRunner: processRunner) { @@ -65,9 +62,6 @@ class PublishCheckCommand extends PluginCommand { final String description = 'Checks to make sure that a plugin *could* be published.'; - /// The custom http client used to query versions on pub. - final http.Client httpClient; - final PubVersionFinder _pubVersionFinder; // The output JSON when the _machineFlag is on. @@ -135,7 +129,7 @@ class PublishCheckCommand extends PluginCommand { } } - Pubspec _tryParsePubspec(Directory package) { + Pubspec? _tryParsePubspec(Directory package) { final File pubspecFile = package.childFile('pubspec.yaml'); try { @@ -205,7 +199,7 @@ class PublishCheckCommand extends PluginCommand { final String packageName = package.basename; print('Checking that $packageName can be published.'); - final Pubspec pubspec = _tryParsePubspec(package); + final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { print('no pubspec'); return _PublishCheckResult._error; @@ -214,7 +208,7 @@ class PublishCheckCommand extends PluginCommand { return _PublishCheckResult._published; } - final Version version = pubspec.version; + final Version? version = pubspec.version; final _PublishCheckResult alreadyPublishedResult = await _checkIfAlreadyPublished( packageName: packageName, version: version); @@ -238,29 +232,24 @@ class PublishCheckCommand extends PluginCommand { // Check if `packageName` already has `version` published on pub. Future<_PublishCheckResult> _checkIfAlreadyPublished( - {String packageName, Version version}) async { + {required String packageName, required Version? version}) async { final PubVersionFinderResponse pubVersionFinderResponse = await _pubVersionFinder.getPackageVersion(package: packageName); - _PublishCheckResult result; switch (pubVersionFinderResponse.result) { case PubVersionFinderResult.success: - result = pubVersionFinderResponse.versions.contains(version) + return pubVersionFinderResponse.versions.contains(version) ? _PublishCheckResult._published : _PublishCheckResult._notPublished; - break; case PubVersionFinderResult.fail: print(''' Error fetching version on pub for $packageName. HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} HTTP response: ${pubVersionFinderResponse.httpResponse.body} '''); - result = _PublishCheckResult._error; - break; + return _PublishCheckResult._error; case PubVersionFinderResult.noPackageFound: - result = _PublishCheckResult._notPublished; - break; + return _PublishCheckResult._notPublished; } - return result; } void _setStatus(String status) { @@ -272,7 +261,7 @@ HTTP response: ${pubVersionFinderResponse.httpResponse.body} return const JsonEncoder.withIndent(' ').convert(_machineOutput); } - void _printImportantStatusMessage(String message, {@required bool isError}) { + void _printImportantStatusMessage(String message, {required bool isError}) { final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; if (getBoolArg(_machineFlag)) { print(statusMessage); diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 6b837cb3f4f..6a215064cf5 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -507,10 +507,11 @@ Safe to ignore if the package is deleted in this commit. } final String credential = io.Platform.environment[_pubCredentialName]; if (credential == null) { - printErrorAndExit(errorMessage: ''' + printError(''' No pub credential available. Please check if `~/.pub-cache/credentials.json` is valid. If running this command on CI, you can set the pub credential content in the $_pubCredentialName environment variable. '''); + throw ToolExit(1); } credentialFile.openSync(mode: FileMode.writeOnlyAppend) ..writeStringSync(credential) diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index d74c7dad3c5..6baa38e465a 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. -// @dart=2.9 - import 'dart:async'; import 'package:file/file.dart'; @@ -37,7 +35,7 @@ enum NextVersionType { /// those have different semver rules. @visibleForTesting Map getAllowedNextVersions( - Version masterVersion, Version headVersion) { + {required Version masterVersion, required Version headVersion}) { final Map allowedNextVersions = { masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, @@ -75,8 +73,8 @@ class VersionCheckCommand extends PluginCommand { VersionCheckCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - GitDir gitDir, - this.httpClient, + GitDir? gitDir, + http.Client? httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), super(packagesDir, processRunner: processRunner, gitDir: gitDir) { @@ -100,9 +98,6 @@ class VersionCheckCommand extends PluginCommand { 'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n' 'This command requires "pub" and "flutter" to be in your path.'; - /// The http client used to query pub server. - final http.Client httpClient; - final PubVersionFinder _pubVersionFinder; @override @@ -112,6 +107,8 @@ class VersionCheckCommand extends PluginCommand { final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); + final List badVersionChangePubspecs = []; + const String indentation = ' '; for (final String pubspecPath in changedPubspecs) { print('Checking versions for $pubspecPath...'); @@ -126,15 +123,16 @@ class VersionCheckCommand extends PluginCommand { continue; } - final Version headVersion = + final Version? headVersion = await gitVersionFinder.getPackageVersion(pubspecPath, gitRef: 'HEAD'); if (headVersion == null) { - printErrorAndExit( - errorMessage: '${indentation}No version found. A package that ' - 'intentionally has no version should be marked ' - '"publish_to: none".'); + printError('${indentation}No version found. A package that ' + 'intentionally has no version should be marked ' + '"publish_to: none".'); + badVersionChangePubspecs.add(pubspecPath); + continue; } - Version sourceVersion; + Version? sourceVersion; if (getBoolArg(_againstPubFlag)) { final String packageName = pubspecFile.parent.basename; final PubVersionFinderResponse pubVersionFinderResponse = @@ -146,12 +144,13 @@ class VersionCheckCommand extends PluginCommand { '$indentation$packageName: Current largest version on pub: $sourceVersion'); break; case PubVersionFinderResult.fail: - printErrorAndExit(errorMessage: ''' + printError(''' ${indentation}Error fetching version on pub for $packageName. ${indentation}HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} '''); - break; + badVersionChangePubspecs.add(pubspecPath); + continue; case PubVersionFinderResult.noPackageFound: sourceVersion = null; break; @@ -180,7 +179,8 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // Check for reverts when doing local validation. if (!getBoolArg(_againstPubFlag) && headVersion < sourceVersion) { final Map possibleVersionsFromNewVersion = - getAllowedNextVersions(headVersion, sourceVersion); + getAllowedNextVersions( + masterVersion: headVersion, headVersion: sourceVersion); // Since this skips validation, try to ensure that it really is likely // to be a revert rather than a typo by checking that the transition // from the lower version to the new version would have been valid. @@ -192,14 +192,16 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } final Map allowedNextVersions = - getAllowedNextVersions(sourceVersion, headVersion); + getAllowedNextVersions( + masterVersion: sourceVersion, headVersion: headVersion); if (!allowedNextVersions.containsKey(headVersion)) { final String source = (getBoolArg(_againstPubFlag)) ? 'pub' : 'master'; - final String error = '${indentation}Incorrectly updated version.\n' + printError('${indentation}Incorrectly updated version.\n' '${indentation}HEAD: $headVersion, $source: $sourceVersion.\n' - '${indentation}Allowed versions: $allowedNextVersions'; - printErrorAndExit(errorMessage: error); + '${indentation}Allowed versions: $allowedNextVersions'); + badVersionChangePubspecs.add(pubspecPath); + continue; } else { print('$indentation$headVersion -> $sourceVersion'); } @@ -208,38 +210,65 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} pubspec.name.endsWith('_platform_interface'); if (isPlatformInterface && allowedNextVersions[headVersion] == NextVersionType.BREAKING_MAJOR) { - final String error = '$pubspecPath breaking change detected.\n' - 'Breaking changes to platform interfaces are strongly discouraged.\n'; - printErrorAndExit(errorMessage: error); + printError('$pubspecPath breaking change detected.\n' + 'Breaking changes to platform interfaces are strongly discouraged.\n'); + badVersionChangePubspecs.add(pubspecPath); + continue; } } + _pubVersionFinder.httpClient.close(); + // TODO(stuartmorgan): Unify the way iteration works for these checks; the + // two checks shouldn't be operating independently on different lists. + final List mismatchedVersionPlugins = []; await for (final Directory plugin in getPlugins()) { - await _checkVersionsMatch(plugin); + if (!(await _checkVersionsMatch(plugin))) { + mismatchedVersionPlugins.add(plugin.basename); + } + } + + bool passed = true; + if (badVersionChangePubspecs.isNotEmpty) { + passed = false; + printError(''' +The following pubspecs failed validaton: +$indentation${badVersionChangePubspecs.join('\n$indentation')} +'''); + } + if (mismatchedVersionPlugins.isNotEmpty) { + passed = false; + printError(''' +The following pubspecs have different versions in pubspec.yaml and CHANGELOG.md: +$indentation${mismatchedVersionPlugins.join('\n$indentation')} +'''); + } + if (!passed) { + throw ToolExit(1); } - _pubVersionFinder.httpClient.close(); print('No version check errors found!'); } - Future _checkVersionsMatch(Directory plugin) async { + /// Returns whether or not the pubspec version and CHANGELOG version for + /// [plugin] match. + Future _checkVersionsMatch(Directory plugin) async { // get version from pubspec final String packageName = plugin.basename; print('-----------------------------------------'); print( 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for $packageName.'); - final Pubspec pubspec = _tryParsePubspec(plugin); + final Pubspec? pubspec = _tryParsePubspec(plugin); if (pubspec == null) { - const String error = 'Cannot parse version from pubspec.yaml'; - printErrorAndExit(errorMessage: error); + printError('Cannot parse version from pubspec.yaml'); + return false; } - final Version fromPubspec = pubspec.version; + final Version? fromPubspec = pubspec.version; // get first version from CHANGELOG final File changelog = plugin.childFile('CHANGELOG.md'); final List lines = changelog.readAsLinesSync(); - String firstLineWithText; + String? firstLineWithText; final Iterator iterator = lines.iterator; while (iterator.moveNext()) { if (iterator.current.trim().isNotEmpty) { @@ -248,7 +277,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } } // Remove all leading mark down syntax from the version line. - String versionString = firstLineWithText.split(' ').last; + String? versionString = firstLineWithText?.split(' ').last; // Skip validation for the special NEXT version that's used to accumulate // changes that don't warrant publishing on their own. @@ -266,51 +295,48 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } } - final Version fromChangeLog = Version.parse(versionString); + final Version? fromChangeLog = + versionString == null ? null : Version.parse(versionString); if (fromChangeLog == null) { - final String error = - 'Cannot find version on the first line of ${plugin.path}/CHANGELOG.md'; - printErrorAndExit(errorMessage: error); + printError( + 'Cannot find version on the first line of ${plugin.path}/CHANGELOG.md'); + return false; } if (fromPubspec != fromChangeLog) { - final String error = ''' + printError(''' versions for $packageName in CHANGELOG.md and pubspec.yaml do not match. The version in pubspec.yaml is $fromPubspec. The first version listed in CHANGELOG.md is $fromChangeLog. -'''; - printErrorAndExit(errorMessage: error); +'''); + return false; } // If NEXT wasn't the first section, it should not exist at all. if (!hasNextSection) { final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); if (lines.any((String line) => nextRegex.hasMatch(line))) { - printErrorAndExit(errorMessage: ''' + printError(''' When bumping the version for release, the NEXT section should be incorporated into the new version's release notes. '''); + return false; } } print('$packageName passed version check'); + return true; } - Pubspec _tryParsePubspec(Directory package) { + Pubspec? _tryParsePubspec(Directory package) { final File pubspecFile = package.childFile('pubspec.yaml'); try { final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - if (pubspec == null) { - final String error = - 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}'; - printErrorAndExit(errorMessage: error); - } return pubspec; } on Exception catch (exception) { - final String error = - 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}'; - printErrorAndExit(errorMessage: error); + printError( + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}'); } return null; } diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart index 0516fab1da3..a51182d91ff 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common_test.dart @@ -457,10 +457,10 @@ file2/file2.cc final PubVersionFinderResponse response = await finder.getPackageVersion(package: 'some_package'); - expect(response.versions, isNull); + expect(response.versions, isEmpty); expect(response.result, PubVersionFinderResult.noPackageFound); - expect(response.httpResponse!.statusCode, 404); - expect(response.httpResponse!.body, ''); + expect(response.httpResponse.statusCode, 404); + expect(response.httpResponse.body, ''); }); test('HTTP error when getting versions from pub', () async { @@ -471,10 +471,10 @@ file2/file2.cc final PubVersionFinderResponse response = await finder.getPackageVersion(package: 'some_package'); - expect(response.versions, isNull); + expect(response.versions, isEmpty); expect(response.result, PubVersionFinderResult.fail); - expect(response.httpResponse!.statusCode, 400); - expect(response.httpResponse!.body, ''); + expect(response.httpResponse.statusCode, 400); + expect(response.httpResponse.body, ''); }); test('Get a correct list of versions when http response is OK.', () async { @@ -517,8 +517,8 @@ file2/file2.cc Version.parse('0.0.1'), ]); expect(response.result, PubVersionFinderResult.success); - expect(response.httpResponse!.statusCode, 200); - expect(response.httpResponse!.body, json.encode(httpResponse)); + expect(response.httpResponse.statusCode, 200); + expect(response.httpResponse.body, json.encode(httpResponse)); }); }); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 11d703177d5..e5722567f20 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.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. -// @dart=2.9 - import 'dart:collection'; import 'dart:convert'; import 'dart:io' as io; @@ -23,9 +21,9 @@ import 'util.dart'; void main() { group('$PublishCheckProcessRunner tests', () { FileSystem fileSystem; - Directory packagesDir; - PublishCheckProcessRunner processRunner; - CommandRunner runner; + late Directory packagesDir; + late PublishCheckProcessRunner processRunner; + late CommandRunner runner; setUp(() { fileSystem = MemoryFileSystem(); @@ -177,7 +175,7 @@ void main() { } else if (request.url.pathSegments.last == 'no_publish_b.json') { return http.Response(json.encode(httpResponseB), 200); } - return null; + return http.Response('', 500); }); final PublishCheckCommand command = PublishCheckCommand(packagesDir, processRunner: processRunner, httpClient: mockClient); @@ -241,7 +239,7 @@ void main() { } else if (request.url.pathSegments.last == 'no_publish_b.json') { return http.Response(json.encode(httpResponseB), 200); } - return null; + return http.Response('', 500); }); final PublishCheckCommand command = PublishCheckCommand(packagesDir, processRunner: processRunner, httpClient: mockClient); @@ -308,7 +306,7 @@ void main() { } else if (request.url.pathSegments.last == 'no_publish_b.json') { return http.Response(json.encode(httpResponseB), 200); } - return null; + return http.Response('', 500); }); final PublishCheckCommand command = PublishCheckCommand(packagesDir, processRunner: processRunner, httpClient: mockClient); diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index fd33c21b2d0..1199c270642 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.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. -// @dart=2.9 - import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -13,24 +11,25 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common.dart'; import 'package:flutter_plugin_tools/src/version_check_command.dart'; -import 'package:git/git.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'; + +import 'common_test.mocks.dart'; import 'util.dart'; void testAllowedVersion( String masterVersion, String headVersion, { bool allowed = true, - NextVersionType nextVersionType, + NextVersionType? nextVersionType, }) { final Version master = Version.parse(masterVersion); final Version head = Version.parse(headVersion); final Map allowedVersions = - getAllowedNextVersions(master, head); + getAllowedNextVersions(masterVersion: master, headVersion: head); if (allowed) { expect(allowedVersions, contains(head)); if (nextVersionType != null) { @@ -41,8 +40,6 @@ void testAllowedVersion( } } -class MockGitDir extends Mock implements GitDir {} - class MockProcessResult extends Mock implements io.ProcessResult {} const String _redColorMessagePrefix = '\x1B[31m'; @@ -57,13 +54,13 @@ void main() { const String indentation = ' '; group('$VersionCheckCommand', () { FileSystem fileSystem; - Directory packagesDir; - CommandRunner runner; - RecordingProcessRunner processRunner; - List> gitDirCommands; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late List> gitDirCommands; String gitDiffResponse; Map gitShowResponses; - MockGitDir gitDir; + late MockGitDir gitDir; setUp(() { fileSystem = MemoryFileSystem(); @@ -77,17 +74,19 @@ void main() { gitDirCommands.add(invocation.positionalArguments[0] as List); final MockProcessResult mockProcessResult = MockProcessResult(); if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout as String) + when(mockProcessResult.stdout as String?) .thenReturn(gitDiffResponse); } else if (invocation.positionalArguments[0][0] == 'show') { - final String response = + final String? response = gitShowResponses[invocation.positionalArguments[0][1]]; if (response == null) { throw const io.ProcessException('git', ['show']); } - when(mockProcessResult.stdout as String).thenReturn(response); + when(mockProcessResult.stdout as String?) + .thenReturn(response); } else if (invocation.positionalArguments[0][0] == 'merge-base') { - when(mockProcessResult.stdout as String).thenReturn('abc123'); + when(mockProcessResult.stdout as String?) + .thenReturn('abc123'); } return Future.value(mockProcessResult); }); From ce948ce3cb395056b53f901110fea76b0639d93a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sat, 12 Jun 2021 12:44:04 -0700 Subject: [PATCH 068/249] [flutter_plugin_tools] Complete migration to NNBD (#4048) --- script/tool/CHANGELOG.md | 1 + script/tool/bin/flutter_plugin_tools.dart | 2 - script/tool/lib/src/main.dart | 2 - .../tool/lib/src/publish_plugin_command.dart | 154 ++++++++++-------- .../test/publish_plugin_command_test.dart | 56 +++---- 5 files changed, 110 insertions(+), 105 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 6b3d96b4f35..ae81ced6366 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,7 @@ compatibility. - `xctest` now supports running macOS tests in addition to iOS - **Breaking change**: it now requires an `--ios` and/or `--macos` flag. +- The tooling now runs in strong null-safe mode. ## 0.2.0 diff --git a/script/tool/bin/flutter_plugin_tools.dart b/script/tool/bin/flutter_plugin_tools.dart index eea0db5a7f0..0f30bee0d25 100644 --- a/script/tool/bin/flutter_plugin_tools.dart +++ b/script/tool/bin/flutter_plugin_tools.dart @@ -2,6 +2,4 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart=2.9 - export 'package:flutter_plugin_tools/src/main.dart'; diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 9f1a1d5bf24..a7603122186 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.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. -// @dart=2.9 - import 'dart:io' as io; import 'package:args/command_runner.dart'; diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 6a215064cf5..1e7c1502984 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_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. -// @dart=2.9 - import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -18,6 +16,17 @@ import 'package:yaml/yaml.dart'; import 'common.dart'; +@immutable +class _RemoteInfo { + const _RemoteInfo({required this.name, required this.url}); + + /// The git name for the remote. + final String name; + + /// The remote's URL. + final String url; +} + /// Wraps pub publish with a few niceties used by the flutter/plugin team. /// /// 1. Checks for any modified files in git and refuses to publish if there's an @@ -35,8 +44,8 @@ class PublishPluginCommand extends PluginCommand { Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Print print = print, - io.Stdin stdinput, - GitDir gitDir, + io.Stdin? stdinput, + GitDir? gitDir, }) : _print = print, _stdin = stdinput ?? io.stdin, super(packagesDir, processRunner: processRunner, gitDir: gitDir) { @@ -118,7 +127,7 @@ class PublishPluginCommand extends PluginCommand { final Print _print; final io.Stdin _stdin; - StreamSubscription _stdinSubscription; + StreamSubscription? _stdinSubscription; @override Future run() async { @@ -138,14 +147,20 @@ class PublishPluginCommand extends PluginCommand { _print('$packagesPath is not a valid Git repository.'); throw ToolExit(1); } - final GitDir baseGitDir = + final GitDir baseGitDir = gitDir ?? await GitDir.fromExisting(packagesPath, allowSubdirectory: true); final bool shouldPushTag = getBoolArg(_pushTagsOption); - final String remote = getStringArg(_remoteOption); - String remoteUrl; + _RemoteInfo? remote; if (shouldPushTag) { - remoteUrl = await _verifyRemote(remote); + final String remoteName = getStringArg(_remoteOption); + final String? remoteUrl = await _verifyRemote(remoteName); + if (remoteUrl == null) { + printError( + 'Unable to find URL for remote $remoteName; cannot push tags'); + throw ToolExit(1); + } + remote = _RemoteInfo(name: remoteName, url: remoteUrl); } _print('Local repo is ready!'); if (getBoolArg(_dryRunFlag)) { @@ -155,27 +170,21 @@ class PublishPluginCommand extends PluginCommand { bool successful; if (publishAllChanged) { successful = await _publishAllChangedPackages( - remote: remote, - remoteUrl: remoteUrl, - shouldPushTag: shouldPushTag, baseGitDir: baseGitDir, + remoteForTagPush: remote, ); } else { successful = await _publishAndTagPackage( packageDir: _getPackageDir(package), - remote: remote, - remoteUrl: remoteUrl, - shouldPushTag: shouldPushTag, + remoteForTagPush: remote, ); } await _finish(successful); } Future _publishAllChangedPackages({ - String remote, - String remoteUrl, - bool shouldPushTag, - GitDir baseGitDir, + required GitDir baseGitDir, + _RemoteInfo? remoteForTagPush, }) async { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final List changedPubspecs = @@ -215,9 +224,7 @@ class PublishPluginCommand extends PluginCommand { _print('\n'); if (await _publishAndTagPackage( packageDir: pubspecFile.parent, - remote: remote, - remoteUrl: remoteUrl, - shouldPushTag: shouldPushTag, + remoteForTagPush: remoteForTagPush, )) { packagesReleased.add(pubspecFile.parent.basename); } else { @@ -237,13 +244,11 @@ class PublishPluginCommand extends PluginCommand { // Publish the package to pub with `pub publish`. // If `_tagReleaseOption` is on, git tag the release. - // If `shouldPushTag` is `true`, the tag will be pushed to `remote`. - // Returns `true` if publishing and tag are successful. + // If `remoteForTagPush` is non-null, the tag will be pushed to that remote. + // Returns `true` if publishing and tagging are successful. Future _publishAndTagPackage({ - @required Directory packageDir, - @required String remote, - @required String remoteUrl, - @required bool shouldPushTag, + required Directory packageDir, + _RemoteInfo? remoteForTagPush, }) async { if (!await _publishPlugin(packageDir: packageDir)) { return false; @@ -251,9 +256,7 @@ class PublishPluginCommand extends PluginCommand { if (getBoolArg(_tagReleaseOption)) { if (!await _tagRelease( packageDir: packageDir, - remote: remote, - remoteUrl: remoteUrl, - shouldPushTag: shouldPushTag, + remoteForPush: remoteForTagPush, )) { return false; } @@ -264,9 +267,9 @@ class PublishPluginCommand extends PluginCommand { // Returns a [_CheckNeedsReleaseResult] that indicates the result. Future<_CheckNeedsReleaseResult> _checkNeedsRelease({ - @required File pubspecFile, - @required GitVersionFinder gitVersionFinder, - @required List existingTags, + required File pubspecFile, + required GitVersionFinder gitVersionFinder, + required List existingTags, }) async { if (!pubspecFile.existsSync()) { _print(''' @@ -287,10 +290,6 @@ Safe to ignore if the package is deleted in this commit. return _CheckNeedsReleaseResult.failure; } - if (pubspec.name == null) { - _print('Fatal: Package name is null.'); - return _CheckNeedsReleaseResult.failure; - } // Get latest tagged version and compare with the current version. // TODO(cyanglaz): Check latest version of the package on pub instead of git // https://github.com/flutter/flutter/issues/81047 @@ -301,7 +300,7 @@ Safe to ignore if the package is deleted in this commit. if (latestTag.isNotEmpty) { final String latestTaggedVersion = latestTag.split('-v').last; final Version latestVersion = Version.parse(latestTaggedVersion); - if (pubspec.version < latestVersion) { + if (pubspec.version! < latestVersion) { _print( 'The new version (${pubspec.version}) is lower than the current version ($latestVersion) for ${pubspec.name}.\nThis git commit is a revert, no release is tagged.'); return _CheckNeedsReleaseResult.noRelease; @@ -313,7 +312,7 @@ Safe to ignore if the package is deleted in this commit. // Publish the plugin. // // Returns `true` if successful, `false` otherwise. - Future _publishPlugin({@required Directory packageDir}) async { + Future _publishPlugin({required Directory packageDir}) async { final bool gitStatusOK = await _checkGitStatus(packageDir); if (!gitStatusOK) { return false; @@ -326,14 +325,13 @@ Safe to ignore if the package is deleted in this commit. return true; } - // Tag the release with -v + // Tag the release with -v, and, if [remoteForTagPush] + // is provided, push it to that remote. // // Return `true` if successful, `false` otherwise. Future _tagRelease({ - @required Directory packageDir, - @required String remote, - @required String remoteUrl, - @required bool shouldPushTag, + required Directory packageDir, + _RemoteInfo? remoteForPush, }) async { final String tag = _getTag(packageDir); _print('Tagging release $tag...'); @@ -350,23 +348,20 @@ Safe to ignore if the package is deleted in this commit. } } - if (!shouldPushTag) { + if (remoteForPush == null) { return true; } - _print('Pushing tag to $remote...'); + _print('Pushing tag to ${remoteForPush.name}...'); return await _pushTagToRemote( - remote: remote, tag: tag, - remoteUrl: remoteUrl, + remote: remoteForPush, ); } Future _finish(bool successful) async { - if (_stdinSubscription != null) { - await _stdinSubscription.cancel(); - _stdinSubscription = null; - } + await _stdinSubscription?.cancel(); + _stdinSubscription = null; if (successful) { _print('Done!'); } else { @@ -408,15 +403,15 @@ Safe to ignore if the package is deleted in this commit. return statusOutput.isEmpty; } - Future _verifyRemote(String remote) async { - final io.ProcessResult remoteInfo = await processRunner.run( + Future _verifyRemote(String remote) async { + final io.ProcessResult getRemoteUrlResult = await processRunner.run( 'git', ['remote', 'get-url', remote], workingDir: packagesDir, exitOnError: true, logOnError: true, ); - return remoteInfo.stdout as String; + return getRemoteUrlResult.stdout as String?; } Future _publish(Directory packageDir) async { @@ -471,15 +466,14 @@ Safe to ignore if the package is deleted in this commit. // // Return `true` if successful, `false` otherwise. Future _pushTagToRemote({ - @required String remote, - @required String tag, - @required String remoteUrl, + required String tag, + required _RemoteInfo remote, }) async { - assert(remote != null && tag != null && remoteUrl != null); + assert(remote != null && tag != null); if (!getBoolArg(_skipConfirmationFlag)) { - _print('Ready to push $tag to $remoteUrl (y/n)?'); - final String input = _stdin.readLineSync(); - if (input.toLowerCase() != 'y') { + _print('Ready to push $tag to ${remote.url} (y/n)?'); + final String? input = _stdin.readLineSync(); + if (input?.toLowerCase() != 'y') { _print('Tag push canceled.'); return false; } @@ -487,7 +481,7 @@ Safe to ignore if the package is deleted in this commit. if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await processRunner.run( 'git', - ['push', remote, tag], + ['push', remote.name, tag], workingDir: packagesDir, exitOnError: false, logOnError: true, @@ -500,15 +494,16 @@ Safe to ignore if the package is deleted in this commit. } void _ensureValidPubCredential() { - final File credentialFile = packagesDir.fileSystem.file(_credentialsPath); + final String credentialsPath = _credentialsPath; + final File credentialFile = packagesDir.fileSystem.file(credentialsPath); if (credentialFile.existsSync() && credentialFile.readAsStringSync().isNotEmpty) { return; } - final String credential = io.Platform.environment[_pubCredentialName]; + final String? credential = io.Platform.environment[_pubCredentialName]; if (credential == null) { printError(''' -No pub credential available. Please check if `~/.pub-cache/credentials.json` is valid. +No pub credential available. Please check if `$credentialsPath` is valid. If running this command on CI, you can set the pub credential content in the $_pubCredentialName environment variable. '''); throw ToolExit(1); @@ -529,17 +524,32 @@ If running this command on CI, you can set the pub credential content in the $_p final String _credentialsPath = () { // This follows the same logic as pub: // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 - String cacheDir; - final String pubCache = io.Platform.environment['PUB_CACHE']; + String? cacheDir; + final String? pubCache = io.Platform.environment['PUB_CACHE']; print(pubCache); if (pubCache != null) { cacheDir = pubCache; } else if (io.Platform.isWindows) { - final String appData = io.Platform.environment['APPDATA']; - cacheDir = p.join(appData, 'Pub', 'Cache'); + final String? appData = io.Platform.environment['APPDATA']; + if (appData == null) { + printError('"APPDATA" environment variable is not set.'); + } else { + cacheDir = p.join(appData, 'Pub', 'Cache'); + } } else { - cacheDir = p.join(io.Platform.environment['HOME'], '.pub-cache'); + final String? home = io.Platform.environment['HOME']; + if (home == null) { + printError('"HOME" environment variable is not set.'); + } else { + cacheDir = p.join(home, '.pub-cache'); + } + } + + if (cacheDir == null) { + printError('Unable to determine pub cache location'); + throw ToolExit(1); } + return p.join(cacheDir, 'credentials.json'); }(); diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 14987d47a40..1cb4245fdb7 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.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. -// @dart=2.9 - import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -22,15 +20,15 @@ import 'util.dart'; void main() { const String testPluginName = 'foo'; - List printedMessages; - - Directory testRoot; - Directory packagesDir; - Directory pluginDir; - GitDir gitDir; - TestProcessRunner processRunner; - CommandRunner commandRunner; - MockStdin mockStdin; + late List printedMessages; + + late Directory testRoot; + late Directory packagesDir; + late Directory pluginDir; + late GitDir gitDir; + late TestProcessRunner processRunner; + late CommandRunner commandRunner; + late MockStdin mockStdin; // This test uses a local file system instead of an in memory one throughout // so that git actually works. In setup we initialize a mono repo of plugins // with one package and commit everything to Git. @@ -65,7 +63,7 @@ void main() { commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object message) => printedMessages.add(message.toString()), + print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, gitDir: gitDir)); }); @@ -285,9 +283,9 @@ void main() { '--no-push-tags', ]); - final String tag = + final String? tag = (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) - .stdout as String; + .stdout as String?; expect(tag, isNotEmpty); }); @@ -303,10 +301,10 @@ void main() { throwsA(const TypeMatcher())); expect(printedMessages, contains('Publish foo failed.')); - final String tag = (await gitDir.runCommand( + final String? tag = (await gitDir.runCommand( ['show-ref', 'fake_package-v0.0.1'], throwOnError: false)) - .stdout as String; + .stdout as String?; expect(tag, isEmpty); }); }); @@ -838,20 +836,20 @@ void main() { class TestProcessRunner extends ProcessRunner { final List results = []; // Most recent returned publish process. - MockProcess mockPublishProcess; + late MockProcess mockPublishProcess; final List mockPublishArgs = []; final MockProcessResult mockPushTagsResult = MockProcessResult(); final List pushTagsArgs = []; - String mockPublishStdout; - String mockPublishStderr; - int mockPublishCompleteCode; + String? mockPublishStdout; + String? mockPublishStderr; + int? mockPublishCompleteCode; @override Future run( String executable, List args, { - Directory workingDir, + Directory? workingDir, bool exitOnError = false, bool logOnError = false, Encoding stdoutEncoding = io.systemEncoding, @@ -874,7 +872,7 @@ class TestProcessRunner extends ProcessRunner { @override Future start(String executable, List args, - {Directory workingDirectory}) async { + {Directory? workingDirectory}) async { /// Never actually publish anything. Start is always and only used for this /// since it returns something we can route stdin through. assert(executable == 'flutter' && @@ -884,10 +882,10 @@ class TestProcessRunner extends ProcessRunner { mockPublishArgs.addAll(args); mockPublishProcess = MockProcess(); if (mockPublishStdout != null) { - mockPublishProcess.stdoutController.add(utf8.encode(mockPublishStdout)); + mockPublishProcess.stdoutController.add(utf8.encode(mockPublishStdout!)); } if (mockPublishStderr != null) { - mockPublishProcess.stderrController.add(utf8.encode(mockPublishStderr)); + mockPublishProcess.stderrController.add(utf8.encode(mockPublishStderr!)); } if (mockPublishCompleteCode != null) { mockPublishProcess.exitCodeCompleter.complete(mockPublishCompleteCode); @@ -899,8 +897,8 @@ class TestProcessRunner extends ProcessRunner { class MockStdin extends Mock implements io.Stdin { List> mockUserInputs = >[]; - StreamController> _controller; - String readLineOutput; + late StreamController> _controller; + String? readLineOutput; @override Stream transform(StreamTransformer, S> streamTransformer) { @@ -915,14 +913,14 @@ class MockStdin extends Mock implements io.Stdin { } @override - StreamSubscription> listen(void onData(List event), - {Function onError, void onDone(), bool cancelOnError}) { + StreamSubscription> listen(void onData(List event)?, + {Function? onError, void onDone()?, bool? cancelOnError}) { return _controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override - String readLineSync( + String? readLineSync( {Encoding encoding = io.systemEncoding, bool retainNewlines = false}) => readLineOutput; From 10486b0cebee3daef8d58c30c18ea1a427b7f47d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 16 Jun 2021 12:37:30 -0700 Subject: [PATCH 069/249] [flutter_plugin_tools] Split common.dart (#4057) common.dart is a large-and-growing file containing all shared code, which makes it hard to navigate. To make maintenance easier, this splits the file (and its test file) into separate files for each major component or category. --- script/tool/lib/src/analyze_command.dart | 4 +- .../tool/lib/src/build_examples_command.dart | 5 +- script/tool/lib/src/common.dart | 781 ------------------ script/tool/lib/src/common/core.dart | 69 ++ .../lib/src/common/git_version_finder.dart | 81 ++ .../tool/lib/src/common/plugin_command.dart | 353 ++++++++ script/tool/lib/src/common/plugin_utils.dart | 110 +++ .../tool/lib/src/common/process_runner.dart | 104 +++ .../lib/src/common/pub_version_finder.dart | 103 +++ .../src/create_all_plugins_app_command.dart | 3 +- .../tool/lib/src/drive_examples_command.dart | 7 +- .../lib/src/firebase_test_lab_command.dart | 4 +- script/tool/lib/src/format_command.dart | 5 +- script/tool/lib/src/java_test_command.dart | 6 +- .../tool/lib/src/license_check_command.dart | 5 +- .../tool/lib/src/lint_podspecs_command.dart | 4 +- script/tool/lib/src/list_command.dart | 4 +- script/tool/lib/src/main.dart | 2 +- .../tool/lib/src/publish_check_command.dart | 5 +- .../tool/lib/src/publish_plugin_command.dart | 5 +- .../tool/lib/src/pubspec_check_command.dart | 6 +- script/tool/lib/src/test_command.dart | 7 +- .../tool/lib/src/version_check_command.dart | 8 +- script/tool/lib/src/xctest_command.dart | 6 +- script/tool/test/analyze_command_test.dart | 2 +- .../test/common/git_version_finder_test.dart | 93 +++ .../plugin_command_test.dart} | 361 +------- .../plugin_command_test.mocks.dart} | 0 .../tool/test/common/plugin_utils_test.dart | 210 +++++ .../test/common/pub_version_finder_test.dart | 89 ++ .../test/drive_examples_command_test.dart | 2 +- script/tool/test/firebase_test_lab_test.dart | 2 +- .../tool/test/license_check_command_test.dart | 2 +- .../tool/test/publish_check_command_test.dart | 2 +- .../test/publish_plugin_command_test.dart | 3 +- .../tool/test/pubspec_check_command_test.dart | 2 +- script/tool/test/util.dart | 3 +- script/tool/test/version_check_test.dart | 4 +- script/tool/test/xctest_command_test.dart | 3 +- 39 files changed, 1284 insertions(+), 1181 deletions(-) delete mode 100644 script/tool/lib/src/common.dart create mode 100644 script/tool/lib/src/common/core.dart create mode 100644 script/tool/lib/src/common/git_version_finder.dart create mode 100644 script/tool/lib/src/common/plugin_command.dart create mode 100644 script/tool/lib/src/common/plugin_utils.dart create mode 100644 script/tool/lib/src/common/process_runner.dart create mode 100644 script/tool/lib/src/common/pub_version_finder.dart create mode 100644 script/tool/test/common/git_version_finder_test.dart rename script/tool/test/{common_test.dart => common/plugin_command_test.dart} (51%) rename script/tool/test/{common_test.mocks.dart => common/plugin_command_test.mocks.dart} (100%) create mode 100644 script/tool/test/common/plugin_utils_test.dart create mode 100644 script/tool/test/common/pub_version_finder_test.dart diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 076c8f69885..003f0bcda82 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 9590aecef98..61d291d87c6 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 5d653ad0ed2..00000000000 --- 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 00000000000..4788b9fa914 --- /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 00000000000..2c9519e7a85 --- /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 00000000000..1ab9d8dcc6e --- /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 00000000000..b6ac433db2e --- /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 00000000000..429761ead3b --- /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 00000000000..ebac473de7a --- /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 cd5b85e45ac..fab41bcf4ec 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 14dfede5b2f..b6576cd13ba 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 741d8569322..b4f5e92933c 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 1ef41f82bb2..5f060d715bf 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 d1366ea7636..d7e453b6ad7 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 805c3ab9f90..4ea8a1e0939 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 364653bd13b..5e86d2be40b 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 f6b186e7ba2..39515cf686b 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 a7603122186..f397a04aa66 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 b77eceecbf4..82a76609e98 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 1e7c1502984..70ec75bc7b7 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 878b683dbbb..480d3a4c119 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 0174b986eb6..b7bf261caa8 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 6baa38e465a..5e9f55333f8 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 288851ca7ed..77e5659df3f 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 ec627f25864..1ef4fdc44b4 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 00000000000..f1f40b5e003 --- /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 a51182d91ff..58d202e1992 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 00000000000..aaa850155da --- /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 00000000000..7d8658a907e --- /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 c9a8b9d90a8..9c5bd18cfb1 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 aa8be17d679..0bc8f1e197c 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 a874d7db17b..dfe8d25197a 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 e5722567f20..c0ccd2989cf 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 1cb4245fdb7..ef682bfe61f 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 576060d23a9..f5fe6aef849 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 c590d8a4bb0..79c46fcc50e 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 1199c270642..a8e7e20bad2 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 8ed8144562c..c0bd6b5dee5 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'; From 81a6f66eeeb93eaf4591a359a1a61cc5fafb6b27 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 17 Jun 2021 13:29:03 -0700 Subject: [PATCH 070/249] [flutter_plugin_tool] Refactor createFakePlugin (#4064) --- .../tool/lib/src/build_examples_command.dart | 32 +-- script/tool/lib/src/common/core.dart | 12 +- script/tool/lib/src/common/plugin_utils.dart | 26 +- .../tool/lib/src/drive_examples_command.dart | 32 +-- script/tool/lib/src/xctest_command.dart | 8 +- script/tool/test/analyze_command_test.dart | 11 +- .../test/build_examples_command_test.dart | 174 ++++++------ .../tool/test/common/plugin_command_test.dart | 18 +- .../tool/test/common/plugin_utils_test.dart | 155 ++++++----- .../test/drive_examples_command_test.dart | 248 +++++++----------- script/tool/test/firebase_test_lab_test.dart | 6 +- script/tool/test/java_test_command_test.dart | 18 +- .../tool/test/lint_podspecs_command_test.dart | 24 +- script/tool/test/list_command_test.dart | 21 +- .../tool/test/publish_check_command_test.dart | 33 +-- .../test/publish_plugin_command_test.dart | 108 +++----- .../tool/test/pubspec_check_command_test.dart | 30 +-- script/tool/test/test_command_test.dart | 81 +++--- script/tool/test/util.dart | 159 +++++------ script/tool/test/version_check_test.dart | 149 +++++------ script/tool/test/xctest_command_test.dart | 112 +++----- 21 files changed, 638 insertions(+), 819 deletions(-) diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 61d291d87c6..aff5ecba498 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -27,10 +27,10 @@ class BuildExamplesCommand extends PluginCommand { Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), }) : super(packagesDir, processRunner: processRunner) { - argParser.addFlag(kPlatformFlagLinux, defaultsTo: false); - argParser.addFlag(kPlatformFlagMacos, defaultsTo: false); - argParser.addFlag(kPlatformFlagWeb, defaultsTo: false); - argParser.addFlag(kPlatformFlagWindows, defaultsTo: false); + argParser.addFlag(kPlatformLinux, defaultsTo: false); + argParser.addFlag(kPlatformMacos, defaultsTo: false); + argParser.addFlag(kPlatformWeb, defaultsTo: false); + argParser.addFlag(kPlatformWindows, defaultsTo: false); argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); argParser.addFlag(kApk); argParser.addOption( @@ -53,10 +53,10 @@ class BuildExamplesCommand extends PluginCommand { final List platformSwitches = [ kApk, kIpa, - kPlatformFlagLinux, - kPlatformFlagMacos, - kPlatformFlagWeb, - kPlatformFlagWindows, + kPlatformLinux, + kPlatformMacos, + kPlatformWeb, + kPlatformWindows, ]; if (!platformSwitches.any((String platform) => getBoolArg(platform))) { print( @@ -75,14 +75,14 @@ class BuildExamplesCommand extends PluginCommand { final String packageName = p.relative(example.path, from: packagesDir.path); - if (getBoolArg(kPlatformFlagLinux)) { + if (getBoolArg(kPlatformLinux)) { print('\nBUILDING Linux for $packageName'); if (isLinuxPlugin(plugin)) { final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ 'build', - kPlatformFlagLinux, + kPlatformLinux, if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], @@ -95,14 +95,14 @@ class BuildExamplesCommand extends PluginCommand { } } - if (getBoolArg(kPlatformFlagMacos)) { + if (getBoolArg(kPlatformMacos)) { print('\nBUILDING macOS for $packageName'); if (isMacOsPlugin(plugin)) { final int exitCode = await processRunner.runAndStream( flutterCommand, [ 'build', - kPlatformFlagMacos, + kPlatformMacos, if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], @@ -115,14 +115,14 @@ class BuildExamplesCommand extends PluginCommand { } } - if (getBoolArg(kPlatformFlagWeb)) { + if (getBoolArg(kPlatformWeb)) { print('\nBUILDING web for $packageName'); if (isWebPlugin(plugin)) { final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ 'build', - kPlatformFlagWeb, + kPlatformWeb, if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], @@ -135,14 +135,14 @@ class BuildExamplesCommand extends PluginCommand { } } - if (getBoolArg(kPlatformFlagWindows)) { + if (getBoolArg(kPlatformWindows)) { print('\nBUILDING Windows for $packageName'); if (isWindowsPlugin(plugin)) { final int buildExitCode = await processRunner.runAndStream( flutterCommand, [ 'build', - kPlatformFlagWindows, + kPlatformWindows, if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 4788b9fa914..1da6ef7a4c8 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -11,22 +11,22 @@ import 'package:yaml/yaml.dart'; typedef Print = void Function(Object? object); /// Key for windows platform. -const String kPlatformFlagWindows = 'windows'; +const String kPlatformWindows = 'windows'; /// Key for macos platform. -const String kPlatformFlagMacos = 'macos'; +const String kPlatformMacos = 'macos'; /// Key for linux platform. -const String kPlatformFlagLinux = 'linux'; +const String kPlatformLinux = 'linux'; /// Key for IPA (iOS) platform. -const String kPlatformFlagIos = 'ios'; +const String kPlatformIos = 'ios'; /// Key for APK (Android) platform. -const String kPlatformFlagAndroid = 'android'; +const String kPlatformAndroid = 'android'; /// Key for Web platform. -const String kPlatformFlagWeb = 'web'; +const String kPlatformWeb = 'web'; /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index b6ac433db2e..0277b78d566 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -29,12 +29,12 @@ enum PlatformSupport { /// 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); + assert(platform == kPlatformIos || + platform == kPlatformAndroid || + platform == kPlatformWeb || + platform == kPlatformMacos || + platform == kPlatformWindows || + platform == kPlatformLinux); if (entity is! Directory) { return false; } @@ -59,7 +59,7 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity, return false; } if (!pluginSection.containsKey('platforms')) { - return platform == kPlatformFlagIos || platform == kPlatformFlagAndroid; + return platform == kPlatformIos || platform == kPlatformAndroid; } return false; } @@ -81,30 +81,30 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity, /// Returns whether the given directory contains a Flutter Android plugin. bool isAndroidPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagAndroid, entity); + return pluginSupportsPlatform(kPlatformAndroid, entity); } /// Returns whether the given directory contains a Flutter iOS plugin. bool isIosPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagIos, entity); + return pluginSupportsPlatform(kPlatformIos, entity); } /// Returns whether the given directory contains a Flutter web plugin. bool isWebPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagWeb, entity); + return pluginSupportsPlatform(kPlatformWeb, entity); } /// Returns whether the given directory contains a Flutter Windows plugin. bool isWindowsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagWindows, entity); + return pluginSupportsPlatform(kPlatformWindows, entity); } /// Returns whether the given directory contains a Flutter macOS plugin. bool isMacOsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagMacos, entity); + return pluginSupportsPlatform(kPlatformMacos, entity); } /// Returns whether the given directory contains a Flutter linux plugin. bool isLinuxPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagLinux, entity); + return pluginSupportsPlatform(kPlatformLinux, entity); } diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index b6576cd13ba..8a8cd6726d0 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -18,17 +18,17 @@ class DriveExamplesCommand extends PluginCommand { Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), }) : super(packagesDir, processRunner: processRunner) { - argParser.addFlag(kPlatformFlagAndroid, + argParser.addFlag(kPlatformAndroid, help: 'Runs the Android implementation of the examples'); - argParser.addFlag(kPlatformFlagIos, + argParser.addFlag(kPlatformIos, help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(kPlatformFlagLinux, + argParser.addFlag(kPlatformLinux, help: 'Runs the Linux implementation of the examples'); - argParser.addFlag(kPlatformFlagMacos, + argParser.addFlag(kPlatformMacos, help: 'Runs the macOS implementation of the examples'); - argParser.addFlag(kPlatformFlagWeb, + argParser.addFlag(kPlatformWeb, help: 'Runs the web implementation of the examples'); - argParser.addFlag(kPlatformFlagWindows, + argParser.addFlag(kPlatformWindows, help: 'Runs the Windows implementation of the examples'); argParser.addOption( kEnableExperiment, @@ -55,10 +55,10 @@ class DriveExamplesCommand extends PluginCommand { Future run() async { final List failingTests = []; final List pluginsWithoutTests = []; - final bool isLinux = getBoolArg(kPlatformFlagLinux); - final bool isMacos = getBoolArg(kPlatformFlagMacos); - final bool isWeb = getBoolArg(kPlatformFlagWeb); - final bool isWindows = getBoolArg(kPlatformFlagWindows); + final bool isLinux = getBoolArg(kPlatformLinux); + final bool isMacos = getBoolArg(kPlatformMacos); + final bool isWeb = getBoolArg(kPlatformWeb); + final bool isWindows = getBoolArg(kPlatformWindows); await for (final Directory plugin in getPlugins()) { final String pluginName = plugin.basename; if (pluginName.endsWith('_platform_interface') && @@ -222,12 +222,12 @@ Tried searching for the following: Future _pluginSupportedOnCurrentPlatform( FileSystemEntity plugin) async { - final bool isAndroid = getBoolArg(kPlatformFlagAndroid); - final bool isIOS = getBoolArg(kPlatformFlagIos); - final bool isLinux = getBoolArg(kPlatformFlagLinux); - final bool isMacos = getBoolArg(kPlatformFlagMacos); - final bool isWeb = getBoolArg(kPlatformFlagWeb); - final bool isWindows = getBoolArg(kPlatformFlagWindows); + final bool isAndroid = getBoolArg(kPlatformAndroid); + final bool isIOS = getBoolArg(kPlatformIos); + final bool isLinux = getBoolArg(kPlatformLinux); + final bool isMacos = getBoolArg(kPlatformMacos); + final bool isWeb = getBoolArg(kPlatformWeb); + final bool isWindows = getBoolArg(kPlatformWindows); if (isAndroid) { return isAndroidPlugin(plugin); } diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 77e5659df3f..741aa9d7283 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -37,8 +37,8 @@ class XCTestCommand extends PluginCommand { 'this is passed to the `-destination` argument in xcodebuild command.\n' 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', ); - argParser.addFlag(kPlatformFlagIos, help: 'Runs the iOS tests'); - argParser.addFlag(kPlatformFlagMacos, help: 'Runs the macOS tests'); + argParser.addFlag(kPlatformIos, help: 'Runs the iOS tests'); + argParser.addFlag(kPlatformMacos, help: 'Runs the macOS tests'); } @override @@ -51,8 +51,8 @@ class XCTestCommand extends PluginCommand { @override Future run() async { - final bool testIos = getBoolArg(kPlatformFlagIos); - final bool testMacos = getBoolArg(kPlatformFlagMacos); + final bool testIos = getBoolArg(kPlatformIos); + final bool testMacos = getBoolArg(kPlatformMacos); if (!(testIos || testMacos)) { print('At least one platform flag must be provided.'); diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 1ef4fdc44b4..84e3478f78c 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -53,8 +53,7 @@ void main() { }); test('skips flutter pub get for examples', () async { - final Directory plugin1Dir = - createFakePlugin('a', packagesDir, withSingleExample: true); + final Directory plugin1Dir = createFakePlugin('a', packagesDir); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -121,7 +120,7 @@ void main() { group('verifies analysis settings', () { test('fails analysis_options.yaml', () async { - createFakePlugin('foo', packagesDir, withExtraFiles: >[ + createFakePlugin('foo', packagesDir, extraFiles: >[ ['analysis_options.yaml'] ]); @@ -130,7 +129,7 @@ void main() { }); test('fails .analysis_options', () async { - createFakePlugin('foo', packagesDir, withExtraFiles: >[ + createFakePlugin('foo', packagesDir, extraFiles: >[ ['.analysis_options'] ]); @@ -140,7 +139,7 @@ void main() { test('takes an allow list', () async { final Directory pluginDir = - createFakePlugin('foo', packagesDir, withExtraFiles: >[ + createFakePlugin('foo', packagesDir, extraFiles: >[ ['analysis_options.yaml'] ]); @@ -161,7 +160,7 @@ void main() { // See: https://github.com/flutter/flutter/issues/78994 test('takes an empty allow list', () async { - createFakePlugin('foo', packagesDir, withExtraFiles: >[ + createFakePlugin('foo', packagesDir, extraFiles: >[ ['analysis_options.yaml'] ]); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 2ad17b374ba..d3c51cfc4e3 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -6,6 +6,8 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/build_examples_command.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:test/test.dart'; @@ -35,17 +37,14 @@ void main() { test('building for iOS when plugin is not set up for iOS results in no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ]); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--ipa', '--no-macos']); final String packageName = @@ -67,17 +66,16 @@ void main() { }); test('building for ios', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformIos: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint(runner, [ 'build-examples', '--ipa', @@ -114,17 +112,14 @@ void main() { test( 'building for Linux when plugin is not set up for Linux results in no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ]); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--linux']); final String packageName = @@ -146,17 +141,16 @@ void main() { }); test('building for Linux', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformLinux: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--linux']); final String packageName = @@ -181,16 +175,14 @@ void main() { test('building for macos with no implementation results in no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ]); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ]); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--macos']); final String packageName = @@ -212,18 +204,17 @@ void main() { }); test('building for macos', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ['example', 'macos', 'macos.swift'], - ], - isMacOsPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ['example', 'macos', 'macos.swift'], + ], platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--macos']); final String packageName = @@ -247,16 +238,14 @@ void main() { }); test('building for web with no implementation results in no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ]); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ]); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--web']); final String packageName = @@ -278,18 +267,17 @@ void main() { }); test('building for web', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ['example', 'web', 'index.html'], - ], - isWebPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ['example', 'web', 'index.html'], + ], platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--web']); final String packageName = @@ -315,17 +303,14 @@ void main() { test( 'building for Windows when plugin is not set up for Windows results in no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isWindowsPlugin: false); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ]); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--windows']); final String packageName = @@ -347,17 +332,16 @@ void main() { }); test('building for windows', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isWindowsPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformWindows: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--no-ipa', '--windows']); final String packageName = @@ -383,17 +367,14 @@ void main() { test( 'building for Android when plugin is not set up for Android results in no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isLinuxPlugin: false); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ]); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint( runner, ['build-examples', '--apk', '--no-ipa']); final String packageName = @@ -415,17 +396,16 @@ void main() { }); test('building for android', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isAndroidPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint(runner, [ 'build-examples', '--apk', @@ -453,17 +433,16 @@ void main() { }); test('enable-experiment flag for Android', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isAndroidPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - await runCapturingPrint(runner, [ 'build-examples', '--apk', @@ -483,17 +462,16 @@ void main() { }); test('enable-experiment flag for ios', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformIos: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - await runCapturingPrint(runner, [ 'build-examples', '--ipa', diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 58d202e1992..deb8e4f56e2 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -95,8 +95,7 @@ void main() { }); test('exclude federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', packagesDir, - parentDirectoryName: 'federated'); + createFakePlugin('plugin1', packagesDir.childDirectory('federated')); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run([ 'sample', @@ -108,8 +107,7 @@ void main() { test('exclude entire federated plugins when plugins flag is specified', () async { - createFakePlugin('plugin1', packagesDir, - parentDirectoryName: 'federated'); + createFakePlugin('plugin1', packagesDir.childDirectory('federated')); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runner.run([ 'sample', @@ -303,8 +301,8 @@ packages/plugin1/plugin1/plugin1.dart packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart packages/plugin1/plugin1_web/plugin1_web.dart '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir, - parentDirectoryName: 'plugin1'); + final Directory plugin1 = + createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); await runner.run([ @@ -323,8 +321,8 @@ packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir, - parentDirectoryName: 'plugin1'); + final Directory plugin1 = + createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); await runner.run([ @@ -343,8 +341,8 @@ packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir, - parentDirectoryName: 'plugin1'); + final Directory plugin1 = + createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); await runner.run([ diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index aaa850155da..c32c3f8e02b 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -4,6 +4,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:test/test.dart'; @@ -22,113 +23,112 @@ void main() { 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); + expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformLinux, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformMacos, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformWeb, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformWindows, plugin), isFalse); }); test('all platforms', () async { - final Directory plugin = createFakePlugin( - 'plugin', - packagesDir, - isAndroidPlugin: true, - isIosPlugin: true, - isLinuxPlugin: true, - isMacOsPlugin: true, - isWebPlugin: true, - isWindowsPlugin: true, - ); + final Directory plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + kPlatformLinux: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + kPlatformWeb: PlatformSupport.inline, + kPlatformWindows: PlatformSupport.inline, + }); - 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); + expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformIos, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformMacos, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformWeb, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformWindows, plugin), isTrue); }); test('some platforms', () async { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - isAndroidPlugin: true, - isIosPlugin: false, - isLinuxPlugin: true, - isMacOsPlugin: false, - isWebPlugin: true, - isWindowsPlugin: false, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformLinux: PlatformSupport.inline, + kPlatformWeb: PlatformSupport.inline, + }, ); - 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); + expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformMacos, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformWeb, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformWindows, 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, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + kPlatformLinux: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + kPlatformWeb: PlatformSupport.inline, + kPlatformWindows: PlatformSupport.inline, + }, ); expect( - pluginSupportsPlatform('android', plugin, + pluginSupportsPlatform(kPlatformAndroid, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform('android', plugin, + pluginSupportsPlatform(kPlatformAndroid, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform('ios', plugin, + pluginSupportsPlatform(kPlatformIos, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform('ios', plugin, + pluginSupportsPlatform(kPlatformIos, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform('linux', plugin, + pluginSupportsPlatform(kPlatformLinux, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform('linux', plugin, + pluginSupportsPlatform(kPlatformLinux, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform('macos', plugin, + pluginSupportsPlatform(kPlatformMacos, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform('macos', plugin, + pluginSupportsPlatform(kPlatformMacos, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform('web', plugin, + pluginSupportsPlatform(kPlatformWeb, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform('web', plugin, + pluginSupportsPlatform(kPlatformWeb, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform('windows', plugin, + pluginSupportsPlatform(kPlatformWindows, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform('windows', plugin, + pluginSupportsPlatform(kPlatformWindows, plugin, requiredMode: PlatformSupport.federated), isFalse); }); @@ -138,71 +138,62 @@ void main() { 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, + platformSupport: { + kPlatformAndroid: PlatformSupport.federated, + kPlatformIos: PlatformSupport.federated, + kPlatformLinux: PlatformSupport.federated, + kPlatformMacos: PlatformSupport.federated, + kPlatformWeb: PlatformSupport.federated, + kPlatformWindows: PlatformSupport.federated, + }, ); expect( - pluginSupportsPlatform('android', plugin, + pluginSupportsPlatform(kPlatformAndroid, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform('android', plugin, + pluginSupportsPlatform(kPlatformAndroid, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform('ios', plugin, + pluginSupportsPlatform(kPlatformIos, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform('ios', plugin, + pluginSupportsPlatform(kPlatformIos, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform('linux', plugin, + pluginSupportsPlatform(kPlatformLinux, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform('linux', plugin, + pluginSupportsPlatform(kPlatformLinux, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform('macos', plugin, + pluginSupportsPlatform(kPlatformMacos, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform('macos', plugin, + pluginSupportsPlatform(kPlatformMacos, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform('web', plugin, + pluginSupportsPlatform(kPlatformWeb, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform('web', plugin, + pluginSupportsPlatform(kPlatformWeb, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform('windows', plugin, + pluginSupportsPlatform(kPlatformWindows, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform('windows', plugin, + pluginSupportsPlatform(kPlatformWindows, plugin, requiredMode: PlatformSupport.inline), isFalse); }); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 9c5bd18cfb1..8893110d125 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -6,6 +6,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.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/drive_examples_command.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; @@ -35,19 +36,18 @@ void main() { }); test('driving under folder "test"', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], - isIosPlugin: true, - isAndroidPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint(runner, [ 'drive-examples', ]); @@ -80,19 +80,18 @@ void main() { }); test('driving under folder "test_driver"', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint(runner, [ 'drive-examples', ]); @@ -126,17 +125,12 @@ void main() { test('driving under folder "test_driver" when test files are missing"', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }); await expectLater( () => runCapturingPrint(runner, ['drive-examples']), @@ -145,17 +139,12 @@ void main() { test('a plugin without any integration test files is reported as an error', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'lib', 'main.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'lib', 'main.dart'], + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }); await expectLater( () => runCapturingPrint(runner, ['drive-examples']), @@ -165,21 +154,20 @@ void main() { test( 'driving under folder "test_driver" when targets are under "integration_test"', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'integration_test.dart'], - ['example', 'integration_test', 'bar_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'ignore_me.dart'], - ], - isAndroidPlugin: true, - isIosPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'integration_test.dart'], + ['example', 'integration_test', 'bar_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'ignore_me.dart'], + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint(runner, [ 'drive-examples', ]); @@ -222,17 +210,10 @@ void main() { }); test('driving when plugin does not support Linux is a no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: false); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -255,18 +236,17 @@ void main() { }); test('driving on a Linux plugin', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isLinuxPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], platformSupport: { + kPlatformLinux: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint(runner, [ 'drive-examples', '--linux', @@ -302,16 +282,10 @@ void main() { }); test('driving when plugin does not suppport macOS is a no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ]); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -333,19 +307,18 @@ void main() { expect(processRunner.recordedCalls, []); }); test('driving on a macOS plugin', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ['example', 'macos', 'macos.swift'], - ], - isMacOsPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ['example', 'macos', 'macos.swift'], + ], platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint(runner, [ 'drive-examples', '--macos', @@ -381,17 +354,10 @@ void main() { }); test('driving when plugin does not suppport web is a no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isWebPlugin: false); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -414,18 +380,17 @@ void main() { }); test('driving a web plugin', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isWebPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint(runner, [ 'drive-examples', '--web', @@ -463,17 +428,10 @@ void main() { }); test('driving when plugin does not suppport Windows is a no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isWindowsPlugin: false); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -496,18 +454,17 @@ void main() { }); test('driving on a Windows plugin', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isWindowsPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], platformSupport: { + kPlatformWindows: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final List output = await runCapturingPrint(runner, [ 'drive-examples', '--windows', @@ -543,17 +500,12 @@ void main() { }); test('driving when plugin does not support mobile is no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], - isMacOsPlugin: true); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - createFakePubspec(pluginExampleDirectory, isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -575,7 +527,8 @@ void main() { }); test('platform interface plugins are silently skipped', () async { - createFakePlugin('aplugin_platform_interface', packagesDir); + createFakePlugin('aplugin_platform_interface', packagesDir, + examples: []); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -595,19 +548,18 @@ void main() { }); test('enable-experiment flag', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], - isIosPlugin: true, - isAndroidPlugin: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - await runCapturingPrint(runner, [ 'drive-examples', '--enable-experiment=exp1', diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index 0bc8f1e197c..8c39b8cf70e 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -40,7 +40,7 @@ void main() { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(1); processRunner.processToReturn = mockProcess; - createFakePlugin('plugin', packagesDir, withExtraFiles: >[ + createFakePlugin('plugin', packagesDir, extraFiles: >[ ['lib/test/should_not_run_e2e.dart'], ['example', 'test_driver', 'plugin_e2e.dart'], ['example', 'test_driver', 'plugin_e2e_test.dart'], @@ -65,7 +65,7 @@ void main() { }); test('runs e2e tests', () async { - createFakePlugin('plugin', packagesDir, withExtraFiles: >[ + createFakePlugin('plugin', packagesDir, extraFiles: >[ ['test', 'plugin_test.dart'], ['test', 'plugin_e2e.dart'], ['should_not_run_e2e.dart'], @@ -168,7 +168,7 @@ void main() { }); test('experimental flag', () async { - createFakePlugin('plugin', packagesDir, withExtraFiles: >[ + createFakePlugin('plugin', packagesDir, extraFiles: >[ ['test', 'plugin_test.dart'], ['test', 'plugin_e2e.dart'], ['should_not_run_e2e.dart'], diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index a1c2d3b864c..3c6319ed447 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.dart @@ -5,6 +5,8 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.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/java_test_command.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -34,10 +36,10 @@ void main() { final Directory plugin = createFakePlugin( 'plugin1', packagesDir, - isAndroidPlugin: true, - isFlutter: true, - withSingleExample: true, - withExtraFiles: >[ + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: >[ ['example/android', 'gradlew'], ['android/src/test', 'example_test.java'], ], @@ -61,10 +63,10 @@ void main() { final Directory plugin = createFakePlugin( 'plugin1', packagesDir, - isAndroidPlugin: true, - isFlutter: true, - withSingleExample: true, - withExtraFiles: >[ + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: >[ ['example/android', 'gradlew'], ['example/android/app/src/test', 'example_test.java'], ], diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 0183704f72c..45dc92e6994 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -45,7 +45,7 @@ void main() { }); test('only runs on macOS', () async { - createFakePlugin('plugin1', packagesDir, withExtraFiles: >[ + createFakePlugin('plugin1', packagesDir, extraFiles: >[ ['plugin1.podspec'], ]); @@ -59,11 +59,11 @@ void main() { }); test('runs pod lib lint on a podspec', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, - withExtraFiles: >[ - ['ios', 'plugin1.podspec'], - ['bogus.dart'], // Ignore non-podspecs. - ]); + final Directory plugin1Dir = + createFakePlugin('plugin1', packagesDir, extraFiles: >[ + ['ios', 'plugin1.podspec'], + ['bogus.dart'], // Ignore non-podspecs. + ]); processRunner.resultStdout = 'Foo'; processRunner.resultStderr = 'Bar'; @@ -106,10 +106,10 @@ void main() { }); test('skips podspecs with known issues', () async { - createFakePlugin('plugin1', packagesDir, withExtraFiles: >[ + createFakePlugin('plugin1', packagesDir, extraFiles: >[ ['plugin1.podspec'] ]); - createFakePlugin('plugin2', packagesDir, withExtraFiles: >[ + createFakePlugin('plugin2', packagesDir, extraFiles: >[ ['plugin2.podspec'] ]); @@ -125,10 +125,10 @@ void main() { }); test('allow warnings for podspecs with known warnings', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, - withExtraFiles: >[ - ['plugin1.podspec'], - ]); + final Directory plugin1Dir = + createFakePlugin('plugin1', packagesDir, extraFiles: >[ + ['plugin1.podspec'], + ]); await runner.run(['podspecs', '--ignore-warnings=plugin1']); diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index 02b898c5c3f..22f00ea046c 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -42,10 +42,10 @@ void main() { }); test('lists examples', () async { - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir, - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3', packagesDir); + examples: ['example1', 'example2']); + createFakePlugin('plugin3', packagesDir, examples: []); final List examples = await runCapturingPrint(runner, ['list', '--type=example']); @@ -61,10 +61,10 @@ void main() { }); test('lists packages', () async { - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir, - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3', packagesDir); + examples: ['example1', 'example2']); + createFakePlugin('plugin3', packagesDir, examples: []); final List packages = await runCapturingPrint(runner, ['list', '--type=package']); @@ -83,10 +83,10 @@ void main() { }); test('lists files', () async { - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir, - withExamples: ['example1', 'example2']); - createFakePlugin('plugin3', packagesDir); + examples: ['example1', 'example2']); + createFakePlugin('plugin3', packagesDir, examples: []); final List examples = await runCapturingPrint(runner, ['list', '--type=file']); @@ -95,11 +95,14 @@ void main() { examples, unorderedEquals([ '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/CHANGELOG.md', '/packages/plugin1/example/pubspec.yaml', '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/CHANGELOG.md', '/packages/plugin2/example/example1/pubspec.yaml', '/packages/plugin2/example/example2/pubspec.yaml', '/packages/plugin3/pubspec.yaml', + '/packages/plugin3/CHANGELOG.md', ]), ); }); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index c0ccd2989cf..26938cc9279 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -40,8 +40,10 @@ void main() { }); test('publish check all packages', () async { - final Directory plugin1Dir = createFakePlugin('a', packagesDir); - final Directory plugin2Dir = createFakePlugin('b', packagesDir); + final Directory plugin1Dir = + createFakePlugin('plugin_tools_test_package_a', packagesDir); + final Directory plugin2Dir = + createFakePlugin('plugin_tools_test_package_b', packagesDir); processRunner.processesToReturn.add( MockProcess()..exitCodeCompleter.complete(0), @@ -66,7 +68,7 @@ void main() { }); test('fail on negative test', () async { - createFakePlugin('a', packagesDir); + createFakePlugin('plugin_tools_test_package_a', packagesDir); final MockProcess process = MockProcess(); process.stdoutController.close(); // ignore: unawaited_futures @@ -186,13 +188,8 @@ void main() { ); runner.addCommand(command); - final Directory plugin1Dir = - createFakePlugin('no_publish_a', packagesDir, includeVersion: true); - final Directory plugin2Dir = - createFakePlugin('no_publish_b', packagesDir, includeVersion: true); - - createFakePubspec(plugin1Dir, name: 'no_publish_a', version: '0.1.0'); - createFakePubspec(plugin2Dir, name: 'no_publish_b', version: '0.2.0'); + createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); + createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); processRunner.processesToReturn.add( MockProcess()..exitCodeCompleter.complete(0), @@ -250,13 +247,8 @@ void main() { ); runner.addCommand(command); - final Directory plugin1Dir = - createFakePlugin('no_publish_a', packagesDir, includeVersion: true); - final Directory plugin2Dir = - createFakePlugin('no_publish_b', packagesDir, includeVersion: true); - - createFakePubspec(plugin1Dir, name: 'no_publish_a', version: '0.1.0'); - createFakePubspec(plugin2Dir, name: 'no_publish_b', version: '0.2.0'); + createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); + createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); processRunner.processesToReturn.add( MockProcess()..exitCodeCompleter.complete(0), @@ -318,12 +310,9 @@ void main() { runner.addCommand(command); final Directory plugin1Dir = - createFakePlugin('no_publish_a', packagesDir, includeVersion: true); - final Directory plugin2Dir = - createFakePlugin('no_publish_b', packagesDir, includeVersion: true); + createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); + createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - createFakePubspec(plugin1Dir, name: 'no_publish_a', version: '0.1.0'); - createFakePubspec(plugin2Dir, name: 'no_publish_b', version: '0.2.0'); await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); processRunner.processesToReturn.add( diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index ef682bfe61f..a2ea9816ea0 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -50,9 +50,8 @@ void main() { testRoot = fileSystem.directory(testRoot.resolveSymbolicLinksSync()); packagesDir = createPackagesDirectory(parentDir: testRoot); pluginDir = - createFakePlugin(testPluginName, packagesDir, withSingleExample: false); + createFakePlugin(testPluginName, packagesDir, examples: []); assert(pluginDir != null && pluginDir.existsSync()); - createFakePubspec(pluginDir, version: '0.0.1'); io.Process.runSync('git', ['init'], workingDirectory: testRoot.path); gitDir = await GitDir.fromExisting(testRoot.path); @@ -138,7 +137,8 @@ void main() { }); test('can publish non-flutter package', () async { - createFakePubspec(pluginDir, version: '0.0.1', isFlutter: false); + const String packageName = 'a_package'; + createFakePackage(packageName, packagesDir); io.Process.runSync('git', ['init'], workingDirectory: testRoot.path); gitDir = await GitDir.fromExisting(testRoot.path); @@ -149,7 +149,7 @@ void main() { await commandRunner.run([ 'publish-plugin', '--package', - testPluginName, + packageName, '--no-push-tags', '--no-tag-release' ]); @@ -284,9 +284,9 @@ void main() { '--no-push-tags', ]); - final String? tag = - (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) - .stdout as String?; + final String? tag = (await gitDir + .runCommand(['show-ref', '$testPluginName-v0.0.1'])) + .stdout as String?; expect(tag, isNotEmpty); }); @@ -303,7 +303,7 @@ void main() { expect(printedMessages, contains('Publish foo failed.')); final String? tag = (await gitDir.runCommand( - ['show-ref', 'fake_package-v0.0.1'], + ['show-ref', '$testPluginName-v0.0.1'], throwOnError: false)) .stdout as String?; expect(tag, isEmpty); @@ -342,7 +342,7 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); expect(printedMessages.last, 'Done!'); }); @@ -360,7 +360,7 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); expect(printedMessages.last, 'Done!'); }); @@ -378,7 +378,7 @@ void main() { containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', - 'Tagging release fake_package-v0.0.1...', + 'Tagging release $testPluginName-v0.0.1...', 'Pushing tag to upstream...', 'Done!' ])); @@ -399,7 +399,7 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'origin'); - expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); expect(printedMessages.last, 'Done!'); }); @@ -428,15 +428,12 @@ void main() { test('can release newly created plugins', () async { // Non-federated - final Directory pluginDir1 = - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, - withSingleExample: true, parentDirectoryName: 'plugin2'); - createFakePubspec(pluginDir1, - name: 'plugin1', isFlutter: false, version: '0.0.1'); - createFakePubspec(pluginDir2, - name: 'plugin2', isFlutter: false, version: '0.0.1'); + final Directory pluginDir2 = createFakePlugin( + 'plugin2', + packagesDir.childDirectory('plugin2'), + ); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -467,10 +464,7 @@ void main() { test('can release newly created plugins, while there are existing plugins', () async { // Prepare an exiting plugin and tag it - final Directory pluginDir0 = - createFakePlugin('plugin0', packagesDir, withSingleExample: true); - createFakePubspec(pluginDir0, - name: 'plugin0', isFlutter: false, version: '0.0.1'); + createFakePlugin('plugin0', packagesDir); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -481,15 +475,10 @@ void main() { processRunner.pushTagsArgs.clear(); // Non-federated - final Directory pluginDir1 = - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, - withSingleExample: true, parentDirectoryName: 'plugin2'); - createFakePubspec(pluginDir1, - name: 'plugin1', isFlutter: false, version: '0.0.1'); - createFakePubspec(pluginDir2, - name: 'plugin2', isFlutter: false, version: '0.0.1'); + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -517,15 +506,10 @@ void main() { test('can release newly created plugins, dry run', () async { // Non-federated - final Directory pluginDir1 = - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, - withSingleExample: true, parentDirectoryName: 'plugin2'); - createFakePubspec(pluginDir1, - name: 'plugin1', isFlutter: false, version: '0.0.1'); - createFakePubspec(pluginDir2, - name: 'plugin2', isFlutter: false, version: '0.0.1'); + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. @@ -558,15 +542,10 @@ void main() { test('version change triggers releases.', () async { // Non-federated - final Directory pluginDir1 = - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, - withSingleExample: true, parentDirectoryName: 'plugin2'); - createFakePubspec(pluginDir1, - name: 'plugin1', isFlutter: false, version: '0.0.1'); - createFakePubspec(pluginDir2, - name: 'plugin2', isFlutter: false, version: '0.0.1'); + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -641,15 +620,10 @@ void main() { 'delete package will not trigger publish but exit the command successfully.', () async { // Non-federated - final Directory pluginDir1 = - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, - withSingleExample: true, parentDirectoryName: 'plugin2'); - createFakePubspec(pluginDir1, - name: 'plugin1', isFlutter: false, version: '0.0.1'); - createFakePubspec(pluginDir2, - name: 'plugin2', isFlutter: false, version: '0.0.1'); + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -722,14 +696,11 @@ void main() { () async { // Non-federated final Directory pluginDir1 = - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, - withSingleExample: true, parentDirectoryName: 'plugin2'); - createFakePubspec(pluginDir1, - name: 'plugin1', isFlutter: false, version: '0.0.2'); - createFakePubspec(pluginDir2, - name: 'plugin2', isFlutter: false, version: '0.0.2'); + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), + version: '0.0.2'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); // Immediately return 0 when running `pub publish`. @@ -795,15 +766,10 @@ void main() { test('No version change does not release any plugins', () async { // Non-federated - final Directory pluginDir1 = - createFakePlugin('plugin1', packagesDir, withSingleExample: true); + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, - withSingleExample: true, parentDirectoryName: 'plugin2'); - createFakePubspec(pluginDir1, - name: 'plugin1', isFlutter: false, version: '0.0.1'); - createFakePubspec(pluginDir2, - name: 'plugin2', isFlutter: false, version: '0.0.1'); + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); io.Process.runSync('git', ['init'], workingDirectory: testRoot.path); diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index f5fe6aef849..af27ac5bd2f 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -88,8 +88,7 @@ dev_dependencies: } test('passes for a plugin following conventions', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} @@ -114,8 +113,7 @@ ${devDependenciesSection()} }); test('passes for a Flutter package following conventions', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin')} @@ -163,8 +161,7 @@ ${dependenciesSection()} }); test('fails when homepage is included', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true, includeHomepage: true)} @@ -184,8 +181,7 @@ ${devDependenciesSection()} }); test('fails when repository is missing', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true, includeRepository: false)} @@ -205,8 +201,7 @@ ${devDependenciesSection()} }); test('fails when homepage is given instead of repository', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} @@ -226,8 +221,7 @@ ${devDependenciesSection()} }); test('fails when issue tracker is missing', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true, includeIssueTracker: false)} @@ -247,8 +241,7 @@ ${devDependenciesSection()} }); test('fails when environment section is out of order', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} @@ -268,8 +261,7 @@ ${environmentSection()} }); test('fails when flutter section is out of order', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} @@ -289,8 +281,7 @@ ${devDependenciesSection()} }); test('fails when dependencies section is out of order', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} @@ -310,8 +301,7 @@ ${dependenciesSection()} }); test('fails when devDependencies section is out of order', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, withSingleExample: true); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${headerSection('plugin', isPlugin: true)} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 5cbbdf5b8d4..b2f663f1e90 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -5,6 +5,8 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.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/test_command.dart'; import 'package:test/test.dart'; @@ -29,14 +31,14 @@ void main() { }); test('runs flutter test on each plugin', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); + final Directory plugin1Dir = + createFakePlugin('plugin1', packagesDir, extraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = + createFakePlugin('plugin2', packagesDir, extraFiles: >[ + ['test', 'empty_test.dart'], + ]); await runner.run(['test']); @@ -53,10 +55,10 @@ void main() { test('skips testing plugins without test directory', () async { createFakePlugin('plugin1', packagesDir); - final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); + final Directory plugin2Dir = + createFakePlugin('plugin2', packagesDir, extraFiles: >[ + ['test', 'empty_test.dart'], + ]); await runner.run(['test']); @@ -70,16 +72,14 @@ void main() { }); test('runs pub run test on non-Flutter packages', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, - isFlutter: true, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, - isFlutter: false, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); + final Directory pluginDir = + createFakePlugin('a', packagesDir, extraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory packageDir = + createFakePackage('b', packagesDir, extraFiles: >[ + ['test', 'empty_test.dart'], + ]); await runner.run(['test', '--enable-experiment=exp1']); @@ -89,12 +89,12 @@ void main() { ProcessCall( 'flutter', const ['test', '--color', '--enable-experiment=exp1'], - plugin1Dir.path), - ProcessCall('dart', const ['pub', 'get'], plugin2Dir.path), + pluginDir.path), + ProcessCall('dart', const ['pub', 'get'], packageDir.path), ProcessCall( 'dart', const ['pub', 'run', '--enable-experiment=exp1', 'test'], - plugin2Dir.path), + packageDir.path), ]), ); }); @@ -103,11 +103,12 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - withExtraFiles: >[ + extraFiles: >[ ['test', 'empty_test.dart'], ], - isFlutter: true, - isWebPlugin: true, + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }, ); await runner.run(['test']); @@ -124,16 +125,14 @@ void main() { }); test('enable-experiment flag', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, - isFlutter: true, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, - isFlutter: false, - withExtraFiles: >[ - ['test', 'empty_test.dart'], - ]); + final Directory pluginDir = + createFakePlugin('a', packagesDir, extraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory packageDir = + createFakePackage('b', packagesDir, extraFiles: >[ + ['test', 'empty_test.dart'], + ]); await runner.run(['test', '--enable-experiment=exp1']); @@ -143,12 +142,12 @@ void main() { ProcessCall( 'flutter', const ['test', '--color', '--enable-experiment=exp1'], - plugin1Dir.path), - ProcessCall('dart', const ['pub', 'get'], plugin2Dir.path), + pluginDir.path), + ProcessCall('dart', const ['pub', 'get'], packageDir.path), ProcessCall( 'dart', const ['pub', 'run', '--enable-experiment=exp1', 'test'], - plugin2Dir.path), + packageDir.path), ]), ); }); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 79c46fcc50e..4ced4eb4837 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -9,6 +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/core.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'; @@ -32,61 +33,70 @@ Directory createPackagesDirectory( } /// Creates a plugin package with the given [name] in [packagesDirectory]. +/// +/// [platformSupport] is a map of platform string to the support details for +/// that platform. Directory createFakePlugin( String name, - Directory packagesDirectory, { - bool withSingleExample = false, - List withExamples = const [], - List> withExtraFiles = const >[], - bool isFlutter = true, - // TODO(stuartmorgan): Change these platform switches to support type enums. - bool isAndroidPlugin = false, - bool isIosPlugin = false, - bool isWebPlugin = false, - bool isLinuxPlugin = false, - bool isMacOsPlugin = false, - bool isWindowsPlugin = false, - bool includeChangeLog = false, - bool includeVersion = false, - String version = '0.0.1', - String parentDirectoryName = '', + Directory parentDirectory, { + List examples = const ['example'], + List> extraFiles = const >[], + Map platformSupport = + const {}, + String? version = '0.0.1', }) { - assert(!(withSingleExample && withExamples.isNotEmpty), - 'cannot pass withSingleExample and withExamples simultaneously'); + final Directory pluginDirectory = createFakePackage(name, parentDirectory, + isFlutter: true, + examples: examples, + extraFiles: extraFiles, + version: version); + + createFakePubspec( + pluginDirectory, + name: name, + isFlutter: true, + isPlugin: true, + platformSupport: platformSupport, + version: version, + ); - Directory parentDirectory = packagesDirectory; - if (parentDirectoryName != '') { - parentDirectory = parentDirectory.childDirectory(parentDirectoryName); + final FileSystem fileSystem = pluginDirectory.fileSystem; + for (final List file in extraFiles) { + final List newFilePath = [pluginDirectory.path, ...file]; + final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath)); + newFile.createSync(recursive: true); } - final Directory pluginDirectory = parentDirectory.childDirectory(name); - pluginDirectory.createSync(recursive: true); - - createFakePubspec(pluginDirectory, - name: name, - isFlutter: isFlutter, - androidSupport: isAndroidPlugin ? PlatformSupport.inline : null, - iosSupport: isIosPlugin ? PlatformSupport.inline : null, - webSupport: isWebPlugin ? PlatformSupport.inline : null, - linuxSupport: isLinuxPlugin ? PlatformSupport.inline : null, - macosSupport: isMacOsPlugin ? PlatformSupport.inline : null, - windowsSupport: isWindowsPlugin ? PlatformSupport.inline : null, - version: includeVersion ? version : null); - if (includeChangeLog) { - createFakeCHANGELOG(pluginDirectory, ''' -## 0.0.1 + + return pluginDirectory; +} + +/// Creates a plugin package with the given [name] in [packagesDirectory]. +Directory createFakePackage( + String name, + Directory parentDirectory, { + List examples = const ['example'], + List> extraFiles = const >[], + bool isFlutter = false, + String? version = '0.0.1', +}) { + final Directory packageDirectory = parentDirectory.childDirectory(name); + packageDirectory.createSync(recursive: true); + + createFakePubspec(packageDirectory, name: name, isFlutter: isFlutter); + createFakeCHANGELOG(packageDirectory, ''' +## $version * Some changes. '''); - } - if (withSingleExample) { - final Directory exampleDir = pluginDirectory.childDirectory('example') + if (examples.length == 1) { + final Directory exampleDir = packageDirectory.childDirectory(examples.first) ..createSync(); createFakePubspec(exampleDir, name: '${name}_example', isFlutter: isFlutter, publishTo: 'none'); - } else if (withExamples.isNotEmpty) { - final Directory exampleDir = pluginDirectory.childDirectory('example') + } else if (examples.isNotEmpty) { + final Directory exampleDir = packageDirectory.childDirectory('example') ..createSync(); - for (final String example in withExamples) { + for (final String example in examples) { final Directory currentExample = exampleDir.childDirectory(example) ..createSync(); createFakePubspec(currentExample, @@ -94,14 +104,14 @@ Directory createFakePlugin( } } - final FileSystem fileSystem = pluginDirectory.fileSystem; - for (final List file in withExtraFiles) { - final List newFilePath = [pluginDirectory.path, ...file]; + final FileSystem fileSystem = packageDirectory.fileSystem; + for (final List file in extraFiles) { + final List newFilePath = [packageDirectory.path, ...file]; final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath)); newFile.createSync(recursive: true); } - return pluginDirectory; + return packageDirectory; } void createFakeCHANGELOG(Directory parent, String texts) { @@ -110,45 +120,38 @@ void createFakeCHANGELOG(Directory parent, String texts) { } /// Creates a `pubspec.yaml` file with a flutter dependency. +/// +/// [platformSupport] is a map of platform string to the support details for +/// that platform. If empty, no `plugin` entry will be created unless `isPlugin` +/// is set to `true`. void createFakePubspec( Directory parent, { String name = 'fake_package', bool isFlutter = true, - PlatformSupport? androidSupport, - PlatformSupport? iosSupport, - PlatformSupport? linuxSupport, - PlatformSupport? macosSupport, - PlatformSupport? webSupport, - PlatformSupport? windowsSupport, + bool isPlugin = false, + Map platformSupport = + const {}, String publishTo = 'http://no_pub_server.com', String? version, }) { + isPlugin |= platformSupport.isNotEmpty; parent.childFile('pubspec.yaml').createSync(); String yaml = ''' name: $name +'''; + if (isFlutter) { + if (isPlugin) { + yaml += ''' flutter: plugin: platforms: '''; - if (androidSupport != null) { - yaml += _pluginPlatformSection('android', androidSupport, name); - } - if (iosSupport != null) { - yaml += _pluginPlatformSection('ios', iosSupport, name); - } - if (webSupport != null) { - yaml += _pluginPlatformSection('web', webSupport, name); - } - if (linuxSupport != null) { - yaml += _pluginPlatformSection('linux', linuxSupport, name); - } - if (macosSupport != null) { - yaml += _pluginPlatformSection('macos', macosSupport, name); - } - if (windowsSupport != null) { - yaml += _pluginPlatformSection('windows', windowsSupport, name); - } - if (isFlutter) { + for (final MapEntry platform + in platformSupport.entries) { + yaml += _pluginPlatformSection(platform.key, platform.value, name); + } + } + yaml += ''' dependencies: flutter: @@ -177,34 +180,34 @@ String _pluginPlatformSection( '''; } switch (platform) { - case 'android': + case kPlatformAndroid: return ''' android: package: io.flutter.plugins.fake pluginClass: FakePlugin '''; - case 'ios': + case kPlatformIos: return ''' ios: pluginClass: FLTFakePlugin '''; - case 'linux': + case kPlatformLinux: return ''' linux: pluginClass: FakePlugin '''; - case 'macos': + case kPlatformMacos: return ''' macos: pluginClass: FakePlugin '''; - case 'web': + case kPlatformWeb: return ''' web: pluginClass: FakePlugin fileName: ${packageName}_web.dart '''; - case 'windows': + case kPlatformWindows: return ''' windows: pluginClass: FakePlugin diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index a8e7e20bad2..6035360a221 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -100,12 +100,12 @@ void main() { }); test('allows valid version', () async { - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '2.0.0'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); @@ -127,12 +127,12 @@ void main() { }); test('denies invalid version', () async { - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '0.2.0'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final Future> result = runCapturingPrint( runner, ['version-check', '--base-sha=master']); @@ -152,12 +152,12 @@ void main() { }); test('allows valid version without explicit base-sha', () async { - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '2.0.0'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List output = await runCapturingPrint(runner, ['version-check']); @@ -171,11 +171,11 @@ void main() { }); test('allows valid version for new package.', () async { - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '1.0.0'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { - 'HEAD:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List output = await runCapturingPrint(runner, ['version-check']); @@ -190,12 +190,12 @@ void main() { }); test('allows likely reverts.', () async { - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '0.6.1'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.6.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List output = await runCapturingPrint(runner, ['version-check']); @@ -209,12 +209,12 @@ void main() { }); test('denies lower version that could not be a simple revert', () async { - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '0.5.1'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.5.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final Future> result = runCapturingPrint(runner, ['version-check']); @@ -226,12 +226,12 @@ void main() { }); test('denies invalid version without explicit base-sha', () async { - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '0.2.0'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 0.0.1', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final Future> result = runCapturingPrint(runner, ['version-check']); @@ -243,8 +243,8 @@ void main() { }); test('gracefully handles missing pubspec.yaml', () async { - final Directory pluginDir = createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, examples: []); gitDiffResponse = 'packages/plugin/pubspec.yaml'; pluginDir.childFile('pubspec.yaml').deleteSync(); final List output = await runCapturingPrint( @@ -265,14 +265,15 @@ void main() { }); test('allows minor changes to platform interfaces', () async { + const String newVersion = '1.1.0'; createFakePlugin('plugin_platform_interface', packagesDir, - includeChangeLog: true, includeVersion: true); + version: newVersion); gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.1.0', + 'version: $newVersion', }; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); @@ -299,14 +300,15 @@ void main() { }); test('disallows breaking changes to platform interfaces', () async { + const String newVersion = '2.0.0'; createFakePlugin('plugin_platform_interface', packagesDir, - includeChangeLog: true, includeVersion: true); + version: newVersion); gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: 2.0.0', + 'version: $newVersion', }; final Future> output = runCapturingPrint( runner, ['version-check', '--base-sha=master']); @@ -332,16 +334,11 @@ void main() { test('Allow empty lines in front of the first version in CHANGELOG', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); - - createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' - - - -## 1.0.1 - +## $version * Some changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); @@ -358,13 +355,10 @@ void main() { }); test('Throws if versions in changelog and pubspec do not match', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); - - createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.1'); const String changelog = ''' ## 1.0.2 - * Some changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); @@ -392,13 +386,12 @@ The first version listed in CHANGELOG.md is 1.0.2. }); test('Success if CHANGELOG and pubspec versions match', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); - createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); const String changelog = ''' -## 1.0.1 - +## $version * Some changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); @@ -417,17 +410,13 @@ The first version listed in CHANGELOG.md is 1.0.2. test( 'Fail if pubspec version only matches an older version listed in CHANGELOG', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); - createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.0'); const String changelog = ''' ## 1.0.1 - * Some changes. - ## 1.0.0 - * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); @@ -458,17 +447,14 @@ The first version listed in CHANGELOG.md is 1.0.1. test('Allow NEXT as a placeholder for gathering CHANGELOG entries', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String version = '1.0.0'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); - createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.0'); const String changelog = ''' ## NEXT - * Some changes that won't be published until the next time there's a release. - -## 1.0.0 - +## $version * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); @@ -486,21 +472,16 @@ The first version listed in CHANGELOG.md is 1.0.1. test('Fail if NEXT is left in the CHANGELOG when adding a version bump', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); - createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); const String changelog = ''' -## 1.0.1 - +## $version * Some changes. - ## NEXT - * Some changes that should have been folded in 1.0.1. - ## 1.0.0 - * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); @@ -529,17 +510,13 @@ into the new version's release notes. }); test('Fail if the version changes without replacing NEXT', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.1'); - createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); const String changelog = ''' ## NEXT - * Some changes that should be listed as part of 1.0.1. - ## 1.0.0 - * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); @@ -588,12 +565,12 @@ The first version listed in CHANGELOG.md is 1.0.0. 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '2.0.0'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List output = await runCapturingPrint(runner, ['version-check', '--base-sha=master', '--against-pub']); @@ -625,12 +602,12 @@ The first version listed in CHANGELOG.md is 1.0.0. 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '2.0.0'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; bool hasError = false; @@ -670,12 +647,12 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '2.0.0'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; bool hasError = false; final List result = await runCapturingPrint(runner, [ @@ -714,12 +691,12 @@ ${indentation}HTTP response: xx 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - createFakePlugin('plugin', packagesDir, - includeChangeLog: true, includeVersion: true); + const String newVersion = '2.0.0'; + createFakePlugin('plugin', packagesDir, version: newVersion); gitDiffResponse = 'packages/plugin/pubspec.yaml'; gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List result = await runCapturingPrint(runner, ['version-check', '--base-sha=master', '--against-pub']); diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index c0bd6b5dee5..050a4d4da73 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -113,16 +113,11 @@ void main() { group('iOS', () { test('skip if iOS is not supported', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: false, - isMacOsPlugin: true); - - createFakePubspec(pluginDirectory.childDirectory('example'), - isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -135,17 +130,11 @@ void main() { }); test('skip if iOS is implemented in a federated package', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); - createFakePubspec(pluginDirectory, - iosSupport: PlatformSupport.federated); - - createFakePubspec(pluginDirectory.childDirectory('example'), - isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformIos: PlatformSupport.federated + }); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -158,25 +147,20 @@ void main() { }); test('running with correct destination, exclude 1 plugin', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin1', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); + createFakePlugin('plugin1', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformIos: PlatformSupport.inline + }); final Directory pluginDirectory2 = - createFakePlugin('plugin2', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); + createFakePlugin('plugin2', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformIos: PlatformSupport.inline + }); - final Directory pluginExampleDirectory1 = - pluginDirectory1.childDirectory('example'); - createFakePubspec(pluginExampleDirectory1, isFlutter: true); final Directory pluginExampleDirectory2 = pluginDirectory2.childDirectory('example'); - createFakePubspec(pluginExampleDirectory2, isFlutter: true); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -223,17 +207,15 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformIos: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); - final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); processRunner.processToReturn = mockProcess; @@ -276,16 +258,13 @@ void main() { group('macOS', () { test('skip if macOS is not supported', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isIosPlugin: true, - isMacOsPlugin: false); - - createFakePubspec(pluginDirectory.childDirectory('example'), - isFlutter: true); + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: >[ + ['example', 'test'], + ], + ); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -298,17 +277,11 @@ void main() { }); test('skip if macOS is implemented in a federated package', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isMacOsPlugin: true); - createFakePubspec(pluginDirectory, - macosSupport: PlatformSupport.federated); - - createFakePubspec(pluginDirectory.childDirectory('example'), - isFlutter: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformMacos: PlatformSupport.federated, + }); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -322,15 +295,14 @@ void main() { test('runs for macOS plugin', () async { final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, - withExtraFiles: >[ - ['example', 'test'], - ], - isMacOsPlugin: true); + createFakePlugin('plugin', packagesDir, extraFiles: >[ + ['example', 'test'], + ], platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - createFakePubspec(pluginExampleDirectory, isFlutter: true); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); From 37f79be4693c3a02d156081516102d9de05c9411 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 17 Jun 2021 18:23:18 -0700 Subject: [PATCH 071/249] [flutter_plugin_tools] Simplify extraFiles in test utils (#4066) Rather than taking a list of list of path elements, just accept a list of Posix-style paths. In practice, the API was already being partially used that way. --- script/tool/test/analyze_command_test.dart | 21 +- .../test/build_examples_command_test.dart | 158 +++++++----- .../test/drive_examples_command_test.dart | 233 +++++++++++------- script/tool/test/firebase_test_lab_test.dart | 87 +++---- script/tool/test/java_test_command_test.dart | 12 +- .../tool/test/lint_podspecs_command_test.dart | 34 ++- script/tool/test/test_command_test.dart | 46 ++-- script/tool/test/util.dart | 26 +- script/tool/test/xctest_command_test.dart | 32 +-- 9 files changed, 343 insertions(+), 306 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 84e3478f78c..464aa1d9147 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -120,28 +120,24 @@ void main() { group('verifies analysis settings', () { test('fails analysis_options.yaml', () async { - createFakePlugin('foo', packagesDir, extraFiles: >[ - ['analysis_options.yaml'] - ]); + createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); await expectLater(() => runner.run(['analyze']), throwsA(const TypeMatcher())); }); test('fails .analysis_options', () async { - createFakePlugin('foo', packagesDir, extraFiles: >[ - ['.analysis_options'] - ]); + createFakePlugin('foo', packagesDir, + extraFiles: ['.analysis_options']); await expectLater(() => runner.run(['analyze']), throwsA(const TypeMatcher())); }); test('takes an allow list', () async { - final Directory pluginDir = - createFakePlugin('foo', packagesDir, extraFiles: >[ - ['analysis_options.yaml'] - ]); + final Directory pluginDir = createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -160,9 +156,8 @@ void main() { // See: https://github.com/flutter/flutter/issues/78994 test('takes an empty allow list', () async { - createFakePlugin('foo', packagesDir, extraFiles: >[ - ['analysis_options.yaml'] - ]); + createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index d3c51cfc4e3..7fc97838c0e 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -37,10 +37,8 @@ void main() { test('building for iOS when plugin is not set up for iOS results in no-op', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], - ]); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + extraFiles: ['example/test']); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -66,12 +64,16 @@ void main() { }); test('building for ios', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], - ], platformSupport: { - kPlatformIos: PlatformSupport.inline - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test', + ], + platformSupport: { + kPlatformIos: PlatformSupport.inline + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -113,8 +115,8 @@ void main() { 'building for Linux when plugin is not set up for Linux results in no-op', () async { final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ]); final Directory pluginExampleDirectory = @@ -141,12 +143,16 @@ void main() { }); test('building for Linux', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], - ], platformSupport: { - kPlatformLinux: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test', + ], + platformSupport: { + kPlatformLinux: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -176,8 +182,8 @@ void main() { test('building for macos with no implementation results in no-op', () async { final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ]); final Directory pluginExampleDirectory = @@ -204,13 +210,17 @@ void main() { }); test('building for macos', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], - ['example', 'macos', 'macos.swift'], - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test', + 'example/macos/macos.swift', + ], + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -239,8 +249,8 @@ void main() { test('building for web with no implementation results in no-op', () async { final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ]); final Directory pluginExampleDirectory = @@ -267,13 +277,17 @@ void main() { }); test('building for web', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], - ['example', 'web', 'index.html'], - ], platformSupport: { - kPlatformWeb: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test', + 'example/web/index.html', + ], + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -304,8 +318,8 @@ void main() { 'building for Windows when plugin is not set up for Windows results in no-op', () async { final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ]); final Directory pluginExampleDirectory = @@ -332,12 +346,16 @@ void main() { }); test('building for windows', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], - ], platformSupport: { - kPlatformWindows: PlatformSupport.inline - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test', + ], + platformSupport: { + kPlatformWindows: PlatformSupport.inline + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -368,8 +386,8 @@ void main() { 'building for Android when plugin is not set up for Android results in no-op', () async { final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ]); final Directory pluginExampleDirectory = @@ -396,12 +414,16 @@ void main() { }); test('building for android', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -433,12 +455,16 @@ void main() { }); test('enable-experiment flag for Android', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -462,12 +488,16 @@ void main() { }); test('enable-experiment flag for ios', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], - ], platformSupport: { - kPlatformIos: PlatformSupport.inline - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test', + ], + platformSupport: { + kPlatformIos: PlatformSupport.inline + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 8893110d125..3175f716354 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -36,14 +36,18 @@ void main() { }); test('driving under folder "test"', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test/plugin.dart', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -80,14 +84,18 @@ void main() { }); test('driving under folder "test_driver"', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -125,12 +133,17 @@ void main() { test('driving under folder "test_driver" when test files are missing"', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - }); + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + ); await expectLater( () => runCapturingPrint(runner, ['drive-examples']), @@ -139,12 +152,17 @@ void main() { test('a plugin without any integration test files is reported as an error', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'lib', 'main.dart'], - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - }); + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/lib/main.dart', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + ); await expectLater( () => runCapturingPrint(runner, ['drive-examples']), @@ -154,16 +172,20 @@ void main() { test( 'driving under folder "test_driver" when targets are under "integration_test"', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'integration_test.dart'], - ['example', 'integration_test', 'bar_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'ignore_me.dart'], - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/integration_test.dart', + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/integration_test/ignore_me.dart', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -210,9 +232,9 @@ void main() { }); test('driving when plugin does not support Linux is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', ]); final List output = await runCapturingPrint(runner, [ @@ -236,13 +258,17 @@ void main() { }); test('driving on a Linux plugin', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], platformSupport: { - kPlatformLinux: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformLinux: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -282,9 +308,9 @@ void main() { }); test('driving when plugin does not suppport macOS is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', ]); final List output = await runCapturingPrint(runner, [ @@ -307,14 +333,18 @@ void main() { expect(processRunner.recordedCalls, []); }); test('driving on a macOS plugin', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ['example', 'macos', 'macos.swift'], - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + 'example/macos/macos.swift', + ], + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -354,9 +384,9 @@ void main() { }); test('driving when plugin does not suppport web is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', ]); final List output = await runCapturingPrint(runner, [ @@ -380,13 +410,17 @@ void main() { }); test('driving a web plugin', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], platformSupport: { - kPlatformWeb: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -428,9 +462,9 @@ void main() { }); test('driving when plugin does not suppport Windows is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', ]); final List output = await runCapturingPrint(runner, [ @@ -454,13 +488,17 @@ void main() { }); test('driving on a Windows plugin', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], platformSupport: { - kPlatformWindows: PlatformSupport.inline - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformWindows: PlatformSupport.inline + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -500,12 +538,17 @@ void main() { }); test('driving when plugin does not support mobile is no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test_driver', 'plugin.dart'], - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }, + ); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -548,14 +591,18 @@ void main() { }); test('enable-experiment flag', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test_driver', 'plugin_test.dart'], - ['example', 'test', 'plugin.dart'], - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - }); + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test/plugin.dart', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + ); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index 8c39b8cf70e..32867c949b4 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -40,20 +40,13 @@ void main() { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(1); processRunner.processToReturn = mockProcess; - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'lib/test/should_not_run_e2e.dart', + 'example/test_driver/plugin_e2e.dart', + 'example/test_driver/plugin_e2e_test.dart', + 'example/android/gradlew', + 'example/should_not_run_e2e.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', ]); await expectLater( () => runCapturingPrint(runner, ['firebase-test-lab']), @@ -65,26 +58,19 @@ void main() { }); test('runs e2e tests', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['test', 'plugin_test.dart'], - ['test', 'plugin_e2e.dart'], - ['should_not_run_e2e.dart'], - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'should_not_run.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'test/plugin_e2e.dart', + 'should_not_run_e2e.dart', + 'lib/test/should_not_run_e2e.dart', + 'example/test/plugin_e2e.dart', + 'example/test_driver/plugin_e2e.dart', + 'example/test_driver/plugin_e2e_test.dart', + 'example/integration_test/foo_test.dart', + 'example/integration_test/should_not_run.dart', + 'example/android/gradlew', + 'example/should_not_run_e2e.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', ]); await runCapturingPrint(runner, [ @@ -168,26 +154,19 @@ void main() { }); test('experimental flag', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['test', 'plugin_test.dart'], - ['test', 'plugin_e2e.dart'], - ['should_not_run_e2e.dart'], - ['lib/test/should_not_run_e2e.dart'], - ['example', 'test', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e.dart'], - ['example', 'test_driver', 'plugin_e2e_test.dart'], - ['example', 'integration_test', 'foo_test.dart'], - ['example', 'integration_test', 'should_not_run.dart'], - ['example', 'android', 'gradlew'], - ['example', 'should_not_run_e2e.dart'], - [ - 'example', - 'android', - 'app', - 'src', - 'androidTest', - 'MainActivityTest.java' - ], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'test/plugin_e2e.dart', + 'should_not_run_e2e.dart', + 'lib/test/should_not_run_e2e.dart', + 'example/test/plugin_e2e.dart', + 'example/test_driver/plugin_e2e.dart', + 'example/test_driver/plugin_e2e_test.dart', + 'example/integration_test/foo_test.dart', + 'example/integration_test/should_not_run.dart', + 'example/android/gradlew', + 'example/should_not_run_e2e.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', ]); await runCapturingPrint(runner, [ diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index 3c6319ed447..fc80961462c 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.dart @@ -39,9 +39,9 @@ void main() { platformSupport: { kPlatformAndroid: PlatformSupport.inline }, - extraFiles: >[ - ['example/android', 'gradlew'], - ['android/src/test', 'example_test.java'], + extraFiles: [ + 'example/android/gradlew', + 'android/src/test/example_test.java', ], ); @@ -66,9 +66,9 @@ void main() { platformSupport: { kPlatformAndroid: PlatformSupport.inline }, - extraFiles: >[ - ['example/android', 'gradlew'], - ['example/android/app/src/test', 'example_test.java'], + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', ], ); diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 45dc92e6994..d86c9145fc1 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -45,9 +45,8 @@ void main() { }); test('only runs on macOS', () async { - createFakePlugin('plugin1', packagesDir, extraFiles: >[ - ['plugin1.podspec'], - ]); + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); mockPlatform.isMacOS = false; await runner.run(['podspecs']); @@ -59,11 +58,14 @@ void main() { }); test('runs pod lib lint on a podspec', () async { - final Directory plugin1Dir = - createFakePlugin('plugin1', packagesDir, extraFiles: >[ - ['ios', 'plugin1.podspec'], - ['bogus.dart'], // Ignore non-podspecs. - ]); + final Directory plugin1Dir = createFakePlugin( + 'plugin1', + packagesDir, + extraFiles: [ + 'ios/plugin1.podspec', + 'bogus.dart', // Ignore non-podspecs. + ], + ); processRunner.resultStdout = 'Foo'; processRunner.resultStderr = 'Bar'; @@ -106,12 +108,10 @@ void main() { }); test('skips podspecs with known issues', () async { - createFakePlugin('plugin1', packagesDir, extraFiles: >[ - ['plugin1.podspec'] - ]); - createFakePlugin('plugin2', packagesDir, extraFiles: >[ - ['plugin2.podspec'] - ]); + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + createFakePlugin('plugin2', packagesDir, + extraFiles: ['plugin2.podspec']); await runner .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); @@ -125,10 +125,8 @@ void main() { }); test('allow warnings for podspecs with known warnings', () async { - final Directory plugin1Dir = - createFakePlugin('plugin1', packagesDir, extraFiles: >[ - ['plugin1.podspec'], - ]); + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); await runner.run(['podspecs', '--ignore-warnings=plugin1']); diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index b2f663f1e90..fdccae3d552 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -31,14 +31,10 @@ void main() { }); test('runs flutter test on each plugin', () async { - final Directory plugin1Dir = - createFakePlugin('plugin1', packagesDir, extraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory plugin2Dir = - createFakePlugin('plugin2', packagesDir, extraFiles: >[ - ['test', 'empty_test.dart'], - ]); + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + extraFiles: ['test/empty_test.dart']); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + extraFiles: ['test/empty_test.dart']); await runner.run(['test']); @@ -55,10 +51,8 @@ void main() { test('skips testing plugins without test directory', () async { createFakePlugin('plugin1', packagesDir); - final Directory plugin2Dir = - createFakePlugin('plugin2', packagesDir, extraFiles: >[ - ['test', 'empty_test.dart'], - ]); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + extraFiles: ['test/empty_test.dart']); await runner.run(['test']); @@ -72,14 +66,10 @@ void main() { }); test('runs pub run test on non-Flutter packages', () async { - final Directory pluginDir = - createFakePlugin('a', packagesDir, extraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory packageDir = - createFakePackage('b', packagesDir, extraFiles: >[ - ['test', 'empty_test.dart'], - ]); + final Directory pluginDir = createFakePlugin('a', packagesDir, + extraFiles: ['test/empty_test.dart']); + final Directory packageDir = createFakePackage('b', packagesDir, + extraFiles: ['test/empty_test.dart']); await runner.run(['test', '--enable-experiment=exp1']); @@ -103,9 +93,7 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - extraFiles: >[ - ['test', 'empty_test.dart'], - ], + extraFiles: ['test/empty_test.dart'], platformSupport: { kPlatformWeb: PlatformSupport.inline, }, @@ -125,14 +113,10 @@ void main() { }); test('enable-experiment flag', () async { - final Directory pluginDir = - createFakePlugin('a', packagesDir, extraFiles: >[ - ['test', 'empty_test.dart'], - ]); - final Directory packageDir = - createFakePackage('b', packagesDir, extraFiles: >[ - ['test', 'empty_test.dart'], - ]); + final Directory pluginDir = createFakePlugin('a', packagesDir, + extraFiles: ['test/empty_test.dart']); + final Directory packageDir = createFakePackage('b', packagesDir, + extraFiles: ['test/empty_test.dart']); await runner.run(['test', '--enable-experiment=exp1']); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 4ced4eb4837..e71a26fa4eb 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -13,6 +13,7 @@ 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/common/process_runner.dart'; import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; import 'package:quiver/collection.dart'; /// Creates a packages directory in the given location. @@ -36,11 +37,14 @@ Directory createPackagesDirectory( /// /// [platformSupport] is a map of platform string to the support details for /// that platform. +/// +/// [extraFiles] is an optional list of plugin-relative paths, using Posix +/// separators, of extra files to create in the plugin. Directory createFakePlugin( String name, Directory parentDirectory, { List examples = const ['example'], - List> extraFiles = const >[], + List extraFiles = const [], Map platformSupport = const {}, String? version = '0.0.1', @@ -60,22 +64,18 @@ Directory createFakePlugin( version: version, ); - final FileSystem fileSystem = pluginDirectory.fileSystem; - for (final List file in extraFiles) { - final List newFilePath = [pluginDirectory.path, ...file]; - final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath)); - newFile.createSync(recursive: true); - } - return pluginDirectory; } /// Creates a plugin package with the given [name] in [packagesDirectory]. +/// +/// [extraFiles] is an optional list of package-relative paths, using unix-style +/// separators, of extra files to create in the package. Directory createFakePackage( String name, Directory parentDirectory, { List examples = const ['example'], - List> extraFiles = const >[], + List extraFiles = const [], bool isFlutter = false, String? version = '0.0.1', }) { @@ -105,8 +105,12 @@ Directory createFakePackage( } final FileSystem fileSystem = packageDirectory.fileSystem; - for (final List file in extraFiles) { - final List newFilePath = [packageDirectory.path, ...file]; + final p.Context posixContext = p.posix; + for (final String file in extraFiles) { + final List newFilePath = [ + packageDirectory.path, + ...posixContext.split(file) + ]; final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath)); newFile.createSync(recursive: true); } diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 050a4d4da73..08af85b39e4 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -113,8 +113,8 @@ void main() { group('iOS', () { test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ], platformSupport: { kPlatformMacos: PlatformSupport.inline, }); @@ -130,8 +130,8 @@ void main() { }); test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ], platformSupport: { kPlatformIos: PlatformSupport.federated }); @@ -147,14 +147,14 @@ void main() { }); test('running with correct destination, exclude 1 plugin', () async { - createFakePlugin('plugin1', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/test', ], platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginDirectory2 = - createFakePlugin('plugin2', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin2', packagesDir, extraFiles: [ + 'example/test', ], platformSupport: { kPlatformIos: PlatformSupport.inline }); @@ -207,8 +207,8 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ], platformSupport: { kPlatformIos: PlatformSupport.inline }); @@ -261,8 +261,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - extraFiles: >[ - ['example', 'test'], + extraFiles: [ + 'example/test', ], ); @@ -277,8 +277,8 @@ void main() { }); test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ], platformSupport: { kPlatformMacos: PlatformSupport.federated, }); @@ -295,8 +295,8 @@ void main() { test('runs for macOS plugin', () async { final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: >[ - ['example', 'test'], + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', ], platformSupport: { kPlatformMacos: PlatformSupport.inline, }); From f0967e5186bec84c3de68c5d340bc590aa916934 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 17 Jun 2021 18:23:51 -0700 Subject: [PATCH 072/249] Update tool commands (#4065) Update CI and README to call the tool via bin/flutter_plugin_tools.dart rather than lib/src/main.dart to avoid a warning line on every run. --- script/tool/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/script/tool/README.md b/script/tool/README.md index c0bcd7c5e10..c0ee8756e16 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -26,7 +26,7 @@ cd ./script/tool && dart pub get && cd ../../ Run: ```sh -dart run ./script/tool/lib/src/main.dart +dart run ./script/tool/bin/flutter_plugin_tools.dart ``` ### Published Version @@ -58,21 +58,21 @@ Note that the `plugins` argument, despite the name, applies to any package. ```sh cd -dart run ./script/tool/lib/src/main.dart format --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart format --plugins plugin_name ``` ### Run the Dart Static Analyzer ```sh cd -dart run ./script/tool/lib/src/main.dart analyze --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart analyze --plugins plugin_name ``` ### Run Dart Unit Tests ```sh cd -dart run ./script/tool/lib/src/main.dart test --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart test --plugins plugin_name ``` ### Run XCTests @@ -80,9 +80,9 @@ dart run ./script/tool/lib/src/main.dart test --plugins plugin_name ```sh cd # For iOS: -dart run ./script/tool/lib/src/main.dart xctest --ios --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --ios --plugins plugin_name # For macOS: -dart run ./script/tool/lib/src/main.dart xctest --macos --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --macos --plugins plugin_name ``` ### Publish a Release @@ -90,11 +90,12 @@ dart run ./script/tool/lib/src/main.dart xctest --macos --plugins plugin_name ``sh cd git checkout -dart run ./script/tool/lib/src/main.dart publish-plugin --package +dart run ./script/tool/bin/flutter_plugin_tools.dart publish-plugin --package `` By default the tool tries to push tags to the `upstream` remote, but some -additional settings can be configured. Run `dart run ./script/tool/lib/src/main.dart publish-plugin --help` for more usage information. +additional settings can be configured. Run `dart run ./script/tool/bin/flutter_plugin_tools.dart +publish-plugin --help` for more usage information. The tool wraps `pub publish` for pushing the package to pub, and then will automatically use git to try to create and push tags. It has some additional From 356d316717baedacc3fe836c044673b29653d783 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 22 Jun 2021 13:32:03 -0700 Subject: [PATCH 073/249] [flutter_plugin_tools] Add a new base command for looping over packages (#4067) Most of our commands are generally of the form: ``` for (each plugin as defined by the tool flags) check some things for success or failure print a summary all of the failing things exit non-zero if anything failed ``` Currently all that logic not consistent, having been at various points copied and pasted around, modified, in some cases rewritten. There's unnecessary boilerplate in each new command, and there's unnecessary variation that makes it harder both to maintain the tool, and to consume the test output: - There's no standard format for separating each plugin's run to search within a log - There's no standard format for the summary at the end - In some cases commands have been written to ToolExit on failure, which means we don't actually get the rest of the runs This makes a new base class for commands that follow this structure to use, with shared code for all the common bits. This makes it harder to accidentally write new commands incorrectly, easier to maintain the code, and lets us standardize output so that searching within large logs will be easier. This ports two commands over as a proof of concept to demonstrate that it works; more will be converted in follow-ups. Related to https://github.com/flutter/flutter/issues/83413 --- script/tool/CHANGELOG.md | 1 + script/tool/lib/src/common/core.dart | 22 +- .../src/common/package_looping_command.dart | 161 +++++++ .../tool/lib/src/common/plugin_command.dart | 3 + .../tool/lib/src/pubspec_check_command.dart | 39 +- script/tool/lib/src/xctest_command.dart | 89 ++-- .../common/package_looping_command_test.dart | 433 ++++++++++++++++++ .../tool/test/pubspec_check_command_test.dart | 22 +- script/tool/test/xctest_command_test.dart | 36 +- 9 files changed, 710 insertions(+), 96 deletions(-) create mode 100644 script/tool/lib/src/common/package_looping_command.dart create mode 100644 script/tool/test/common/package_looping_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index ae81ced6366..f43d2fcc9bd 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -6,6 +6,7 @@ - `xctest` now supports running macOS tests in addition to iOS - **Breaking change**: it now requires an `--ios` and/or `--macos` flag. - The tooling now runs in strong null-safe mode. +- Modified the output format of `pubspec-check` and `xctest` ## 0.2.0 diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 1da6ef7a4c8..b2be8f56d17 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -53,13 +53,25 @@ bool isFlutterPackage(FileSystemEntity entity) { } } +/// Prints `successMessage` in green. +void printSuccess(String successMessage) { + print(Colorize(successMessage)..green()); +} + /// Prints `errorMessage` in red. void printError(String errorMessage) { - final Colorize redError = Colorize(errorMessage)..red(); - print(redError); + print(Colorize(errorMessage)..red()); } /// Error thrown when a command needs to exit with a non-zero exit code. +/// +/// While there is no specific definition of the meaning of different non-zero +/// exit codes for this tool, commands should follow the general convention: +/// 1: The command ran correctly, but found errors. +/// 2: The command failed to run because the arguments were invalid. +/// >2: The command failed to run correctly for some other reason. Ideally, +/// each such failure should have a unique exit code within the context of +/// that command. class ToolExit extends Error { /// Creates a tool exit with the given [exitCode]. ToolExit(this.exitCode); @@ -67,3 +79,9 @@ class ToolExit extends Error { /// The code that the process should exit with. final int exitCode; } + +/// A exit code for [ToolExit] for a successful run that found errors. +const int exitCommandFoundErrors = 1; + +/// A exit code for [ToolExit] for a failure to run due to invalid arguments. +const int exitInvalidArguments = 2; diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart new file mode 100644 index 00000000000..1349a5ed5dc --- /dev/null +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -0,0 +1,161 @@ +// 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:git/git.dart'; +import 'package:path/path.dart' as p; + +import 'core.dart'; +import 'plugin_command.dart'; +import 'process_runner.dart'; + +/// An abstract base class for a command that iterates over a set of packages +/// controlled by a standard set of flags, running some actions on each package, +/// and collecting and reporting the success/failure of those actions. +abstract class PackageLoopingCommand extends PluginCommand { + /// Creates a command to operate on [packagesDir] with the given environment. + PackageLoopingCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + GitDir? gitDir, + }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); + + /// Called during [run] before any calls to [runForPackage]. This provides an + /// opportunity to fail early if the command can't be run (e.g., because the + /// arguments are invalid), and to set up any run-level state. + Future initializeRun() async {} + + /// Runs the command for [package], returning a list of errors. + /// + /// Errors may either be an empty string if there is no context that should + /// be included in the final error summary (e.g., a command that only has a + /// single failure mode), or strings that should be listed for that package + /// in the final summary. An empty list indicates success. + Future> runForPackage(Directory package); + + /// Whether or not the output (if any) of [runForPackage] is long, or short. + /// + /// This changes the logging that happens at the start of each package's + /// run; long output gets a banner-style message to make it easier to find, + /// while short output gets a single-line entry. + /// + /// When this is false, runForPackage output should be indented if possible, + /// to make the output structure easier to follow. + bool get hasLongOutput => true; + + /// Whether to loop over all packages (e.g., including example/), rather than + /// only top-level packages. + bool get includeSubpackages => false; + + /// The text to output at the start when reporting one or more failures. + /// This will be followed by a list of packages that reported errors, with + /// the per-package details if any. + /// + /// This only needs to be overridden if the summary should provide extra + /// context. + String get failureListHeader => 'The following packages had errors:'; + + /// The text to output at the end when reporting one or more failures. This + /// will be printed immediately after the a list of packages that reported + /// errors. + /// + /// This only needs to be overridden if the summary should provide extra + /// context. + String get failureListFooter => 'See above for full details.'; + + // ---------------------------------------- + + /// A convenience constant for [runForPackage] success that's more + /// self-documenting than the value. + static const List success = []; + + /// A convenience constant for [runForPackage] failure without additional + /// context that's more self-documenting than the value. + static const List failure = ['']; + + /// Prints a message using a standard format indicating that the package was + /// skipped, with an explanation of why. + void printSkip(String reason) { + print(Colorize('SKIPPING: $reason')..darkGray()); + } + + /// Returns the identifying name to use for [package]. + /// + /// Implementations should not expect a specific format for this string, since + /// it uses heuristics to try to be precise without being overly verbose. If + /// an exact format (e.g., published name, or basename) is required, that + /// should be used instead. + String getPackageDescription(Directory package) { + String packageName = p.relative(package.path, from: packagesDir.path); + final List components = p.split(packageName); + // For the common federated plugin pattern of `foo/foo_subpackage`, drop + // the first part since it's not useful. + if (components.length == 2 && + components[1].startsWith('${components[0]}_')) { + packageName = components[1]; + } + return packageName; + } + + // ---------------------------------------- + + @override + Future run() async { + await initializeRun(); + + final List packages = includeSubpackages + ? await getPackages().toList() + : await getPlugins().toList(); + + final Map> results = >{}; + for (final Directory package in packages) { + _printPackageHeading(package); + results[package] = await runForPackage(package); + } + + // If there were any errors reported, summarize them and exit. + if (results.values.any((List failures) => failures.isNotEmpty)) { + const String indentation = ' '; + printError(failureListHeader); + for (final Directory package in packages) { + final List errors = results[package]!; + if (errors.isNotEmpty) { + final String errorIndentation = indentation * 2; + String errorDetails = errors.join('\n$errorIndentation'); + if (errorDetails.isNotEmpty) { + errorDetails = ':\n$errorIndentation$errorDetails'; + } + printError( + '$indentation${getPackageDescription(package)}$errorDetails'); + } + } + printError(failureListFooter); + throw ToolExit(exitCommandFoundErrors); + } + + printSuccess('\n\nNo issues found!'); + } + + /// Prints the status message indicating that the command is being run for + /// [package]. + /// + /// Something is always printed to make it easier to distinguish between + /// a command running for a package and producing no output, and a command + /// not having been run for a package. + void _printPackageHeading(Directory package) { + String heading = 'Running for ${getPackageDescription(package)}'; + if (hasLongOutput) { + heading = ''' + +============================================================ +|| $heading +============================================================ +'''; + } else { + heading = '$heading...'; + } + print(Colorize(heading)..cyan()); + } +} diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 1ab9d8dcc6e..4c095858e45 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -14,6 +14,7 @@ import 'git_version_finder.dart'; import 'process_runner.dart'; /// Interface definition for all commands in this tool. +// TODO(stuartmorgan): Move most of this logic to PackageLoopingCommand. abstract class PluginCommand extends Command { /// Creates a command to operate on [packagesDir] with the given environment. PluginCommand( @@ -136,6 +137,8 @@ abstract class PluginCommand extends Command { /// Returns the root Dart package folders of the plugins involved in this /// command execution. + // TODO(stuartmorgan): Rename/restructure this, _getAllPlugins, and + // getPackages, as the current naming is very confusing. Stream getPlugins() async* { // To avoid assuming consistency of `Directory.list` across command // invocations, we collect and sort the plugin folders before sharding. diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 480d3a4c119..44b6b061542 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -4,11 +4,9 @@ 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/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; /// A command to enforce pubspec conventions across the repository. @@ -16,7 +14,7 @@ import 'common/process_runner.dart'; /// This both ensures that repo best practices for which optional fields are /// used are followed, and that the structure is consistent to make edits /// across multiple pubspec files easier. -class PubspecCheckCommand extends PluginCommand { +class PubspecCheckCommand extends PackageLoopingCommand { /// Creates an instance of the version check command. PubspecCheckCommand( Directory packagesDir, { @@ -52,29 +50,20 @@ class PubspecCheckCommand extends PluginCommand { 'Checks that pubspecs follow repository conventions.'; @override - Future run() async { - final List failingPackages = []; - await for (final Directory package in getPackages()) { - final String relativePackagePath = - p.relative(package.path, from: packagesDir.path); - print('Checking $relativePackagePath...'); - final File pubspec = package.childFile('pubspec.yaml'); - final bool passesCheck = !pubspec.existsSync() || - await _checkPubspec(pubspec, packageName: package.basename); - if (!passesCheck) { - failingPackages.add(relativePackagePath); - } - } + bool get hasLongOutput => false; - if (failingPackages.isNotEmpty) { - print('The following packages have pubspec issues:'); - for (final String package in failingPackages) { - print(' $package'); - } - throw ToolExit(1); - } + @override + bool get includeSubpackages => true; - print('\nNo pubspec issues found!'); + @override + Future> runForPackage(Directory package) async { + final File pubspec = package.childFile('pubspec.yaml'); + final bool passesCheck = !pubspec.existsSync() || + await _checkPubspec(pubspec, packageName: package.basename); + if (!passesCheck) { + return PackageLoopingCommand.failure; + } + return PackageLoopingCommand.success; } Future _checkPubspec( diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 741aa9d7283..3f50feef6fe 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -9,7 +9,7 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; @@ -19,12 +19,15 @@ const String _kXCRunCommand = 'xcrun'; const String _kFoundNoSimulatorsMessage = 'Cannot find any available simulators, tests failed'; +const int _exitFindingSimulatorsFailed = 3; +const int _exitNoSimulators = 4; + /// The command to run XCTests (XCUnitTest and XCUITest) in plugins. /// The tests target have to be added to the Xcode project of the example app, /// usually at "example/{ios,macos}/Runner.xcworkspace". /// /// The static analyzer is also run. -class XCTestCommand extends PluginCommand { +class XCTestCommand extends PackageLoopingCommand { /// Creates an instance of the test command. XCTestCommand( Directory packagesDir, { @@ -41,6 +44,9 @@ class XCTestCommand extends PluginCommand { argParser.addFlag(kPlatformMacos, help: 'Runs the macOS tests'); } + // The device destination flags for iOS tests. + List _iosDestinationFlags = []; + @override final String name = 'xctest'; @@ -50,62 +56,53 @@ class XCTestCommand extends PluginCommand { 'This command requires "flutter" and "xcrun" to be in your path.'; @override - Future run() async { - final bool testIos = getBoolArg(kPlatformIos); - final bool testMacos = getBoolArg(kPlatformMacos); + String get failureListHeader => 'The following packages are failing XCTests:'; - if (!(testIos || testMacos)) { - print('At least one platform flag must be provided.'); - throw ToolExit(2); + @override + Future initializeRun() async { + final bool shouldTestIos = getBoolArg(kPlatformIos); + final bool shouldTestMacos = getBoolArg(kPlatformMacos); + + if (!(shouldTestIos || shouldTestMacos)) { + printError('At least one platform flag must be provided.'); + throw ToolExit(exitInvalidArguments); } - List iosDestinationFlags = []; - if (testIos) { + if (shouldTestIos) { String destination = getStringArg(_kiOSDestination); if (destination.isEmpty) { final String? simulatorId = await _findAvailableIphoneSimulator(); if (simulatorId == null) { - print(_kFoundNoSimulatorsMessage); - throw ToolExit(1); + printError(_kFoundNoSimulatorsMessage); + throw ToolExit(_exitNoSimulators); } destination = 'id=$simulatorId'; } - iosDestinationFlags = [ + _iosDestinationFlags = [ '-destination', destination, ]; } + } - final List failingPackages = []; - await for (final Directory plugin in getPlugins()) { - final String packageName = - p.relative(plugin.path, from: packagesDir.path); - print('============================================================'); - print('Start running for $packageName...'); - bool passed = true; - if (testIos) { - passed &= await _testPlugin(plugin, 'iOS', - extraXcrunFlags: iosDestinationFlags); - } - if (testMacos) { - passed &= await _testPlugin(plugin, 'macOS'); - } - if (!passed) { - failingPackages.add(packageName); - } - } + @override + Future> runForPackage(Directory package) async { + final List failures = []; + final bool testIos = getBoolArg(kPlatformIos); + final bool testMacos = getBoolArg(kPlatformMacos); + // Only provide the failing platform(s) in the summary if testing multiple + // platforms, otherwise it's just noise. + final bool provideErrorDetails = testIos && testMacos; - // Command end, print reports. - if (failingPackages.isEmpty) { - print('All XCTests have passed!'); - } else { - print( - 'The following packages are failing XCTests (see above for details):'); - for (final String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); + if (testIos && + !await _testPlugin(package, 'iOS', + extraXcrunFlags: _iosDestinationFlags)) { + failures.add(provideErrorDetails ? 'iOS' : ''); + } + if (testMacos && !await _testPlugin(package, 'macOS')) { + failures.add(provideErrorDetails ? 'macOS' : ''); } + return failures; } /// Runs all applicable tests for [plugin], printing status and returning @@ -117,8 +114,7 @@ class XCTestCommand extends PluginCommand { }) async { if (!pluginSupportsPlatform(platform.toLowerCase(), plugin, requiredMode: PlatformSupport.inline)) { - print('$platform is not implemented by this plugin package.'); - print('\n'); + printSkip('$platform is not implemented by this plugin package.\n'); return true; } bool passing = true; @@ -136,7 +132,7 @@ class XCTestCommand extends PluginCommand { extraFlags: extraXcrunFlags); } if (exitCode == 0) { - print('Successfully ran $platform xctest for $examplePath'); + printSuccess('Successfully ran $platform xctest for $examplePath'); } else { passing = false; } @@ -184,9 +180,10 @@ class XCTestCommand extends PluginCommand { final io.ProcessResult findSimulatorsResult = await processRunner.run(_kXCRunCommand, findSimulatorsArguments); if (findSimulatorsResult.exitCode != 0) { - print('Error occurred while running "$findSimulatorCompleteCommand":\n' + printError( + 'Error occurred while running "$findSimulatorCompleteCommand":\n' '${findSimulatorsResult.stderr}'); - throw ToolExit(1); + throw ToolExit(_exitFindingSimulatorsFailed); } final Map simulatorListJson = jsonDecode(findSimulatorsResult.stdout as String) diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart new file mode 100644 index 00000000000..1012f764a62 --- /dev/null +++ b/script/tool/test/common/package_looping_command_test.dart @@ -0,0 +1,433 @@ +// 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:io'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; +import 'plugin_command_test.mocks.dart'; + +// Constants for colorized output start and end. +const String _startHeadingColor = '\x1B[36m'; +const String _startSkipColor = '\x1B[90m'; +const String _startSuccessColor = '\x1B[32m'; +const String _startErrorColor = '\x1B[31m'; +const String _endColor = '\x1B[0m'; + +// The filename within a package containing errors to return from runForPackage. +const String _errorFile = 'errors'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + late Directory thirdPartyPackagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + thirdPartyPackagesDir = packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages'); + }); + + /// Creates a TestPackageLoopingCommand instance that uses [gitDiffResponse] + /// for git diffs, and logs output to [printOutput]. + TestPackageLoopingCommand createTestCommand({ + String gitDiffResponse = '', + bool hasLongOutput = true, + bool includeSubpackages = false, + bool failsDuringInit = false, + String? customFailureListHeader, + String? customFailureListFooter, + }) { + // Set up the git diff response. + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout as String?) + .thenReturn(gitDiffResponse); + } + return Future.value(mockProcessResult); + }); + + return TestPackageLoopingCommand( + packagesDir, + hasLongOutput: hasLongOutput, + includeSubpackages: includeSubpackages, + failsDuringInit: failsDuringInit, + customFailureListHeader: customFailureListHeader, + customFailureListFooter: customFailureListFooter, + gitDir: gitDir, + ); + } + + /// Runs [command] with the given [arguments], and returns its output. + Future> runCommand( + TestPackageLoopingCommand command, { + List arguments = const [], + void Function(Error error)? errorHandler, + }) async { + late CommandRunner runner; + runner = CommandRunner('test_package_looping_command', + 'Test for base package looping functionality'); + runner.addCommand(command); + return await runCapturingPrint( + runner, + [command.name, ...arguments], + errorHandler: errorHandler, + ); + } + + group('tool exit', () { + test('is handled during initializeRun', () async { + final TestPackageLoopingCommand command = + createTestCommand(failsDuringInit: true); + + expect(() => runCommand(command), throwsA(isA())); + }); + + test('does not stop looping', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + failingPackage.childFile(_errorFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + '${_startHeadingColor}Running for package_c...$_endColor', + ])); + }); + }); + + group('package iteration', () { + test('includes plugins and packages', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + final Directory package = createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = createTestCommand(); + await runCommand(command); + + expect(command.checkedPackages, + unorderedEquals([plugin.path, package.path])); + }); + + test('includes third_party/packages', () async { + final Directory package1 = createFakePackage('a_package', packagesDir); + final Directory package2 = + createFakePackage('another_package', thirdPartyPackagesDir); + + final TestPackageLoopingCommand command = createTestCommand(); + await runCommand(command); + + expect(command.checkedPackages, + unorderedEquals([package1.path, package2.path])); + }); + + test('includes subpackages when requested', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + final Directory package = createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(includeSubpackages: true); + await runCommand(command); + + expect( + command.checkedPackages, + unorderedEquals([ + plugin.path, + plugin.childDirectory('example').childDirectory('example1').path, + plugin.childDirectory('example').childDirectory('example2').path, + package.path, + package.childDirectory('example').path, + ])); + }); + }); + + group('output', () { + test('has the expected package headers for long-form output', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = await runCommand(command); + + const String separator = + '============================================================'; + expect( + output, + containsAllInOrder([ + '$_startHeadingColor\n$separator\n|| Running for package_a\n$separator\n$_endColor', + '$_startHeadingColor\n$separator\n|| Running for package_b\n$separator\n$_endColor', + ])); + }); + + test('has the expected package headers for short-form output', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + ])); + }); + + test('shows the success message when nothing fails', () async { + createFakePackage('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '$_startSuccessColor\n\nNo issues found!$_endColor', + ])); + }); + + test('shows failure summaries when something fails without extra details', + () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage1 = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + final Directory failingPackage2 = + createFakePlugin('package_d', packagesDir); + failingPackage1.childFile(_errorFile).createSync(); + failingPackage2.childFile(_errorFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startErrorColor}The following packages had errors:$_endColor', + '$_startErrorColor package_b$_endColor', + '$_startErrorColor package_d$_endColor', + '${_startErrorColor}See above for full details.$_endColor', + ])); + }); + + test('uses custom summary header and footer if provided', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage1 = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + final Directory failingPackage2 = + createFakePlugin('package_d', packagesDir); + failingPackage1.childFile(_errorFile).createSync(); + failingPackage2.childFile(_errorFile).createSync(); + + final TestPackageLoopingCommand command = createTestCommand( + hasLongOutput: false, + customFailureListHeader: 'This is a custom header', + customFailureListFooter: 'And a custom footer!'); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startErrorColor}This is a custom header$_endColor', + '$_startErrorColor package_b$_endColor', + '$_startErrorColor package_d$_endColor', + '${_startErrorColor}And a custom footer!$_endColor', + ])); + }); + + test('shows failure summaries when something fails with extra details', + () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage1 = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + final Directory failingPackage2 = + createFakePlugin('package_d', packagesDir); + final File errorFile1 = failingPackage1.childFile(_errorFile); + errorFile1.createSync(); + errorFile1.writeAsStringSync('just one detail'); + final File errorFile2 = failingPackage2.childFile(_errorFile); + errorFile2.createSync(); + errorFile2.writeAsStringSync('first detail\nsecond detail'); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startErrorColor}The following packages had errors:$_endColor', + '$_startErrorColor package_b:\n just one detail$_endColor', + '$_startErrorColor package_d:\n first detail\n second detail$_endColor', + '${_startErrorColor}See above for full details.$_endColor', + ])); + }); + }); + + group('utility', () { + test('printSkip has expected output', () async { + final TestPackageLoopingCommand command = + TestPackageLoopingCommand(packagesDir); + + final List printBuffer = []; + Zone.current.fork(specification: ZoneSpecification( + print: (_, __, ___, String message) { + printBuffer.add(message); + }, + )).run(() => command.printSkip('For a reason')); + + expect(printBuffer.first, + '${_startSkipColor}SKIPPING: For a reason$_endColor'); + }); + + test('getPackageDescription prints packageDir-relative paths by default', + () async { + final TestPackageLoopingCommand command = + TestPackageLoopingCommand(packagesDir); + + expect( + command.getPackageDescription(packagesDir.childDirectory('foo')), + 'foo', + ); + expect( + command.getPackageDescription(packagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')), + 'foo/bar/baz', + ); + }); + + test( + 'getPackageDescription elides group name in grouped federated plugin structure', + () async { + final TestPackageLoopingCommand command = + TestPackageLoopingCommand(packagesDir); + + expect( + command.getPackageDescription(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_platform_interface')), + 'a_plugin_platform_interface', + ); + expect( + command.getPackageDescription(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_web')), + 'a_plugin_web', + ); + }); + }); +} + +class TestPackageLoopingCommand extends PackageLoopingCommand { + TestPackageLoopingCommand( + Directory packagesDir, { + this.hasLongOutput = true, + this.includeSubpackages = false, + this.customFailureListHeader, + this.customFailureListFooter, + this.failsDuringInit = false, + ProcessRunner processRunner = const ProcessRunner(), + GitDir? gitDir, + }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); + + final List checkedPackages = []; + + final String? customFailureListHeader; + final String? customFailureListFooter; + + final bool failsDuringInit; + + @override + bool hasLongOutput; + + @override + bool includeSubpackages; + + @override + String get failureListHeader => + customFailureListHeader ?? super.failureListHeader; + + @override + String get failureListFooter => + customFailureListFooter ?? super.failureListFooter; + + @override + final String name = 'loop-test'; + + @override + final String description = 'sample package looping command'; + + @override + Future initializeRun() async { + if (failsDuringInit) { + throw ToolExit(2); + } + } + + @override + Future> runForPackage(Directory package) async { + checkedPackages.add(package.path); + final File errorFile = package.childFile(_errorFile); + if (errorFile.existsSync()) { + final List errors = errorFile.readAsLinesSync(); + return errors.isNotEmpty ? errors : PackageLoopingCommand.failure; + } + return PackageLoopingCommand.success; + } +} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index af27ac5bd2f..38182a4d183 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -104,10 +104,10 @@ ${devDependenciesSection()} expect( output, - containsAllInOrder([ - 'Checking plugin...', - 'Checking plugin/example...', - '\nNo pubspec issues found!', + containsAllInOrder([ + contains('Running for plugin...'), + contains('Running for plugin/example...'), + contains('No issues found!'), ]), ); }); @@ -129,10 +129,10 @@ ${flutterSection()} expect( output, - containsAllInOrder([ - 'Checking plugin...', - 'Checking plugin/example...', - '\nNo pubspec issues found!', + containsAllInOrder([ + contains('Running for plugin...'), + contains('Running for plugin/example...'), + contains('No issues found!'), ]), ); }); @@ -153,9 +153,9 @@ ${dependenciesSection()} expect( output, - containsAllInOrder([ - 'Checking package...', - '\nNo pubspec issues found!', + containsAllInOrder([ + contains('Running for package...'), + contains('No issues found!'), ]), ); }); diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 08af85b39e4..b12ad852cda 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -125,7 +125,9 @@ void main() { final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); expect( - output, contains('iOS is not implemented by this plugin package.')); + output, + contains( + contains('iOS is not implemented by this plugin package.'))); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -142,7 +144,9 @@ void main() { final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); expect( - output, contains('iOS is not implemented by this plugin package.')); + output, + contains( + contains('iOS is not implemented by this plugin package.'))); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -176,10 +180,12 @@ void main() { 'plugin1' ]); - expect(output, isNot(contains('Start running for plugin1...'))); - expect(output, contains('Start running for plugin2...')); - expect(output, - contains('Successfully ran iOS xctest for plugin2/example')); + expect(output, isNot(contains(contains('Running for plugin1')))); + expect(output, contains(contains('Running for plugin2'))); + expect( + output, + contains( + contains('Successfully ran iOS xctest for plugin2/example'))); expect( processRunner.recordedCalls, @@ -271,8 +277,10 @@ void main() { processRunner.processToReturn = mockProcess; final List output = await runCapturingPrint(runner, ['xctest', '--macos', _kDestination, 'foo_destination']); - expect(output, - contains('macOS is not implemented by this plugin package.')); + expect( + output, + contains( + contains('macOS is not implemented by this plugin package.'))); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -288,8 +296,10 @@ void main() { processRunner.processToReturn = mockProcess; final List output = await runCapturingPrint(runner, ['xctest', '--macos', _kDestination, 'foo_destination']); - expect(output, - contains('macOS is not implemented by this plugin package.')); + expect( + output, + contains( + contains('macOS is not implemented by this plugin package.'))); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -314,8 +324,10 @@ void main() { '--macos', ]); - expect(output, - contains('Successfully ran macOS xctest for plugin/example')); + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); expect( processRunner.recordedCalls, From 552fceef915bd725c694d56734bd8d2ac99a28d2 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 24 Jun 2021 12:46:24 -0700 Subject: [PATCH 074/249] [flutter_plugin_tools] `publish-plugin` check against pub to determine if a release should happen (#4068) This PR removes a TODO where we used to check if a release should happen against git tags, instead, we check against pub. Also, when auto-publish and the package is manually released, the CI will continue to fail without this change. Fixes flutter/flutter#81047 --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/publish_plugin_command.dart | 42 ++- .../test/publish_plugin_command_test.dart | 308 ++++++++++++++---- 3 files changed, 281 insertions(+), 70 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index f43d2fcc9bd..3e2ec3f4c99 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -6,6 +6,7 @@ - `xctest` now supports running macOS tests in addition to iOS - **Breaking change**: it now requires an `--ios` and/or `--macos` flag. - The tooling now runs in strong null-safe mode. +- `publish plugins` check against pub.dev to determine if a release should happen. - Modified the output format of `pubspec-check` and `xctest` ## 0.2.0 diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 70ec75bc7b7..622a1a3cb13 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -8,6 +8,7 @@ import 'dart:io' as io; import 'package:file/file.dart'; import 'package:git/git.dart'; +import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; @@ -18,6 +19,7 @@ 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'; @immutable class _RemoteInfo { @@ -49,7 +51,10 @@ class PublishPluginCommand extends PluginCommand { Print print = print, io.Stdin? stdinput, GitDir? gitDir, - }) : _print = print, + http.Client? httpClient, + }) : _pubVersionFinder = + PubVersionFinder(httpClient: httpClient ?? http.Client()), + _print = print, _stdin = stdinput ?? io.stdin, super(packagesDir, processRunner: processRunner, gitDir: gitDir) { argParser.addOption( @@ -131,6 +136,7 @@ class PublishPluginCommand extends PluginCommand { final Print _print; final io.Stdin _stdin; StreamSubscription? _stdinSubscription; + final PubVersionFinder _pubVersionFinder; @override Future run() async { @@ -182,6 +188,8 @@ class PublishPluginCommand extends PluginCommand { remoteForTagPush: remote, ); } + + _pubVersionFinder.httpClient.close(); await _finish(successful); } @@ -196,6 +204,7 @@ class PublishPluginCommand extends PluginCommand { _print('No version updates in this commit.'); return true; } + _print('Getting existing tags...'); final io.ProcessResult existingTagsResult = await baseGitDir.runCommand(['tag', '--sort=-committerdate']); @@ -212,7 +221,6 @@ class PublishPluginCommand extends PluginCommand { .childFile(pubspecPath); final _CheckNeedsReleaseResult result = await _checkNeedsRelease( pubspecFile: pubspecFile, - gitVersionFinder: gitVersionFinder, existingTags: existingTags, ); switch (result) { @@ -271,7 +279,6 @@ class PublishPluginCommand extends PluginCommand { // Returns a [_CheckNeedsReleaseResult] that indicates the result. Future<_CheckNeedsReleaseResult> _checkNeedsRelease({ required File pubspecFile, - required GitVersionFinder gitVersionFinder, required List existingTags, }) async { if (!pubspecFile.existsSync()) { @@ -293,19 +300,24 @@ Safe to ignore if the package is deleted in this commit. return _CheckNeedsReleaseResult.failure; } - // Get latest tagged version and compare with the current version. - // TODO(cyanglaz): Check latest version of the package on pub instead of git - // https://github.com/flutter/flutter/issues/81047 - - final String latestTag = existingTags.firstWhere( - (String tag) => tag.split('-v').first == pubspec.name, - orElse: () => ''); - if (latestTag.isNotEmpty) { - final String latestTaggedVersion = latestTag.split('-v').last; - final Version latestVersion = Version.parse(latestTaggedVersion); - if (pubspec.version! < latestVersion) { + // Check if the package named `packageName` with `version` has already published. + final Version version = pubspec.version!; + final PubVersionFinderResponse pubVersionFinderResponse = + await _pubVersionFinder.getPackageVersion(package: pubspec.name); + if (pubVersionFinderResponse.versions.contains(version)) { + final String tagsForPackageWithSameVersion = existingTags.firstWhere( + (String tag) => + tag.split('-v').first == pubspec.name && + tag.split('-v').last == version.toString(), + orElse: () => ''); + _print( + 'The version $version of ${pubspec.name} has already been published'); + if (tagsForPackageWithSameVersion.isEmpty) { _print( - 'The new version (${pubspec.version}) is lower than the current version ($latestVersion) for ${pubspec.name}.\nThis git commit is a revert, no release is tagged.'); + 'However, the git release tag for this version (${pubspec.name}-v$version) is not found. Please manually fix the tag then run the command again.'); + return _CheckNeedsReleaseResult.failure; + } else { + _print('skip.'); return _CheckNeedsReleaseResult.noRelease; } } diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index a2ea9816ea0..c7832e0da19 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -13,6 +13,8 @@ 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:http/http.dart' as http; +import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -427,6 +429,37 @@ void main() { }); test('can release newly created plugins', () async { + const Map httpResponsePlugin1 = { + 'name': 'plugin1', + 'versions': [], + }; + + const Map httpResponsePlugin2 = { + 'name': 'plugin2', + 'versions': [], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'plugin1.json') { + return http.Response(json.encode(httpResponsePlugin1), 200); + } else if (request.url.pathSegments.last == 'plugin2.json') { + return http.Response(json.encode(httpResponsePlugin2), 200); + } + return http.Response('', 500); + }); + final PublishPluginCommand command = PublishPluginCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString()), + stdinput: mockStdin, + httpClient: mockClient, + gitDir: gitDir); + + commandRunner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + commandRunner.addCommand(command); + // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated @@ -446,7 +479,6 @@ void main() { containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', - 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', @@ -463,10 +495,50 @@ void main() { test('can release newly created plugins, while there are existing plugins', () async { + const Map httpResponsePlugin0 = { + 'name': 'plugin0', + 'versions': ['0.0.1'], + }; + + const Map httpResponsePlugin1 = { + 'name': 'plugin1', + 'versions': [], + }; + + const Map httpResponsePlugin2 = { + 'name': 'plugin2', + 'versions': [], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'plugin0.json') { + return http.Response(json.encode(httpResponsePlugin0), 200); + } else if (request.url.pathSegments.last == 'plugin1.json') { + return http.Response(json.encode(httpResponsePlugin1), 200); + } else if (request.url.pathSegments.last == 'plugin2.json') { + return http.Response(json.encode(httpResponsePlugin2), 200); + } + return http.Response('', 500); + }); + final PublishPluginCommand command = PublishPluginCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString()), + stdinput: mockStdin, + httpClient: mockClient, + gitDir: gitDir); + + commandRunner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + commandRunner.addCommand(command); + // Prepare an exiting plugin and tag it createFakePlugin('plugin0', packagesDir); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); + await gitDir.runCommand(['tag', 'plugin0-v0.0.1']); + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -489,7 +561,6 @@ void main() { containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', - 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', @@ -505,6 +576,36 @@ void main() { }); test('can release newly created plugins, dry run', () async { + const Map httpResponsePlugin1 = { + 'name': 'plugin1', + 'versions': [], + }; + + const Map httpResponsePlugin2 = { + 'name': 'plugin2', + 'versions': [], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'plugin1.json') { + return http.Response(json.encode(httpResponsePlugin1), 200); + } else if (request.url.pathSegments.last == 'plugin2.json') { + return http.Response(json.encode(httpResponsePlugin2), 200); + } + return http.Response('', 500); + }); + final PublishPluginCommand command = PublishPluginCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString()), + stdinput: mockStdin, + httpClient: mockClient, + gitDir: gitDir); + + commandRunner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + commandRunner.addCommand(command); // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated @@ -527,7 +628,6 @@ void main() { 'Checking local repo...', 'Local repo is ready!', '=============== DRY RUN ===============', - 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Tagging release plugin1-v0.0.1...', 'Pushing tag to upstream...', @@ -541,6 +641,37 @@ void main() { }); test('version change triggers releases.', () async { + const Map httpResponsePlugin1 = { + 'name': 'plugin1', + 'versions': [], + }; + + const Map httpResponsePlugin2 = { + 'name': 'plugin2', + 'versions': [], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'plugin1.json') { + return http.Response(json.encode(httpResponsePlugin1), 200); + } else if (request.url.pathSegments.last == 'plugin2.json') { + return http.Response(json.encode(httpResponsePlugin2), 200); + } + return http.Response('', 500); + }); + final PublishPluginCommand command = PublishPluginCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString()), + stdinput: mockStdin, + httpClient: mockClient, + gitDir: gitDir); + + commandRunner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + commandRunner.addCommand(command); + // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated @@ -558,7 +689,6 @@ void main() { containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', - 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', @@ -600,7 +730,6 @@ void main() { containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', - 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', @@ -619,6 +748,37 @@ void main() { test( 'delete package will not trigger publish but exit the command successfully.', () async { + const Map httpResponsePlugin1 = { + 'name': 'plugin1', + 'versions': [], + }; + + const Map httpResponsePlugin2 = { + 'name': 'plugin2', + 'versions': [], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'plugin1.json') { + return http.Response(json.encode(httpResponsePlugin1), 200); + } else if (request.url.pathSegments.last == 'plugin2.json') { + return http.Response(json.encode(httpResponsePlugin2), 200); + } + return http.Response('', 500); + }); + final PublishPluginCommand command = PublishPluginCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString()), + stdinput: mockStdin, + httpClient: mockClient, + gitDir: gitDir); + + commandRunner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + commandRunner.addCommand(command); + // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated @@ -636,7 +796,6 @@ void main() { containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', - 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Packages released: plugin1, plugin2', @@ -677,7 +836,6 @@ void main() { containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', - 'Getting existing tags...', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'The file at The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n', 'Packages released: plugin1', @@ -691,18 +849,48 @@ void main() { expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); }); - test( - 'versions revert do not trigger releases. Also prints out warning message.', + test('Exiting versions do not trigger release, also prints out message.', () async { + const Map httpResponsePlugin1 = { + 'name': 'plugin1', + 'versions': ['0.0.2'], + }; + + const Map httpResponsePlugin2 = { + 'name': 'plugin2', + 'versions': ['0.0.2'], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'plugin1.json') { + return http.Response(json.encode(httpResponsePlugin1), 200); + } else if (request.url.pathSegments.last == 'plugin2.json') { + return http.Response(json.encode(httpResponsePlugin2), 200); + } + return http.Response('', 500); + }); + final PublishPluginCommand command = PublishPluginCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString()), + stdinput: mockStdin, + httpClient: mockClient, + gitDir: gitDir); + + commandRunner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + commandRunner.addCommand(command); + // Non-federated - final Directory pluginDir1 = - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = createFakePlugin( - 'plugin2', packagesDir.childDirectory('plugin2'), + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); + await gitDir.runCommand(['tag', 'plugin1-v0.0.2']); + await gitDir.runCommand(['tag', 'plugin2-v0.0.2']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -713,54 +901,64 @@ void main() { containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', - 'Getting existing tags...', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', + 'The version 0.0.2 of plugin1 has already been published', + 'skip.', + 'The version 0.0.2 of plugin2 has already been published', + 'skip.', 'Done!' ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); - processRunner.pushTagsArgs.clear(); - printedMessages.clear(); + expect(processRunner.pushTagsArgs, isEmpty); + }); - final List plugin1Pubspec = - pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); - plugin1Pubspec[plugin1Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.1'; - pluginDir1 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin1Pubspec.join('\n')); - final List plugin2Pubspec = - pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); - plugin2Pubspec[plugin2Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.1'; - pluginDir2 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin2Pubspec.join('\n')); - await gitDir.runCommand(['add', '-A']); - await gitDir - .runCommand(['commit', '-m', 'Update versions to 0.0.1']); + test( + 'Exiting versions do not trigger release, but fail if the tags do not exist.', + () async { + const Map httpResponsePlugin1 = { + 'name': 'plugin1', + 'versions': ['0.0.2'], + }; + + const Map httpResponsePlugin2 = { + 'name': 'plugin2', + 'versions': ['0.0.2'], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'plugin1.json') { + return http.Response(json.encode(httpResponsePlugin1), 200); + } else if (request.url.pathSegments.last == 'plugin2.json') { + return http.Response(json.encode(httpResponsePlugin2), 200); + } + return http.Response('', 500); + }); + final PublishPluginCommand command = PublishPluginCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString()), + stdinput: mockStdin, + httpClient: mockClient, + gitDir: gitDir); - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - expect( - printedMessages, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Getting existing tags...', - 'The new version (0.0.1) is lower than the current version (0.0.2) for plugin1.\nThis git commit is a revert, no release is tagged.', - 'The new version (0.0.1) is lower than the current version (0.0.2) for plugin2.\nThis git commit is a revert, no release is tagged.', - 'Done!' - ])); + commandRunner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + commandRunner.addCommand(command); + // Non-federated + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + // federated + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'), + version: '0.0.2'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await expectLater( + () => commandRunner.run( + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']), + throwsA(const TypeMatcher())); expect(processRunner.pushTagsArgs, isEmpty); }); From d9857db0cf222640f5f5b9c95977257a798bdd89 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 25 Jun 2021 08:59:03 -0700 Subject: [PATCH 075/249] [flutter_plugin_tools] Migrate analyze to new base command (#4084) Switches `analyze` to the new base command that handles the boilerplate of looping over target packages. This will change the output format slightly, but shoudn't have any functional change. Updates tests to use runCapturingPrint so that test run output isn't mixed with command output. Part of https://github.com/flutter/flutter/issues/83413 --- script/tool/lib/src/analyze_command.dart | 74 ++++++++++++---------- script/tool/test/analyze_command_test.dart | 19 +++--- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 003f0bcda82..3f6a2444ad9 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -8,11 +8,13 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +const int _exitBadCustomAnalysisFile = 2; + /// A command to run Dart analysis on packages. -class AnalyzeCommand extends PluginCommand { +class AnalyzeCommand extends PackageLoopingCommand { /// Creates a analysis command instance. AnalyzeCommand( Directory packagesDir, { @@ -32,6 +34,8 @@ class AnalyzeCommand extends PluginCommand { static const String _analysisSdk = 'analysis-sdk'; + late String _dartBinaryPath; + @override final String name = 'analyze'; @@ -40,9 +44,10 @@ class AnalyzeCommand extends PluginCommand { 'This command requires "dart" and "flutter" to be in your path.'; @override - Future run() async { - print('Verifying analysis settings...'); + final bool hasLongOutput = false; + /// Checks that there are no unexpected analysis_options.yaml files. + void _validateAnalysisOptions() { final List files = packagesDir.listSync(recursive: true); for (final FileSystemEntity file in files) { if (file.basename != 'analysis_options.yaml' && @@ -59,18 +64,25 @@ class AnalyzeCommand extends PluginCommand { continue; } - print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); - print( - 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); - throw ToolExit(1); + printError( + 'Found an extra analysis_options.yaml in ${file.absolute.path}.'); + printError( + 'If this was deliberate, pass the package to the analyze command ' + 'with the --$_customAnalysisFlag flag and try again.'); + throw ToolExit(_exitBadCustomAnalysisFile); } + } + /// Ensures that the dependent packages have been fetched for all packages + /// (including their sub-packages) that will be analyzed. + Future _runPackagesGetOnTargetPackages() async { final List packageDirectories = await getPackages().toList(); final Set packagePaths = packageDirectories.map((Directory dir) => dir.path).toSet(); packageDirectories.removeWhere((Directory directory) { - // We remove the 'example' subdirectories - 'flutter pub get' automatically - // runs 'pub get' there as part of handling the parent directory. + // Remove the 'example' subdirectories; 'flutter packages get' + // automatically runs 'pub get' there as part of handling the parent + // directory. return directory.basename == 'example' && packagePaths.contains(directory.parent.path); }); @@ -78,33 +90,29 @@ class AnalyzeCommand extends PluginCommand { await processRunner.runAndStream('flutter', ['packages', 'get'], workingDir: package, exitOnError: true); } + } + + @override + Future initializeRun() async { + print('Verifying analysis settings...'); + _validateAnalysisOptions(); + + print('Fetching dependencies...'); + await _runPackagesGetOnTargetPackages(); // Use the Dart SDK override if one was passed in. final String? dartSdk = argResults![_analysisSdk] as String?; - final String dartBinary = - dartSdk == null ? 'dart' : p.join(dartSdk, 'bin', 'dart'); - - final List failingPackages = []; - final List pluginDirectories = await getPlugins().toList(); - for (final Directory package in pluginDirectories) { - final int exitCode = await processRunner.runAndStream( - dartBinary, ['analyze', '--fatal-infos'], - workingDir: package); - if (exitCode != 0) { - failingPackages.add(p.basename(package.path)); - } - } - - print('\n\n'); + _dartBinaryPath = dartSdk == null ? 'dart' : p.join(dartSdk, 'bin', 'dart'); + } - if (failingPackages.isNotEmpty) { - print('The following packages have analyzer errors (see above):'); - for (final String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); + @override + Future> runForPackage(Directory package) async { + final int exitCode = await processRunner.runAndStream( + _dartBinaryPath, ['analyze', '--fatal-infos'], + workingDir: package); + if (exitCode != 0) { + return PackageLoopingCommand.failure; } - - print('No analyzer errors found!'); + return PackageLoopingCommand.success; } } diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 464aa1d9147..bdf9910f0b1 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -36,7 +36,7 @@ void main() { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); processRunner.processToReturn = mockProcess; - await runner.run(['analyze']); + await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, @@ -58,7 +58,7 @@ void main() { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); processRunner.processToReturn = mockProcess; - await runner.run(['analyze']); + await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, @@ -77,7 +77,7 @@ void main() { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); processRunner.processToReturn = mockProcess; - await runner.run(['analyze']); + await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, @@ -99,7 +99,8 @@ void main() { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); processRunner.processToReturn = mockProcess; - await runner.run(['analyze', '--analysis-sdk', 'foo/bar/baz']); + await runCapturingPrint( + runner, ['analyze', '--analysis-sdk', 'foo/bar/baz']); expect( processRunner.recordedCalls, @@ -123,7 +124,7 @@ void main() { createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); - await expectLater(() => runner.run(['analyze']), + await expectLater(() => runCapturingPrint(runner, ['analyze']), throwsA(const TypeMatcher())); }); @@ -131,7 +132,7 @@ void main() { createFakePlugin('foo', packagesDir, extraFiles: ['.analysis_options']); - await expectLater(() => runner.run(['analyze']), + await expectLater(() => runCapturingPrint(runner, ['analyze']), throwsA(const TypeMatcher())); }); @@ -142,7 +143,8 @@ void main() { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); processRunner.processToReturn = mockProcess; - await runner.run(['analyze', '--custom-analysis', 'foo']); + await runCapturingPrint( + runner, ['analyze', '--custom-analysis', 'foo']); expect( processRunner.recordedCalls, @@ -164,7 +166,8 @@ void main() { processRunner.processToReturn = mockProcess; await expectLater( - () => runner.run(['analyze', '--custom-analysis', '']), + () => runCapturingPrint( + runner, ['analyze', '--custom-analysis', '']), throwsA(const TypeMatcher())); }); }); From 9ce20139d54edf9ce3bfc63803ceaa36724f746a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 25 Jun 2021 14:40:52 -0700 Subject: [PATCH 076/249] Migrate command, add failure test, remove skip (#4106) Switches `podspec` to the new base command that handles the boilerplate of looping over target packages. Includes test improvements: - Adds failure tests; previously no failure cases were covered. - Captures output using the standard mechanism instead of using a custom `_print`, simplifying the command code. Also removes `--skip`, which is redundant with `--exclude`. Part of flutter/flutter#83413 --- script/tool/CHANGELOG.md | 3 +- .../tool/lib/src/common/plugin_command.dart | 10 ++- .../tool/lib/src/lint_podspecs_command.dart | 65 ++++++--------- .../tool/test/lint_podspecs_command_test.dart | 79 ++++++++++++------- 4 files changed, 87 insertions(+), 70 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 3e2ec3f4c99..1b6cf2a4471 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -7,7 +7,8 @@ - **Breaking change**: it now requires an `--ios` and/or `--macos` flag. - The tooling now runs in strong null-safe mode. - `publish plugins` check against pub.dev to determine if a release should happen. -- Modified the output format of `pubspec-check` and `xctest` +- Modified the output format of many commands +- Removed `podspec`'s `--skip` in favor of `--ignore` using the new structure. ## 0.2.0 diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 4c095858e45..e3ee109dd0c 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -250,10 +250,16 @@ abstract class PluginCommand extends Command { /// Returns the files contained, recursively, within the plugins /// involved in this command execution. Stream getFiles() { - return getPlugins().asyncExpand((Directory folder) => folder + return getPlugins() + .asyncExpand((Directory folder) => getFilesForPackage(folder)); + } + + /// Returns the files contained, recursively, within [package]. + Stream getFilesForPackage(Directory package) { + return package .list(recursive: true, followLinks: false) .where((FileSystemEntity entity) => entity is File) - .cast()); + .cast(); } /// Returns whether the specified entity is a directory containing a diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 5e86d2be40b..2b4beeb92a1 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -10,29 +10,26 @@ import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +const int _exitUnsupportedPlatform = 2; + /// Lint the CocoaPod podspecs and run unit tests. /// /// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. -class LintPodspecsCommand extends PluginCommand { +class LintPodspecsCommand extends PackageLoopingCommand { /// Creates an instance of the linter command. LintPodspecsCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), - Print print = print, }) : _platform = platform, - _print = print, super(packagesDir, processRunner: processRunner) { - argParser.addMultiOption('skip', - help: - 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', - valueHelp: 'podspec_file_name'); argParser.addMultiOption('ignore-warnings', help: - 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', + 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs ' + 'with this basename (example: plugins with known warnings)', valueHelp: 'podspec_file_name'); } @@ -49,13 +46,11 @@ class LintPodspecsCommand extends PluginCommand { final Platform _platform; - final Print _print; - @override - Future run() async { + Future initializeRun() async { if (!_platform.isMacOS) { - _print('Detected platform is not macOS, skipping podspec lint'); - return; + printError('This command is only supported on macOS'); + throw ToolExit(_exitUnsupportedPlatform); } await processRunner.run( @@ -65,32 +60,24 @@ class LintPodspecsCommand extends PluginCommand { exitOnError: true, logOnError: true, ); + } - _print('Starting podspec lint test'); - - final List failingPlugins = []; - for (final File podspec in await _podspecsToLint()) { + @override + Future> runForPackage(Directory package) async { + final List errors = []; + for (final File podspec in await _podspecsToLint(package)) { if (!await _lintPodspec(podspec)) { - failingPlugins.add(p.basenameWithoutExtension(podspec.path)); - } - } - - _print('\n\n'); - if (failingPlugins.isNotEmpty) { - _print('The following plugins have podspec errors (see above):'); - for (final String plugin in failingPlugins) { - _print(' * $plugin'); + errors.add(p.basename(podspec.path)); } - throw ToolExit(1); } + return errors; } - Future> _podspecsToLint() async { - final List podspecs = await getFiles().where((File entity) { + Future> _podspecsToLint(Directory package) async { + final List podspecs = + await getFilesForPackage(package).where((File entity) { final String filePath = entity.path; - return p.extension(filePath) == '.podspec' && - !getStringListArg('skip') - .contains(p.basenameWithoutExtension(filePath)); + return p.extension(filePath) == '.podspec'; }).toList(); podspecs.sort( @@ -103,19 +90,19 @@ class LintPodspecsCommand extends PluginCommand { final String podspecPath = podspec.path; final String podspecBasename = p.basename(podspecPath); - _print('Linting $podspecBasename'); + print('Linting $podspecBasename'); // Lint plugin as framework (use_frameworks!). final ProcessResult frameworkResult = await _runPodLint(podspecPath, libraryLint: true); - _print(frameworkResult.stdout); - _print(frameworkResult.stderr); + print(frameworkResult.stdout); + print(frameworkResult.stderr); // Lint plugin as library. final ProcessResult libraryResult = await _runPodLint(podspecPath, libraryLint: false); - _print(libraryResult.stdout); - _print(libraryResult.stderr); + print(libraryResult.stdout); + print(libraryResult.stderr); return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; } @@ -135,7 +122,7 @@ class LintPodspecsCommand extends PluginCommand { if (libraryLint) '--use-libraries' ]; - _print('Running "pod ${arguments.join(' ')}"'); + print('Running "pod ${arguments.join(' ')}"'); return processRunner.run('pod', arguments, workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); } diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index d86c9145fc1..a6a5502913a 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -5,6 +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/core.dart'; import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -19,20 +20,17 @@ void main() { late CommandRunner runner; late MockPlatform mockPlatform; late RecordingProcessRunner processRunner; - late List printedMessages; setUp(() { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - printedMessages = []; mockPlatform = MockPlatform(isMacOS: true); processRunner = RecordingProcessRunner(); final LintPodspecsCommand command = LintPodspecsCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, - print: (Object? message) => printedMessages.add(message.toString()), ); runner = @@ -47,14 +45,26 @@ void main() { test('only runs on macOS', () async { createFakePlugin('plugin1', packagesDir, extraFiles: ['plugin1.podspec']); - mockPlatform.isMacOS = false; - await runner.run(['podspecs']); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspecs'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( processRunner.recordedCalls, equals([]), ); + + expect( + output, + containsAllInOrder( + [contains('only supported on macOS')], + )); }); test('runs pod lib lint on a podspec', () async { @@ -70,7 +80,8 @@ void main() { processRunner.resultStdout = 'Foo'; processRunner.resultStderr = 'Bar'; - await runner.run(['podspecs']); + final List output = + await runCapturingPrint(runner, ['podspecs']); expect( processRunner.recordedCalls, @@ -102,33 +113,17 @@ void main() { ]), ); - expect(printedMessages, contains('Linting plugin1.podspec')); - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); - }); - - test('skips podspecs with known issues', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - createFakePlugin('plugin2', packagesDir, - extraFiles: ['plugin2.podspec']); - - await runner - .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', const ['pod'], packagesDir.path), - ]), - ); + expect(output, contains('Linting plugin1.podspec')); + expect(output, contains('Foo')); + expect(output, contains('Bar')); }); test('allow warnings for podspecs with known warnings', () async { final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, extraFiles: ['plugin1.podspec']); - await runner.run(['podspecs', '--ignore-warnings=plugin1']); + final List output = await runCapturingPrint( + runner, ['podspecs', '--ignore-warnings=plugin1']); expect( processRunner.recordedCalls, @@ -162,7 +157,35 @@ void main() { ]), ); - expect(printedMessages, contains('Linting plugin1.podspec')); + expect(output, contains('Linting plugin1.podspec')); + }); + + test('fails if linting fails', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + + // Simulate failure from `pod`. + final MockProcess mockDriveProcess = MockProcess(); + mockDriveProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockDriveProcess; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspecs'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains('The following packages had errors:'), + contains('plugin1:\n' + ' plugin1.podspec') + ], + )); }); }); } From fef8e6c2fbdbf150d67274f3523fef76c154b11c Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Sun, 27 Jun 2021 18:30:27 -0700 Subject: [PATCH 077/249] [flutter_plugin_tools] release 0.3.0 (#4109) --- script/tool/CHANGELOG.md | 2 +- script/tool/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1b6cf2a4471..94514e38103 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 0.3.0 - Add a --build-id flag to `firebase-test-lab` instead of hard-coding the use of `CIRRUS_BUILD_ID`. `CIRRUS_BUILD_ID` is the default value for that flag, for backward diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 5d2200abcdb..6273fe9bf27 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.2.0 +version: 0.3.0 dependencies: args: ^2.1.0 From 355ffc0a8868c2bb2cfb9b0bb932017e2a3dce0b Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 28 Jun 2021 09:06:03 -0700 Subject: [PATCH 078/249] [flutter_plugin_tools] ignore flutter_plugin_tools when publishing (#4110) --- .../tool/lib/src/publish_plugin_command.dart | 8 +++ .../test/publish_plugin_command_test.dart | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 622a1a3cb13..18b6ff0ed74 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -290,6 +290,14 @@ Safe to ignore if the package is deleted in this commit. } final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + if (pubspec.name == 'flutter_plugin_tools') { + // Ignore flutter_plugin_tools package when running publishing through flutter_plugin_tools. + // TODO(cyanglaz): Make the tool also auto publish flutter_plugin_tools package. + // https://github.com/flutter/flutter/issues/85430 + return _CheckNeedsReleaseResult.noRelease; + } + if (pubspec.publishTo == 'none') { return _CheckNeedsReleaseResult.noRelease; } diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index c7832e0da19..f060cd2fbfd 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -995,6 +995,57 @@ void main() { ])); expect(processRunner.pushTagsArgs, isEmpty); }); + + test('Do not release flutter_plugin_tools', () async { + const Map httpResponsePlugin1 = { + 'name': 'flutter_plugin_tools', + 'versions': [], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'flutter_plugin_tools.json') { + return http.Response(json.encode(httpResponsePlugin1), 200); + } + return http.Response('', 500); + }); + final PublishPluginCommand command = PublishPluginCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString()), + stdinput: mockStdin, + httpClient: mockClient, + gitDir: gitDir); + + commandRunner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + commandRunner.addCommand(command); + + final Directory flutterPluginTools = + createFakePlugin('flutter_plugin_tools', packagesDir); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Done!' + ])); + expect( + printedMessages.contains( + 'Running `pub publish ` in ${flutterPluginTools.path}...\n', + ), + isFalse); + expect(processRunner.pushTagsArgs, isEmpty); + processRunner.pushTagsArgs.clear(); + printedMessages.clear(); + }); }); } From 40162caa650d2e036ba2bfdb27e51ef47c26ad5d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 28 Jun 2021 11:26:34 -0700 Subject: [PATCH 079/249] [flutter_plugin_tools] Migrate java-test to new base command (#4105) Switches `java-test` to the new base command that handles the boilerplate of looping over target packages. Includes test improvements: - Adds failure tests; previously no failure cases were covered. - Captures output so test output isn't spammed with command output. Part of flutter/flutter#83413 --- script/tool/lib/src/java_test_command.dart | 55 +++++---------- script/tool/test/java_test_command_test.dart | 70 +++++++++++++++++++- 2 files changed, 84 insertions(+), 41 deletions(-) diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index d7e453b6ad7..77b8aa70a6e 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -6,17 +6,19 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; /// A command to run the Java tests of Android plugins. -class JavaTestCommand extends PluginCommand { +class JavaTestCommand extends PackageLoopingCommand { /// Creates an instance of the test runner. JavaTestCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), }) : super(packagesDir, processRunner: processRunner); + static const String _gradleWrapper = 'gradlew'; + @override final String name = 'java-test'; @@ -25,12 +27,10 @@ class JavaTestCommand extends PluginCommand { 'Building the apks of the example apps is required before executing this' 'command.'; - static const String _gradleWrapper = 'gradlew'; - @override - Future run() async { - final Stream examplesWithTests = getExamples().where( - (Directory d) => + Future> runForPackage(Directory package) async { + final Iterable examplesWithTests = getExamplesForPlugin(package) + .where((Directory d) => isFlutterPackage(d) && (d .childDirectory('android') @@ -44,18 +44,17 @@ class JavaTestCommand extends PluginCommand { .childDirectory('test') .existsSync())); - final List failingPackages = []; - final List missingFlutterBuild = []; - await for (final Directory example in examplesWithTests) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - print('\nRUNNING JAVA TESTS for $packageName'); + final List errors = []; + for (final Directory example in examplesWithTests) { + final String exampleName = p.relative(example.path, from: package.path); + print('\nRUNNING JAVA TESTS for $exampleName'); final Directory androidDirectory = example.childDirectory('android'); if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { - print('ERROR: Run "flutter build apk" on example app of $packageName' + printError('ERROR: Run "flutter build apk" on $exampleName, or run ' + 'this tool\'s "build-examples --apk" command, ' 'before executing tests.'); - missingFlutterBuild.add(packageName); + errors.add('$exampleName has not been built.'); continue; } @@ -64,31 +63,9 @@ class JavaTestCommand extends PluginCommand { ['testDebugUnitTest', '--info'], workingDir: androidDirectory); if (exitCode != 0) { - failingPackages.add(packageName); - } - } - - print('\n\n'); - if (failingPackages.isNotEmpty) { - print( - 'The Java tests for the following packages are failing (see above for' - 'details):'); - for (final String package in failingPackages) { - print(' * $package'); - } - } - if (missingFlutterBuild.isNotEmpty) { - print('Run "pub global run flutter_plugin_tools build-examples --apk" on' - 'the following packages before executing tests again:'); - for (final String package in missingFlutterBuild) { - print(' * $package'); + errors.add('$exampleName tests failed.'); } } - - if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { - throw ToolExit(1); - } - - print('All Java tests successful!'); + return errors; } } diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index fc80961462c..894a5c3fce7 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.dart @@ -11,6 +11,7 @@ import 'package:flutter_plugin_tools/src/java_test_command.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { @@ -45,7 +46,7 @@ void main() { ], ); - await runner.run(['java-test']); + await runCapturingPrint(runner, ['java-test']); expect( processRunner.recordedCalls, @@ -72,7 +73,7 @@ void main() { ], ); - await runner.run(['java-test']); + await runCapturingPrint(runner, ['java-test']); expect( processRunner.recordedCalls, @@ -85,5 +86,70 @@ void main() { ]), ); }); + + test('fails when the app needs to be built', () async { + createFakePlugin( + 'plugin1', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/app/src/test/example_test.java', + ], + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['java-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('ERROR: Run "flutter build apk" on example'), + contains('plugin1:\n' + ' example has not been built.') + ]), + ); + }); + + test('fails when a test fails', () async { + createFakePlugin( + 'plugin1', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + // Simulate failure from `gradlew`. + final MockProcess mockDriveProcess = MockProcess(); + mockDriveProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockDriveProcess; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['java-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('plugin1:\n' + ' example tests failed.') + ]), + ); + }); }); } From 23c3f5794ade0beae29c547687e6c588d339f10a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 28 Jun 2021 13:25:06 -0700 Subject: [PATCH 080/249] [flutter_plugin_tools] Restructure version-check (#4111) Combines the two different aspects of version-checking into a single looping structure based on plugins, using the new base command, rather than one operating on plugins as controlled by the usual flags and the other operating on a git list of changed files. Also simplifies the determination of the new version by simply checking the file, rather than querying git for the HEAD state of the file. Tests setup is simplified since we no longer need to set up nearly as much fake `git` output. Minor changes to base commands: - Move indentation up to PackageLoopingCommand so that it is consistent across commands - Add a new post-loop command for any cleanup, which is needed by version-check - Change the way the GitDir instance is managed by the base PluginCommand, so that it's always cached even when not overridden, to reduce duplicate work and code. Part of https://github.com/flutter/flutter/issues/83413 --- script/tool/lib/src/common/core.dart | 10 + .../src/common/package_looping_command.dart | 9 + .../tool/lib/src/common/plugin_command.dart | 43 ++- .../tool/lib/src/publish_plugin_command.dart | 12 +- .../tool/lib/src/pubspec_check_command.dart | 1 - .../tool/lib/src/version_check_command.dart | 344 +++++++++--------- ...t.dart => version_check_command_test.dart} | 243 ++++--------- 7 files changed, 282 insertions(+), 380 deletions(-) rename script/tool/test/{version_check_test.dart => version_check_command_test.dart} (70%) diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index b2be8f56d17..3b07baf5dc1 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -58,6 +58,16 @@ void printSuccess(String successMessage) { print(Colorize(successMessage)..green()); } +/// Prints `warningMessage` in yellow. +/// +/// Warnings are not surfaced in CI summaries, so this is only useful for +/// highlighting something when someone is already looking though the log +/// messages. DO NOT RELY on someone noticing a warning; instead, use it for +/// things that might be useful to someone debugging an unexpected result. +void printWarning(String warningMessage) { + print(Colorize(warningMessage)..yellow()); +} + /// Prints `errorMessage` in red. void printError(String errorMessage) { print(Colorize(errorMessage)..red()); diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 1349a5ed5dc..cfe99313068 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -35,6 +35,10 @@ abstract class PackageLoopingCommand extends PluginCommand { /// in the final summary. An empty list indicates success. Future> runForPackage(Directory package); + /// Called during [run] after all calls to [runForPackage]. This provides an + /// opportunity to do any cleanup of run-level state. + Future completeRun() async {} + /// Whether or not the output (if any) of [runForPackage] is long, or short. /// /// This changes the logging that happens at the start of each package's @@ -99,6 +103,9 @@ abstract class PackageLoopingCommand extends PluginCommand { return packageName; } + /// The suggested indentation for printed output. + String get indentation => hasLongOutput ? '' : ' '; + // ---------------------------------------- @override @@ -115,6 +122,8 @@ abstract class PackageLoopingCommand extends PluginCommand { results[package] = await runForPackage(package); } + completeRun(); + // If there were any errors reported, summarize them and exit. if (results.values.any((List failures) => failures.isNotEmpty)) { const String indentation = ' '; diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index e3ee109dd0c..9a96ab13443 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -20,8 +20,8 @@ abstract class PluginCommand extends Command { PluginCommand( this.packagesDir, { this.processRunner = const ProcessRunner(), - this.gitDir, - }) { + GitDir? gitDir, + }) : _gitDir = gitDir { argParser.addMultiOption( _pluginsArg, splitCommas: true, @@ -76,10 +76,11 @@ abstract class PluginCommand extends Command { /// This can be overridden for testing. final ProcessRunner processRunner; - /// The git directory to use. By default it uses the parent directory. + /// The git directory to use. If unset, [gitDir] populates it from the + /// packages directory's enclosing repository. /// /// This can be mocked for testing. - final GitDir? gitDir; + GitDir? _gitDir; int? _shardIndex; int? _shardCount; @@ -100,6 +101,26 @@ abstract class PluginCommand extends Command { return _shardCount!; } + /// Returns the [GitDir] containing [packagesDir]. + Future get gitDir async { + GitDir? gitDir = _gitDir; + if (gitDir != null) { + return gitDir; + } + + // Ensure there are no symlinks in the path, as it can break + // GitDir's allowSubdirectory:true. + final String packagesPath = packagesDir.resolveSymbolicLinksSync(); + if (!await GitDir.isGitDir(packagesPath)) { + printError('$packagesPath is not a valid Git repository.'); + throw ToolExit(2); + } + gitDir = + await GitDir.fromExisting(packagesDir.path, allowSubdirectory: true); + _gitDir = gitDir; + return gitDir; + } + /// Convenience accessor for boolean arguments. bool getBoolArg(String key) { return (argResults![key] as bool?) ?? false; @@ -291,22 +312,10 @@ abstract class PluginCommand extends Command { /// /// 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); + GitVersionFinder(await gitDir, baseSha); return gitVersionFinder; } diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 18b6ff0ed74..740178829ca 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -149,15 +149,7 @@ class PublishPluginCommand extends PluginCommand { } _print('Checking local repo...'); - // Ensure there are no symlinks in the path, as it can break - // GitDir's allowSubdirectory:true. - final String packagesPath = packagesDir.resolveSymbolicLinksSync(); - if (!await GitDir.isGitDir(packagesPath)) { - _print('$packagesPath is not a valid Git repository.'); - throw ToolExit(1); - } - final GitDir baseGitDir = gitDir ?? - await GitDir.fromExisting(packagesPath, allowSubdirectory: true); + final GitDir repository = await gitDir; final bool shouldPushTag = getBoolArg(_pushTagsOption); _RemoteInfo? remote; @@ -179,7 +171,7 @@ class PublishPluginCommand extends PluginCommand { bool successful; if (publishAllChanged) { successful = await _publishAllChangedPackages( - baseGitDir: baseGitDir, + baseGitDir: repository, remoteForTagPush: remote, ); } else { diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 44b6b061542..d257638971d 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -70,7 +70,6 @@ class PubspecCheckCommand extends PackageLoopingCommand { File pubspecFile, { required String packageName, }) async { - const String indentation = ' '; final String contents = pubspecFile.readAsStringSync(); final Pubspec? pubspec = _tryParsePubspec(contents); if (pubspec == null) { diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 5e9f55333f8..2584d70c5fc 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -6,12 +6,13 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; @@ -31,46 +32,47 @@ enum NextVersionType { } /// Returns the set of allowed next versions, with their change type, for -/// [masterVersion]. +/// [version]. /// -/// [headVerison] is used to check whether this is a pre-1.0 version bump, as +/// [newVersion] is used to check whether this is a pre-1.0 version bump, as /// those have different semver rules. @visibleForTesting Map getAllowedNextVersions( - {required Version masterVersion, required Version headVersion}) { + Version version, { + required Version newVersion, +}) { final Map allowedNextVersions = { - masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, - masterVersion.nextMinor: NextVersionType.MINOR, - masterVersion.nextPatch: NextVersionType.PATCH, + version.nextMajor: NextVersionType.BREAKING_MAJOR, + version.nextMinor: NextVersionType.MINOR, + version.nextPatch: NextVersionType.PATCH, }; - if (masterVersion.major < 1 && headVersion.major < 1) { + if (version.major < 1 && newVersion.major < 1) { int nextBuildNumber = -1; - if (masterVersion.build.isEmpty) { + if (version.build.isEmpty) { nextBuildNumber = 1; } else { - final int currentBuildNumber = masterVersion.build.first as int; + final int currentBuildNumber = version.build.first as int; nextBuildNumber = currentBuildNumber + 1; } final Version preReleaseVersion = Version( - masterVersion.major, - masterVersion.minor, - masterVersion.patch, + version.major, + version.minor, + version.patch, build: nextBuildNumber.toString(), ); allowedNextVersions.clear(); - allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; - allowedNextVersions[masterVersion.nextMinor] = - NextVersionType.BREAKING_MAJOR; - allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; + allowedNextVersions[version.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[version.nextMinor] = NextVersionType.BREAKING_MAJOR; + allowedNextVersions[version.nextPatch] = NextVersionType.MINOR; allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; } return allowedNextVersions; } /// A command to validate version changes to packages. -class VersionCheckCommand extends PluginCommand { +class VersionCheckCommand extends PackageLoopingCommand { /// Creates an instance of the version check command. VersionCheckCommand( Directory packagesDir, { @@ -91,6 +93,8 @@ class VersionCheckCommand extends PluginCommand { static const String _againstPubFlag = 'against-pub'; + final PubVersionFinder _pubVersionFinder; + @override final String name = 'version-check'; @@ -100,175 +104,173 @@ class VersionCheckCommand extends PluginCommand { 'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n' 'This command requires "pub" and "flutter" to be in your path.'; - final PubVersionFinder _pubVersionFinder; + @override + bool get hasLongOutput => false; @override - Future run() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + Future initializeRun() async {} - final List changedPubspecs = - await gitVersionFinder.getChangedPubSpecs(); + @override + Future> runForPackage(Directory package) async { + final List errors = []; - final List badVersionChangePubspecs = []; + final Pubspec? pubspec = _tryParsePubspec(package); + if (pubspec == null) { + errors.add('Invalid pubspec.yaml.'); + return errors; // No remaining checks make sense. + } - const String indentation = ' '; - for (final String pubspecPath in changedPubspecs) { - print('Checking versions for $pubspecPath...'); - final File pubspecFile = packagesDir.fileSystem.file(pubspecPath); - if (!pubspecFile.existsSync()) { - print('${indentation}Deleted; skipping.'); - continue; - } - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - if (pubspec.publishTo == 'none') { - print('${indentation}Found "publish_to: none"; skipping.'); - continue; - } + if (pubspec.publishTo == 'none') { + printSkip('${indentation}Found "publish_to: none".'); + return PackageLoopingCommand.success; + } - final Version? headVersion = - await gitVersionFinder.getPackageVersion(pubspecPath, gitRef: 'HEAD'); - if (headVersion == null) { - printError('${indentation}No version found. A package that ' - 'intentionally has no version should be marked ' - '"publish_to: none".'); - badVersionChangePubspecs.add(pubspecPath); - continue; - } - Version? sourceVersion; - if (getBoolArg(_againstPubFlag)) { - final String packageName = pubspecFile.parent.basename; - final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: packageName); - switch (pubVersionFinderResponse.result) { - case PubVersionFinderResult.success: - sourceVersion = pubVersionFinderResponse.versions.first; - print( - '$indentation$packageName: Current largest version on pub: $sourceVersion'); - break; - case PubVersionFinderResult.fail: - printError(''' + final Version? currentPubspecVersion = pubspec.version; + if (currentPubspecVersion == null) { + printError('${indentation}No version found in pubspec.yaml. A package ' + 'that intentionally has no version should be marked ' + '"publish_to: none".'); + errors.add('No pubspec.yaml version.'); + return errors; // No remaining checks make sense. + } + + if (!await _hasValidVersionChange(package, pubspec: pubspec)) { + errors.add('Disallowed version change.'); + } + + if (!(await _hasConsistentVersion(package, pubspec: pubspec))) { + errors.add('pubspec.yaml and CHANGELOG.md have different versions'); + } + + return errors; + } + + @override + Future completeRun() async { + _pubVersionFinder.httpClient.close(); + } + + /// Returns the previous published version of [package]. + /// + /// [packageName] must be the actual name of the package as published (i.e., + /// the name from pubspec.yaml, not the on disk name if different.) + Future _fetchPreviousVersionFromPub(String packageName) async { + final PubVersionFinderResponse pubVersionFinderResponse = + await _pubVersionFinder.getPackageVersion(package: packageName); + switch (pubVersionFinderResponse.result) { + case PubVersionFinderResult.success: + return pubVersionFinderResponse.versions.first; + case PubVersionFinderResult.fail: + printError(''' ${indentation}Error fetching version on pub for $packageName. ${indentation}HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} '''); - badVersionChangePubspecs.add(pubspecPath); - continue; - case PubVersionFinderResult.noPackageFound: - sourceVersion = null; - break; - } - } else { - sourceVersion = await gitVersionFinder.getPackageVersion(pubspecPath); - } - if (sourceVersion == null) { - String safeToIgnoreMessage; - if (getBoolArg(_againstPubFlag)) { - safeToIgnoreMessage = - '${indentation}Unable to find package on pub server.'; - } else { - safeToIgnoreMessage = - '${indentation}Unable to find pubspec in master.'; - } - print('$safeToIgnoreMessage Safe to ignore if the project is new.'); - continue; - } + return null; + case PubVersionFinderResult.noPackageFound: + return Version.none; + } + } - if (sourceVersion == headVersion) { - print('${indentation}No version change.'); - continue; - } + /// Returns the version of [package] from git at the base comparison hash. + Future _getPreviousVersionFromGit( + Directory package, { + required GitVersionFinder gitVersionFinder, + }) async { + final File pubspecFile = package.childFile('pubspec.yaml'); + return await gitVersionFinder.getPackageVersion( + p.relative(pubspecFile.absolute.path, from: (await gitDir).path)); + } - // Check for reverts when doing local validation. - if (!getBoolArg(_againstPubFlag) && headVersion < sourceVersion) { - final Map possibleVersionsFromNewVersion = - getAllowedNextVersions( - masterVersion: headVersion, headVersion: sourceVersion); - // Since this skips validation, try to ensure that it really is likely - // to be a revert rather than a typo by checking that the transition - // from the lower version to the new version would have been valid. - if (possibleVersionsFromNewVersion.containsKey(sourceVersion)) { - print('${indentation}New version is lower than previous version. ' - 'This is assumed to be a revert.'); - continue; - } + /// Returns true if the version of [package] is either unchanged relative to + /// the comparison base (git or pub, depending on flags), or is a valid + /// version transition. + Future _hasValidVersionChange( + Directory package, { + required Pubspec pubspec, + }) async { + // This method isn't called unless `version` is non-null. + final Version currentVersion = pubspec.version!; + Version? previousVersion; + if (getBoolArg(_againstPubFlag)) { + previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); + if (previousVersion == null) { + return false; } - - final Map allowedNextVersions = - getAllowedNextVersions( - masterVersion: sourceVersion, headVersion: headVersion); - - if (!allowedNextVersions.containsKey(headVersion)) { - final String source = (getBoolArg(_againstPubFlag)) ? 'pub' : 'master'; - printError('${indentation}Incorrectly updated version.\n' - '${indentation}HEAD: $headVersion, $source: $sourceVersion.\n' - '${indentation}Allowed versions: $allowedNextVersions'); - badVersionChangePubspecs.add(pubspecPath); - continue; - } else { - print('$indentation$headVersion -> $sourceVersion'); + if (previousVersion != Version.none) { + print( + '$indentation${pubspec.name}: Current largest version on pub: $previousVersion'); } + } else { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + previousVersion = await _getPreviousVersionFromGit(package, + gitVersionFinder: gitVersionFinder) ?? + Version.none; + } + if (previousVersion == Version.none) { + print('${indentation}Unable to find previous version ' + '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); + printWarning( + '${indentation}If this plugin is not new, something has gone wrong.'); + return true; + } - final bool isPlatformInterface = - pubspec.name.endsWith('_platform_interface'); - if (isPlatformInterface && - allowedNextVersions[headVersion] == NextVersionType.BREAKING_MAJOR) { - printError('$pubspecPath breaking change detected.\n' - 'Breaking changes to platform interfaces are strongly discouraged.\n'); - badVersionChangePubspecs.add(pubspecPath); - continue; - } + if (previousVersion == currentVersion) { + print('${indentation}No version change.'); + return true; } - _pubVersionFinder.httpClient.close(); - // TODO(stuartmorgan): Unify the way iteration works for these checks; the - // two checks shouldn't be operating independently on different lists. - final List mismatchedVersionPlugins = []; - await for (final Directory plugin in getPlugins()) { - if (!(await _checkVersionsMatch(plugin))) { - mismatchedVersionPlugins.add(plugin.basename); + // Check for reverts when doing local validation. + if (!getBoolArg(_againstPubFlag) && currentVersion < previousVersion) { + final Map possibleVersionsFromNewVersion = + getAllowedNextVersions(currentVersion, newVersion: previousVersion); + // Since this skips validation, try to ensure that it really is likely + // to be a revert rather than a typo by checking that the transition + // from the lower version to the new version would have been valid. + if (possibleVersionsFromNewVersion.containsKey(previousVersion)) { + print('${indentation}New version is lower than previous version. ' + 'This is assumed to be a revert.'); + return true; } } - bool passed = true; - if (badVersionChangePubspecs.isNotEmpty) { - passed = false; - printError(''' -The following pubspecs failed validaton: -$indentation${badVersionChangePubspecs.join('\n$indentation')} -'''); - } - if (mismatchedVersionPlugins.isNotEmpty) { - passed = false; - printError(''' -The following pubspecs have different versions in pubspec.yaml and CHANGELOG.md: -$indentation${mismatchedVersionPlugins.join('\n$indentation')} -'''); - } - if (!passed) { - throw ToolExit(1); + final Map allowedNextVersions = + getAllowedNextVersions(previousVersion, newVersion: currentVersion); + + if (allowedNextVersions.containsKey(currentVersion)) { + print('$indentation$previousVersion -> $currentVersion'); + } else { + final String source = (getBoolArg(_againstPubFlag)) ? 'pub' : 'master'; + printError('${indentation}Incorrectly updated version.\n' + '${indentation}HEAD: $currentVersion, $source: $previousVersion.\n' + '${indentation}Allowed versions: $allowedNextVersions'); + return false; } - print('No version check errors found!'); + final bool isPlatformInterface = + pubspec.name.endsWith('_platform_interface'); + // TODO(stuartmorgan): Relax this check. See + // https://github.com/flutter/flutter/issues/85391 + if (isPlatformInterface && + allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR) { + printError('${indentation}Breaking change detected.\n' + '${indentation}Breaking changes to platform interfaces are strongly discouraged.\n'); + return false; + } + return true; } /// Returns whether or not the pubspec version and CHANGELOG version for /// [plugin] match. - Future _checkVersionsMatch(Directory plugin) async { - // get version from pubspec - final String packageName = plugin.basename; - print('-----------------------------------------'); - print( - 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for $packageName.'); - - final Pubspec? pubspec = _tryParsePubspec(plugin); - if (pubspec == null) { - printError('Cannot parse version from pubspec.yaml'); - return false; - } - final Version? fromPubspec = pubspec.version; + Future _hasConsistentVersion( + Directory package, { + required Pubspec pubspec, + }) async { + // This method isn't called unless `version` is non-null. + final Version fromPubspec = pubspec.version!; // get first version from CHANGELOG - final File changelog = plugin.childFile('CHANGELOG.md'); + final File changelog = package.childFile('CHANGELOG.md'); final List lines = changelog.readAsLinesSync(); String? firstLineWithText; final Iterator iterator = lines.iterator; @@ -285,7 +287,8 @@ $indentation${mismatchedVersionPlugins.join('\n$indentation')} // changes that don't warrant publishing on their own. final bool hasNextSection = versionString == 'NEXT'; if (hasNextSection) { - print('Found NEXT; validating next version in the CHANGELOG.'); + print( + '${indentation}Found NEXT; validating next version in the CHANGELOG.'); // Ensure that the version in pubspec hasn't changed without updating // CHANGELOG. That means the next version entry in the CHANGELOG pass the // normal validation. @@ -301,15 +304,15 @@ $indentation${mismatchedVersionPlugins.join('\n$indentation')} versionString == null ? null : Version.parse(versionString); if (fromChangeLog == null) { printError( - 'Cannot find version on the first line of ${plugin.path}/CHANGELOG.md'); + '${indentation}Cannot find version on the first line CHANGELOG.md'); return false; } if (fromPubspec != fromChangeLog) { printError(''' -versions for $packageName in CHANGELOG.md and pubspec.yaml do not match. -The version in pubspec.yaml is $fromPubspec. -The first version listed in CHANGELOG.md is $fromChangeLog. +${indentation}Versions in CHANGELOG.md and pubspec.yaml do not match. +${indentation}The version in pubspec.yaml is $fromPubspec. +${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. '''); return false; } @@ -318,15 +321,13 @@ The first version listed in CHANGELOG.md is $fromChangeLog. if (!hasNextSection) { final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); if (lines.any((String line) => nextRegex.hasMatch(line))) { - printError(''' -When bumping the version for release, the NEXT section should be incorporated -into the new version's release notes. -'''); + printError('${indentation}When bumping the version for release, the ' + 'NEXT section should be incorporated into the new version\'s ' + 'release notes.'); return false; } } - print('$packageName passed version check'); return true; } @@ -337,9 +338,8 @@ into the new version's release notes. final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); return pubspec; } on Exception catch (exception) { - printError( - 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}'); + printError('${indentation}Failed to parse `pubspec.yaml`: $exception}'); + return null; } - return null; } } diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_command_test.dart similarity index 70% rename from script/tool/test/version_check_test.dart rename to script/tool/test/version_check_command_test.dart index 6035360a221..4d884692046 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -29,7 +29,7 @@ void testAllowedVersion( final Version master = Version.parse(masterVersion); final Version head = Version.parse(headVersion); final Map allowedVersions = - getAllowedNextVersions(masterVersion: master, headVersion: head); + getAllowedNextVersions(master, newVersion: head); if (allowed) { expect(allowedVersions, contains(head)); if (nextVersionType != null) { @@ -42,14 +42,6 @@ void testAllowedVersion( class MockProcessResult extends Mock implements io.ProcessResult {} -const String _redColorMessagePrefix = '\x1B[31m'; -const String _redColorMessagePostfix = '\x1B[0m'; - -// Some error message was printed in a "Colorized" red message. So `\x1B[31m` and `\x1B[0m` needs to be included. -String _redColorString(String string) { - return '$_redColorMessagePrefix$string$_redColorMessagePostfix'; -} - void main() { const String indentation = ' '; group('$VersionCheckCommand', () { @@ -58,7 +50,6 @@ void main() { late CommandRunner runner; late RecordingProcessRunner processRunner; late List> gitDirCommands; - String gitDiffResponse; Map gitShowResponses; late MockGitDir gitDir; @@ -66,17 +57,14 @@ void main() { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); gitDirCommands = >[]; - gitDiffResponse = ''; gitShowResponses = {}; gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); 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] == 'show') { + if (invocation.positionalArguments[0][0] == 'show') { final String? response = gitShowResponses[invocation.positionalArguments[0][1]]; if (response == null) { @@ -100,39 +88,32 @@ void main() { }); test('allows valid version', () async { - const String newVersion = '2.0.0'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); expect( output, - containsAllInOrder([ - 'No version check errors found!', + containsAllInOrder([ + contains('Running for plugin'), + contains('1.0.0 -> 2.0.0'), ]), ); - expect(gitDirCommands.length, equals(3)); + expect(gitDirCommands.length, equals(1)); expect( gitDirCommands, containsAll([ - equals(['diff', '--name-only', 'master', 'HEAD']), equals(['show', 'master:packages/plugin/pubspec.yaml']), - equals(['show', 'HEAD:packages/plugin/pubspec.yaml']), ])); }); test('denies invalid version', () async { - const String newVersion = '0.2.0'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '0.2.0'); gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final Future> result = runCapturingPrint( runner, ['version-check', '--base-sha=master']); @@ -141,80 +122,66 @@ void main() { result, throwsA(const TypeMatcher()), ); - expect(gitDirCommands.length, equals(3)); + expect(gitDirCommands.length, equals(1)); expect( gitDirCommands, containsAll([ - equals(['diff', '--name-only', 'master', 'HEAD']), equals(['show', 'master:packages/plugin/pubspec.yaml']), - equals(['show', 'HEAD:packages/plugin/pubspec.yaml']), ])); }); test('allows valid version without explicit base-sha', () async { - const String newVersion = '2.0.0'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List output = await runCapturingPrint(runner, ['version-check']); expect( output, - containsAllInOrder([ - 'No version check errors found!', + containsAllInOrder([ + contains('Running for plugin'), + contains('1.0.0 -> 2.0.0'), ]), ); }); test('allows valid version for new package.', () async { - const String newVersion = '1.0.0'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; - gitShowResponses = { - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', - }; + createFakePlugin('plugin', packagesDir, version: '1.0.0'); final List output = await runCapturingPrint(runner, ['version-check']); expect( output, - containsAllInOrder([ - '${indentation}Unable to find pubspec in master. Safe to ignore if the project is new.', - 'No version check errors found!', + containsAllInOrder([ + contains('Running for plugin'), + contains('Unable to find previous version at git base.'), ]), ); }); test('allows likely reverts.', () async { - const String newVersion = '0.6.1'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '0.6.1'); gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List output = await runCapturingPrint(runner, ['version-check']); expect( output, - containsAllInOrder([ - '${indentation}New version is lower than previous version. This is assumed to be a revert.', + containsAllInOrder([ + contains('New version is lower than previous version. ' + 'This is assumed to be a revert.'), ]), ); }); test('denies lower version that could not be a simple revert', () async { - const String newVersion = '0.5.1'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '0.5.1'); gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final Future> result = runCapturingPrint(runner, ['version-check']); @@ -226,12 +193,9 @@ void main() { }); test('denies invalid version without explicit base-sha', () async { - const String newVersion = '0.2.0'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '0.2.0'); gitShowResponses = { 'abc123:packages/plugin/pubspec.yaml': 'version: 0.0.1', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final Future> result = runCapturingPrint(runner, ['version-check']); @@ -242,73 +206,39 @@ void main() { ); }); - test('gracefully handles missing pubspec.yaml', () async { - final Directory pluginDir = - createFakePlugin('plugin', packagesDir, examples: []); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; - pluginDir.childFile('pubspec.yaml').deleteSync(); - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); - - expect( - output, - orderedEquals([ - 'Determine diff with base sha: master', - 'Checking versions for packages/plugin/pubspec.yaml...', - ' Deleted; skipping.', - 'No version check errors found!', - ]), - ); - expect(gitDirCommands.length, equals(1)); - expect(gitDirCommands.first.join(' '), - equals('diff --name-only master HEAD')); - }); - test('allows minor changes to platform interfaces', () async { - const String newVersion = '1.1.0'; createFakePlugin('plugin_platform_interface', packagesDir, - version: newVersion); - gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; + version: '1.1.0'); gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: $newVersion', }; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); expect( output, - containsAllInOrder([ - 'No version check errors found!', + containsAllInOrder([ + contains('Running for plugin'), + contains('1.0.0 -> 1.1.0'), ]), ); - expect(gitDirCommands.length, equals(3)); + expect(gitDirCommands.length, equals(1)); expect( gitDirCommands, containsAll([ - equals(['diff', '--name-only', 'master', 'HEAD']), equals([ 'show', 'master:packages/plugin_platform_interface/pubspec.yaml' ]), - equals([ - 'show', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml' - ]), ])); }); test('disallows breaking changes to platform interfaces', () async { - const String newVersion = '2.0.0'; createFakePlugin('plugin_platform_interface', packagesDir, - version: newVersion); - gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; + version: '2.0.0'); gitShowResponses = { 'master:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml': - 'version: $newVersion', }; final Future> output = runCapturingPrint( runner, ['version-check', '--base-sha=master']); @@ -316,19 +246,14 @@ void main() { output, throwsA(const TypeMatcher()), ); - expect(gitDirCommands.length, equals(3)); + expect(gitDirCommands.length, equals(1)); expect( gitDirCommands, containsAll([ - equals(['diff', '--name-only', 'master', 'HEAD']), equals([ 'show', 'master:packages/plugin_platform_interface/pubspec.yaml' ]), - equals([ - 'show', - 'HEAD:packages/plugin_platform_interface/pubspec.yaml' - ]), ])); }); @@ -338,6 +263,7 @@ void main() { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' + ## $version * Some changes. '''; @@ -346,10 +272,8 @@ void main() { runner, ['version-check', '--base-sha=master']); expect( output, - containsAllInOrder([ - 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for plugin.', - 'plugin passed version check', - 'No version check errors found!' + containsAllInOrder([ + contains('Running for plugin'), ]), ); }); @@ -375,12 +299,8 @@ void main() { expect( output, - containsAllInOrder([ - _redColorString(''' -versions for plugin in CHANGELOG.md and pubspec.yaml do not match. -The version in pubspec.yaml is 1.0.1. -The first version listed in CHANGELOG.md is 1.0.2. -'''), + containsAllInOrder([ + contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), ]), ); }); @@ -399,10 +319,8 @@ The first version listed in CHANGELOG.md is 1.0.2. runner, ['version-check', '--base-sha=master']); expect( output, - containsAllInOrder([ - 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for plugin.', - 'plugin passed version check', - 'No version check errors found!' + containsAllInOrder([ + contains('Running for plugin'), ]), ); }); @@ -433,14 +351,8 @@ The first version listed in CHANGELOG.md is 1.0.2. expect( output, - containsAllInOrder([ - _redColorString( - ''' -versions for plugin in CHANGELOG.md and pubspec.yaml do not match. -The version in pubspec.yaml is 1.0.0. -The first version listed in CHANGELOG.md is 1.0.1. -''', - ) + containsAllInOrder([ + contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), ]), ); }); @@ -462,10 +374,9 @@ The first version listed in CHANGELOG.md is 1.0.1. runner, ['version-check', '--base-sha=master']); await expectLater( output, - containsAllInOrder([ - 'Found NEXT; validating next version in the CHANGELOG.', - 'plugin passed version check', - 'No version check errors found!', + containsAllInOrder([ + contains('Running for plugin'), + contains('Found NEXT; validating next version in the CHANGELOG.'), ]), ); }); @@ -498,13 +409,9 @@ The first version listed in CHANGELOG.md is 1.0.1. expect( output, - containsAllInOrder([ - _redColorString( - ''' -When bumping the version for release, the NEXT section should be incorporated -into the new version's release notes. -''', - ) + containsAllInOrder([ + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.') ]), ); }); @@ -533,15 +440,9 @@ into the new version's release notes. expect( output, - containsAllInOrder([ - 'Found NEXT; validating next version in the CHANGELOG.', - _redColorString( - ''' -versions for plugin in CHANGELOG.md and pubspec.yaml do not match. -The version in pubspec.yaml is 1.0.1. -The first version listed in CHANGELOG.md is 1.0.0. -''', - ) + containsAllInOrder([ + contains('Found NEXT; validating next version in the CHANGELOG.'), + contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), ]), ); }); @@ -565,21 +466,17 @@ The first version listed in CHANGELOG.md is 1.0.0. 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - const String newVersion = '2.0.0'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List output = await runCapturingPrint(runner, ['version-check', '--base-sha=master', '--against-pub']); expect( output, - containsAllInOrder([ - '${indentation}plugin: Current largest version on pub: 1.0.0', - 'No version check errors found!', + containsAllInOrder([ + contains('plugin: Current largest version on pub: 1.0.0'), ]), ); }); @@ -602,12 +499,9 @@ The first version listed in CHANGELOG.md is 1.0.0. 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - const String newVersion = '2.0.0'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; bool hasError = false; @@ -623,13 +517,11 @@ The first version listed in CHANGELOG.md is 1.0.0. expect( result, - containsAllInOrder([ - _redColorString( - ''' + containsAllInOrder([ + contains(''' ${indentation}Incorrectly updated version. ${indentation}HEAD: 2.0.0, pub: 0.0.2. -${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: NextVersionType.MINOR, 0.0.3: NextVersionType.PATCH}''', - ) +${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: NextVersionType.MINOR, 0.0.3: NextVersionType.PATCH}''') ]), ); }); @@ -647,12 +539,9 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - const String newVersion = '2.0.0'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; bool hasError = false; final List result = await runCapturingPrint(runner, [ @@ -667,14 +556,12 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N expect( result, - containsAllInOrder([ - _redColorString( - ''' + containsAllInOrder([ + contains(''' ${indentation}Error fetching version on pub for plugin. ${indentation}HTTP Status 400 ${indentation}HTTP response: xx -''', - ) +''') ]), ); }); @@ -691,21 +578,17 @@ ${indentation}HTTP response: xx 'version_check_command', 'Test for $VersionCheckCommand'); runner.addCommand(command); - const String newVersion = '2.0.0'; - createFakePlugin('plugin', packagesDir, version: newVersion); - gitDiffResponse = 'packages/plugin/pubspec.yaml'; + createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - 'HEAD:packages/plugin/pubspec.yaml': 'version: $newVersion', }; final List result = await runCapturingPrint(runner, ['version-check', '--base-sha=master', '--against-pub']); expect( result, - containsAllInOrder([ - '${indentation}Unable to find package on pub server. Safe to ignore if the project is new.', - 'No version check errors found!', + containsAllInOrder([ + contains('Unable to find previous version on pub server.'), ]), ); }); From 6ccb3441193c2e42832d3596e41a1ab441b31734 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 30 Jun 2021 11:21:05 -0700 Subject: [PATCH 081/249] [flutter_plugin_tools] Move license-check tests to runCapturingPrint (#4107) --- .../tool/lib/src/license_check_command.dart | 40 ++--- .../tool/test/license_check_command_test.dart | 170 ++++++++++-------- 2 files changed, 114 insertions(+), 96 deletions(-) diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 4ea8a1e0939..1d3e49c6a7c 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -96,13 +96,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /// Validates that code files have copyright and license blocks. class LicenseCheckCommand extends PluginCommand { /// Creates a new license check command for [packagesDir]. - LicenseCheckCommand( - Directory packagesDir, { - Print print = print, - }) : _print = print, - super(packagesDir); - - final Print _print; + LicenseCheckCommand(Directory packagesDir) : super(packagesDir); @override final String name = 'license-check'; @@ -121,7 +115,7 @@ class LicenseCheckCommand extends PluginCommand { p.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); final bool copyrightCheckSucceeded = await _checkCodeLicenses(codeFiles); - _print('\n=======================================\n'); + print('\n=======================================\n'); final bool licenseCheckSucceeded = await _checkLicenseFiles(firstPartyLicenseFiles); @@ -157,7 +151,7 @@ class LicenseCheckCommand extends PluginCommand { }; for (final File file in codeFiles) { - _print('Checking ${file.path}'); + print('Checking ${file.path}'); final String content = await file.readAsString(); final String firstParyLicense = @@ -177,7 +171,7 @@ class LicenseCheckCommand extends PluginCommand { } } } - _print('\n'); + print('\n'); // Sort by path for more usable output. final int Function(File, File) pathCompare = @@ -186,22 +180,22 @@ class LicenseCheckCommand extends PluginCommand { unrecognizedThirdPartyFiles.sort(pathCompare); if (incorrectFirstPartyFiles.isNotEmpty) { - _print('The license block for these files is missing or incorrect:'); + print('The license block for these files is missing or incorrect:'); for (final File file in incorrectFirstPartyFiles) { - _print(' ${file.path}'); + print(' ${file.path}'); } - _print('If this third-party code, move it to a "third_party/" directory, ' + print('If this third-party code, move it to a "third_party/" directory, ' 'otherwise ensure that you are using the exact copyright and license ' 'text used by all first-party files in this repository.\n'); } if (unrecognizedThirdPartyFiles.isNotEmpty) { - _print( + print( 'No recognized license was found for the following third-party files:'); for (final File file in unrecognizedThirdPartyFiles) { - _print(' ${file.path}'); + print(' ${file.path}'); } - _print('Please check that they have a license at the top of the file. ' + print('Please check that they have a license at the top of the file. ' 'If they do, the license check needs to be updated to recognize ' 'the new third-party license block.\n'); } @@ -209,7 +203,7 @@ class LicenseCheckCommand extends PluginCommand { final bool succeeded = incorrectFirstPartyFiles.isEmpty && unrecognizedThirdPartyFiles.isEmpty; if (succeeded) { - _print('All source files passed validation!'); + print('All source files passed validation!'); } return succeeded; } @@ -220,25 +214,25 @@ class LicenseCheckCommand extends PluginCommand { final List incorrectLicenseFiles = []; for (final File file in files) { - _print('Checking ${file.path}'); + print('Checking ${file.path}'); if (!file.readAsStringSync().contains(_fullBsdLicenseText)) { incorrectLicenseFiles.add(file); } } - _print('\n'); + print('\n'); if (incorrectLicenseFiles.isNotEmpty) { - _print('The following LICENSE files do not follow the expected format:'); + print('The following LICENSE files do not follow the expected format:'); for (final File file in incorrectLicenseFiles) { - _print(' ${file.path}'); + print(' ${file.path}'); } - _print( + print( 'Please ensure that they use the exact format used in this repository".\n'); } final bool succeeded = incorrectLicenseFiles.isEmpty; if (succeeded) { - _print('All LICENSE files passed validation!'); + print('All LICENSE files passed validation!'); } return succeeded; } diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index dfe8d25197a..64adc9214d8 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -9,11 +9,12 @@ import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/license_check_command.dart'; import 'package:test/test.dart'; +import 'util.dart'; + void main() { group('$LicenseCheckCommand', () { late CommandRunner runner; late FileSystem fileSystem; - late List printedMessages; late Directory root; setUp(() { @@ -22,10 +23,8 @@ void main() { fileSystem.currentDirectory.childDirectory('packages'); root = packagesDir.parent; - printedMessages = []; final LicenseCheckCommand command = LicenseCheckCommand( packagesDir, - print: (Object? message) => printedMessages.add(message.toString()), ); runner = CommandRunner('license_test', 'Test for $LicenseCheckCommand'); @@ -81,18 +80,16 @@ void main() { root.childFile('$filenameBase.$fileExtension').createSync(); } - try { - await runner.run(['license-check']); - } on ToolExit { + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { // Ignore failure; the files are empty so the check is expected to fail, // but this test isn't for that behavior. - } + }); extensions.forEach((String fileExtension, bool shouldCheck) { final Matcher logLineMatcher = contains('Checking $filenameBase.$fileExtension'); - expect(printedMessages, - shouldCheck ? logLineMatcher : isNot(logLineMatcher)); + expect(output, shouldCheck ? logLineMatcher : isNot(logLineMatcher)); }); }); @@ -115,10 +112,11 @@ void main() { root.childFile(name).createSync(); } - await runner.run(['license-check']); + final List output = + await runCapturingPrint(runner, ['license-check']); for (final String name in ignoredFiles) { - expect(printedMessages, isNot(contains('Checking $name'))); + expect(output, isNot(contains('Checking $name'))); } }); @@ -129,11 +127,12 @@ void main() { final File notChecked = root.childFile('not_checked.md'); notChecked.createSync(); - await runner.run(['license-check']); + final List output = + await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check a file. - expect(printedMessages, contains('Checking checked.cc')); - expect(printedMessages, contains('All source files passed validation!')); + expect(output, contains('Checking checked.cc')); + expect(output, contains('All source files passed validation!')); }); test('handles the comment styles for all supported languages', () async { @@ -147,13 +146,14 @@ void main() { fileC.createSync(); _writeLicense(fileC, comment: '', prefix: ''); - await runner.run(['license-check']); + final List output = + await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the files. - expect(printedMessages, contains('Checking file_a.cc')); - expect(printedMessages, contains('Checking file_b.sh')); - expect(printedMessages, contains('Checking file_c.html')); - expect(printedMessages, contains('All source files passed validation!')); + expect(output, contains('Checking file_a.cc')); + expect(output, contains('Checking file_b.sh')); + expect(output, contains('Checking file_c.html')); + expect(output, contains('All source files passed validation!')); }); test('fails if any checked files are missing license blocks', () async { @@ -166,19 +166,22 @@ void main() { root.childFile('bad.cc').createSync(); root.childFile('bad.h').createSync(); - await expectLater(() => runner.run(['license-check']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); // Failure should give information about the problematic files. expect( - printedMessages, + output, contains( 'The license block for these files is missing or incorrect:')); - expect(printedMessages, contains(' bad.cc')); - expect(printedMessages, contains(' bad.h')); + expect(output, contains(' bad.cc')); + expect(output, contains(' bad.h')); // Failure shouldn't print the success message. - expect(printedMessages, - isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains('All source files passed validation!'))); }); test('fails if any checked files are missing just the copyright', () async { @@ -189,18 +192,21 @@ void main() { bad.createSync(); _writeLicense(bad, copyright: ''); - await expectLater(() => runner.run(['license-check']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); // Failure should give information about the problematic files. expect( - printedMessages, + output, contains( 'The license block for these files is missing or incorrect:')); - expect(printedMessages, contains(' bad.cc')); + expect(output, contains(' bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, - isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains('All source files passed validation!'))); }); test('fails if any checked files are missing just the license', () async { @@ -211,18 +217,21 @@ void main() { bad.createSync(); _writeLicense(bad, license: []); - await expectLater(() => runner.run(['license-check']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); // Failure should give information about the problematic files. expect( - printedMessages, + output, contains( 'The license block for these files is missing or incorrect:')); - expect(printedMessages, contains(' bad.cc')); + expect(output, contains(' bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, - isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains('All source files passed validation!'))); }); test('fails if any third-party code is not in a third_party directory', @@ -231,18 +240,21 @@ void main() { thirdPartyFile.createSync(); _writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); - await expectLater(() => runner.run(['license-check']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); // Failure should give information about the problematic files. expect( - printedMessages, + output, contains( 'The license block for these files is missing or incorrect:')); - expect(printedMessages, contains(' third_party.cc')); + expect(output, contains(' third_party.cc')); // Failure shouldn't print the success message. - expect(printedMessages, - isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains('All source files passed validation!'))); }); test('succeeds for third-party code in a third_party directory', () async { @@ -260,12 +272,12 @@ void main() { 'you may not use this file except in compliance with the License.' ]); - await runner.run(['license-check']); + final List output = + await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the file. - expect(printedMessages, - contains('Checking a_plugin/lib/src/third_party/file.cc')); - expect(printedMessages, contains('All source files passed validation!')); + expect(output, contains('Checking a_plugin/lib/src/third_party/file.cc')); + expect(output, contains('All source files passed validation!')); }); test('allows first-party code in a third_party directory', () async { @@ -278,12 +290,13 @@ void main() { firstPartyFileInThirdParty.createSync(recursive: true); _writeLicense(firstPartyFileInThirdParty); - await runner.run(['license-check']); + final List output = + await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the file. - expect(printedMessages, + expect(output, contains('Checking a_plugin/lib/src/third_party/first_party.cc')); - expect(printedMessages, contains('All source files passed validation!')); + expect(output, contains('All source files passed validation!')); }); test('fails for licenses that the tool does not expect', () async { @@ -297,18 +310,21 @@ void main() { 'it under the terms of the GNU General Public License', ]); - await expectLater(() => runner.run(['license-check']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); // Failure should give information about the problematic files. expect( - printedMessages, + output, contains( 'No recognized license was found for the following third-party files:')); - expect(printedMessages, contains(' third_party/bad.cc')); + expect(output, contains(' third_party/bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, - isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains('All source files passed validation!'))); }); test('Apache is not recognized for new authors without validation changes', @@ -327,18 +343,21 @@ void main() { ], ); - await expectLater(() => runner.run(['license-check']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); // Failure should give information about the problematic files. expect( - printedMessages, + output, contains( 'No recognized license was found for the following third-party files:')); - expect(printedMessages, contains(' third_party/bad.cc')); + expect(output, contains(' third_party/bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, - isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains('All source files passed validation!'))); }); test('passes if all first-party LICENSE files are correctly formatted', @@ -347,11 +366,12 @@ void main() { license.createSync(); license.writeAsStringSync(_correctLicenseFileText); - await runner.run(['license-check']); + final List output = + await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the file. - expect(printedMessages, contains('Checking LICENSE')); - expect(printedMessages, contains('All LICENSE files passed validation!')); + expect(output, contains('Checking LICENSE')); + expect(output, contains('All LICENSE files passed validation!')); }); test('fails if any first-party LICENSE files are incorrectly formatted', @@ -360,11 +380,14 @@ void main() { license.createSync(); license.writeAsStringSync(_incorrectLicenseFileText); - await expectLater(() => runner.run(['license-check']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); - expect(printedMessages, - isNot(contains('All LICENSE files passed validation!'))); + expect(commandError, isA()); + expect(output, isNot(contains('All LICENSE files passed validation!'))); }); test('ignores third-party LICENSE format', () async { @@ -373,11 +396,12 @@ void main() { license.createSync(recursive: true); license.writeAsStringSync(_incorrectLicenseFileText); - await runner.run(['license-check']); + final List output = + await runCapturingPrint(runner, ['license-check']); // The file shouldn't be checked. - expect(printedMessages, isNot(contains('Checking third_party/LICENSE'))); - expect(printedMessages, contains('All LICENSE files passed validation!')); + expect(output, isNot(contains('Checking third_party/LICENSE'))); + expect(output, contains('All LICENSE files passed validation!')); }); }); } From ea72f74d0bc75710b7844eee0a558b4100ae8560 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 30 Jun 2021 11:33:09 -0700 Subject: [PATCH 082/249] [flutter_plugin_tools] Migrate build-examples to new base command (#4087) Switches build-examples to the new base command that handles the boilerplate of looping over target packages. While modifying the command, also does some minor cleanup: - Extracts a helper to reduce duplicated details of calling `flutter build` - Switches the flag for iOS to `--ios` rather than `--ipa` since `ios` is what is actually passed to the build command - iOS no longer defaults to on, so that it behaves like all the other platform flags - Passing no platform flags is now an error rather than a silent pass, to ensure that we never accidentally have CI doing a no-op run without noticing. - Rewords the logging slightly for the versions where the label for what is being built is a platform, not an artifact (which is now everything but Android). Part of flutter/flutter#83413 --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/build_examples_command.dart | 241 ++++++++---------- .../test/build_examples_command_test.dart | 146 +++++------ 3 files changed, 159 insertions(+), 230 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 94514e38103..a2716cb53d3 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,8 @@ compatibility. - `xctest` now supports running macOS tests in addition to iOS - **Breaking change**: it now requires an `--ios` and/or `--macos` flag. +- **Breaking change**: `build-examples` for iOS now uses `--ios` rather than + `--ipa`. - The tooling now runs in strong null-safe mode. - `publish plugins` check against pub.dev to determine if a release should happen. - Modified the output format of many commands diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index aff5ecba498..c8280f4e867 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -3,36 +3,34 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io' as io; import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; -/// Key for IPA. -const String kIpa = 'ipa'; - /// Key for APK. -const String kApk = 'apk'; +const String _platformFlagApk = 'apk'; + +const int _exitNoPlatformFlags = 2; /// A command to build the example applications for packages. -class BuildExamplesCommand extends PluginCommand { +class BuildExamplesCommand extends PackageLoopingCommand { /// Creates an instance of the build command. BuildExamplesCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), }) : super(packagesDir, processRunner: processRunner) { - argParser.addFlag(kPlatformLinux, defaultsTo: false); - argParser.addFlag(kPlatformMacos, defaultsTo: false); - argParser.addFlag(kPlatformWeb, defaultsTo: false); - argParser.addFlag(kPlatformWindows, defaultsTo: false); - argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); - argParser.addFlag(kApk); + argParser.addFlag(kPlatformLinux); + argParser.addFlag(kPlatformMacos); + argParser.addFlag(kPlatformWeb); + argParser.addFlag(kPlatformWindows); + argParser.addFlag(kPlatformIos); + argParser.addFlag(_platformFlagApk); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -49,164 +47,125 @@ class BuildExamplesCommand extends PluginCommand { 'This command requires "flutter" to be in your path.'; @override - Future run() async { + Future initializeRun() async { final List platformSwitches = [ - kApk, - kIpa, + _platformFlagApk, + kPlatformIos, kPlatformLinux, kPlatformMacos, kPlatformWeb, kPlatformWindows, ]; if (!platformSwitches.any((String platform) => getBoolArg(platform))) { - print( + printError( 'None of ${platformSwitches.map((String platform) => '--$platform').join(', ')} ' - 'were specified, so not building anything.'); - return; + 'were specified. At least one platform must be provided.'); + throw ToolExit(_exitNoPlatformFlags); } - final String flutterCommand = - const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - - final String enableExperiment = getStringArg(kEnableExperiment); + } - final List failingPackages = []; - await for (final Directory plugin in getPlugins()) { - for (final Directory example in getExamplesForPlugin(plugin)) { - final String packageName = - p.relative(example.path, from: packagesDir.path); - - if (getBoolArg(kPlatformLinux)) { - print('\nBUILDING Linux for $packageName'); - if (isLinuxPlugin(plugin)) { - final int buildExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kPlatformLinux, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (buildExitCode != 0) { - failingPackages.add('$packageName (linux)'); - } - } else { - print('Linux is not supported by this plugin'); + @override + Future> runForPackage(Directory package) async { + final List errors = []; + + for (final Directory example in getExamplesForPlugin(package)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + + if (getBoolArg(kPlatformLinux)) { + print('\nBUILDING $packageName for Linux'); + if (isLinuxPlugin(package)) { + if (!await _buildExample(example, kPlatformLinux)) { + errors.add('$packageName (Linux)'); } + } else { + printSkip('Linux is not supported by this plugin'); } + } - if (getBoolArg(kPlatformMacos)) { - print('\nBUILDING macOS for $packageName'); - if (isMacOsPlugin(plugin)) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kPlatformMacos, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (macos)'); - } - } else { - print('macOS is not supported by this plugin'); + if (getBoolArg(kPlatformMacos)) { + print('\nBUILDING $packageName for macOS'); + if (isMacOsPlugin(package)) { + if (!await _buildExample(example, kPlatformMacos)) { + errors.add('$packageName (macOS)'); } + } else { + printSkip('macOS is not supported by this plugin'); } + } - if (getBoolArg(kPlatformWeb)) { - print('\nBUILDING web for $packageName'); - if (isWebPlugin(plugin)) { - final int buildExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kPlatformWeb, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (buildExitCode != 0) { - failingPackages.add('$packageName (web)'); - } - } else { - print('Web is not supported by this plugin'); + if (getBoolArg(kPlatformWeb)) { + print('\nBUILDING $packageName for web'); + if (isWebPlugin(package)) { + if (!await _buildExample(example, kPlatformWeb)) { + errors.add('$packageName (web)'); } + } else { + printSkip('Web is not supported by this plugin'); } + } - if (getBoolArg(kPlatformWindows)) { - print('\nBUILDING Windows for $packageName'); - if (isWindowsPlugin(plugin)) { - final int buildExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - kPlatformWindows, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (buildExitCode != 0) { - failingPackages.add('$packageName (windows)'); - } - } else { - print('Windows is not supported by this plugin'); + if (getBoolArg(kPlatformWindows)) { + print('\nBUILDING $packageName for Windows'); + if (isWindowsPlugin(package)) { + if (!await _buildExample(example, kPlatformWindows)) { + errors.add('$packageName (Windows)'); } + } else { + printSkip('Windows is not supported by this plugin'); } + } - if (getBoolArg(kIpa)) { - print('\nBUILDING IPA for $packageName'); - if (isIosPlugin(plugin)) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'ios', - '--no-codesign', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (ipa)'); - } - } else { - print('iOS is not supported by this plugin'); + if (getBoolArg(kPlatformIos)) { + print('\nBUILDING $packageName for iOS'); + if (isIosPlugin(package)) { + if (!await _buildExample( + example, + kPlatformIos, + extraBuildFlags: ['--no-codesign'], + )) { + errors.add('$packageName (iOS)'); } + } else { + printSkip('iOS is not supported by this plugin'); } + } - if (getBoolArg(kApk)) { - print('\nBUILDING APK for $packageName'); - if (isAndroidPlugin(plugin)) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'apk', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example); - if (exitCode != 0) { - failingPackages.add('$packageName (apk)'); - } - } else { - print('Android is not supported by this plugin'); + if (getBoolArg(_platformFlagApk)) { + print('\nBUILDING APK for $packageName'); + if (isAndroidPlugin(package)) { + if (!await _buildExample(example, _platformFlagApk)) { + errors.add('$packageName (apk)'); } + } else { + printSkip('Android is not supported by this plugin'); } } } - print('\n\n'); - if (failingPackages.isNotEmpty) { - print('The following build are failing (see above for details):'); - for (final String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); - } + return errors; + } - print('All builds successful!'); + Future _buildExample( + Directory example, + String flutterBuildType, { + List extraBuildFlags = const [], + }) async { + final String flutterCommand = + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + final String enableExperiment = getStringArg(kEnableExperiment); + + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + flutterBuildType, + ...extraBuildFlags, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example, + ); + return exitCode == 0; } } diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 7fc97838c0e..c6febdc26fb 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -15,7 +15,7 @@ import 'package:test/test.dart'; import 'util.dart'; void main() { - group('test build_example_command', () { + group('build-example', () { late FileSystem fileSystem; late Directory packagesDir; late CommandRunner runner; @@ -35,6 +35,13 @@ void main() { runner.addCommand(command); }); + test('fails if no plaform flags are passed', () async { + expect( + () => runCapturingPrint(runner, ['build-examples']), + throwsA(isA()), + ); + }); + test('building for iOS when plugin is not set up for iOS results in no-op', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, @@ -43,18 +50,16 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint( - runner, ['build-examples', '--ipa', '--no-macos']); + final List output = + await runCapturingPrint(runner, ['build-examples', '--ios']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING IPA for $packageName', - 'iOS is not supported by this plugin', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + contains('BUILDING $packageName for iOS'), + contains('iOS is not supported by this plugin'), ]), ); @@ -78,21 +83,15 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint(runner, [ - 'build-examples', - '--ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); + final List output = await runCapturingPrint(runner, + ['build-examples', '--ios', '--enable-experiment=exp1']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING IPA for $packageName', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + '\nBUILDING $packageName for iOS', ]), ); @@ -123,17 +122,15 @@ void main() { pluginDirectory.childDirectory('example'); final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--linux']); + runner, ['build-examples', '--linux']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING Linux for $packageName', - 'Linux is not supported by this plugin', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + contains('BUILDING $packageName for Linux'), + contains('Linux is not supported by this plugin'), ]), ); @@ -158,16 +155,14 @@ void main() { pluginDirectory.childDirectory('example'); final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--linux']); + runner, ['build-examples', '--linux']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING Linux for $packageName', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + '\nBUILDING $packageName for Linux', ]), ); @@ -190,17 +185,15 @@ void main() { pluginDirectory.childDirectory('example'); final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--macos']); + runner, ['build-examples', '--macos']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING macOS for $packageName', - 'macOS is not supported by this plugin', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + contains('BUILDING $packageName for macOS'), + contains('macOS is not supported by this plugin'), ]), ); @@ -226,16 +219,14 @@ void main() { pluginDirectory.childDirectory('example'); final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--macos']); + runner, ['build-examples', '--macos']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING macOS for $packageName', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + '\nBUILDING $packageName for macOS', ]), ); @@ -256,18 +247,16 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--web']); + final List output = + await runCapturingPrint(runner, ['build-examples', '--web']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING web for $packageName', - 'Web is not supported by this plugin', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + contains('BUILDING $packageName for web'), + contains('Web is not supported by this plugin'), ]), ); @@ -292,17 +281,15 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--web']); + final List output = + await runCapturingPrint(runner, ['build-examples', '--web']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING web for $packageName', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + '\nBUILDING $packageName for web', ]), ); @@ -326,17 +313,15 @@ void main() { pluginDirectory.childDirectory('example'); final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--windows']); + runner, ['build-examples', '--windows']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING Windows for $packageName', - 'Windows is not supported by this plugin', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + contains('BUILDING $packageName for Windows'), + contains('Windows is not supported by this plugin'), ]), ); @@ -361,16 +346,14 @@ void main() { pluginDirectory.childDirectory('example'); final List output = await runCapturingPrint( - runner, ['build-examples', '--no-ipa', '--windows']); + runner, ['build-examples', '--windows']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING Windows for $packageName', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + '\nBUILDING $packageName for Windows', ]), ); @@ -393,18 +376,16 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint( - runner, ['build-examples', '--apk', '--no-ipa']); + final List output = + await runCapturingPrint(runner, ['build-examples', '--apk']); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ - '\nBUILDING APK for $packageName', - 'Android is not supported by this plugin', - '\n\n', - 'All builds successful!', + containsAllInOrder([ + contains('\nBUILDING APK for $packageName'), + contains('Android is not supported by this plugin'), ]), ); @@ -431,18 +412,14 @@ void main() { final List output = await runCapturingPrint(runner, [ 'build-examples', '--apk', - '--no-ipa', - '--no-macos', ]); final String packageName = p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, - orderedEquals([ + containsAllInOrder([ '\nBUILDING APK for $packageName', - '\n\n', - 'All builds successful!', ]), ); @@ -469,13 +446,8 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - await runCapturingPrint(runner, [ - 'build-examples', - '--apk', - '--no-ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); + await runCapturingPrint(runner, + ['build-examples', '--apk', '--enable-experiment=exp1']); expect( processRunner.recordedCalls, @@ -502,12 +474,8 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - await runCapturingPrint(runner, [ - 'build-examples', - '--ipa', - '--no-macos', - '--enable-experiment=exp1' - ]); + await runCapturingPrint(runner, + ['build-examples', '--ios', '--enable-experiment=exp1']); expect( processRunner.recordedCalls, orderedEquals([ From ec9233eb415b49ca428257801354a0a758dad78b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 30 Jun 2021 11:43:41 -0700 Subject: [PATCH 083/249] [flutter_plugin_tools] Migrate firebase-test-lab to new base command (#4116) Migrates firebase-test-lab to use the new package-looping base command. Other changes: - Extracts several helpers to make the main flow easier to follow - Removes support for finding and running `*_e2e.dart` files, since we no longer use that file structure for integration tests. Part of https://github.com/flutter/flutter/issues/83413 --- script/tool/CHANGELOG.md | 6 + .../lib/src/firebase_test_lab_command.dart | 285 ++++++++---------- ...rt => firebase_test_lab_command_test.dart} | 181 ++++++----- 3 files changed, 246 insertions(+), 226 deletions(-) rename script/tool/test/{firebase_test_lab_test.dart => firebase_test_lab_command_test.dart} (62%) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a2716cb53d3..2b15ccdd2ac 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## NEXT + +- Modified the output format of many commands +- **Breaking change**: `firebase-test-lab` no longer supports `*_e2e.dart` + files, only `integration_test/*_test.dart`. + ## 0.3.0 - Add a --build-id flag to `firebase-test-lab` instead of hard-coding the use of diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index b4f5e92933c..9f4982b2783 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -10,18 +10,16 @@ import 'package:path/path.dart' as p; import 'package:uuid/uuid.dart'; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; /// A command to run tests via Firebase test lab. -class FirebaseTestLabCommand extends PluginCommand { +class FirebaseTestLabCommand extends PackageLoopingCommand { /// Creates an instance of the test runner command. FirebaseTestLabCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - Print print = print, - }) : _print = print, - super(packagesDir, processRunner: processRunner) { + }) : super(packagesDir, processRunner: processRunner) { argParser.addOption( 'project', defaultsTo: 'flutter-infra', @@ -74,8 +72,6 @@ class FirebaseTestLabCommand extends PluginCommand { static const String _gradleWrapper = 'gradlew'; - final Print _print; - Completer? _firebaseProjectConfigured; Future _configureFirebaseProject() async { @@ -86,7 +82,7 @@ class FirebaseTestLabCommand extends PluginCommand { final String serviceKey = getStringArg('service-key'); if (serviceKey.isEmpty) { - _print('No --service-key provided; skipping gcloud authorization'); + print('No --service-key provided; skipping gcloud authorization'); } else { await processRunner.run( 'gcloud', @@ -105,10 +101,10 @@ class FirebaseTestLabCommand extends PluginCommand { getStringArg('project'), ]); if (exitCode == 0) { - _print('\nFirebase project configured.'); + print('\nFirebase project configured.'); return; } else { - _print( + print( '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); } } @@ -116,172 +112,155 @@ class FirebaseTestLabCommand extends PluginCommand { } @override - Future run() async { - final Stream packagesWithTests = getPackages().where( - (Directory d) => - isFlutterPackage(d) && - d - .childDirectory('example') - .childDirectory('android') - .childDirectory('app') - .childDirectory('src') - .childDirectory('androidTest') - .existsSync()); + Future> runForPackage(Directory package) async { + if (!package + .childDirectory('example') + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('androidTest') + .existsSync()) { + printSkip('No example with androidTest directory'); + return PackageLoopingCommand.success; + } - final List failingPackages = []; - final List missingFlutterBuild = []; - int resultsCounter = - 0; // We use a unique GCS bucket for each Firebase Test Lab run - await for (final Directory package in packagesWithTests) { - // See https://github.com/flutter/flutter/issues/38983 + final List errors = []; - final Directory exampleDirectory = package.childDirectory('example'); - final String packageName = - p.relative(package.path, from: packagesDir.path); - _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); + final Directory exampleDirectory = package.childDirectory('example'); + final Directory androidDirectory = + exampleDirectory.childDirectory('android'); - final Directory androidDirectory = - exampleDirectory.childDirectory('android'); + // Ensures that gradle wrapper exists + if (!await _ensureGradleWrapperExists(androidDirectory)) { + errors.add('Unable to build example apk'); + return errors; + } - final String enableExperiment = getStringArg(kEnableExperiment); - final String encodedEnableExperiment = - Uri.encodeComponent('--enable-experiment=$enableExperiment'); + await _configureFirebaseProject(); - // Ensures that gradle wrapper exists - if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { - final int exitCode = await processRunner.runAndStream( - 'flutter', - [ - 'build', - 'apk', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: androidDirectory); + if (!await _runGradle(androidDirectory, 'app:assembleAndroidTest')) { + errors.add('Unable to assemble androidTest'); + return errors; + } - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } + // Used within the loop to ensure a unique GCS output location for each + // test file's run. + int resultsCounter = 0; + for (final File test in _findIntegrationTestFiles(package)) { + final String testName = p.relative(test.path, from: package.path); + print('Testing $testName...'); + if (!await _runGradle(androidDirectory, 'app:assembleDebug', + testFile: test)) { + printError('Could not build $testName'); + errors.add('$testName failed to build'); continue; } + final String buildId = getStringArg('build-id'); + final String testRunId = getStringArg('test-run-id'); + final String resultsDir = + 'plugins_android_test/${getPackageDescription(package)}/$buildId/$testRunId/${resultsCounter++}/'; + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '5m', + '--results-bucket=${getStringArg('results-bucket')}', + '--results-dir=$resultsDir', + ]; + for (final String device in getStringListArg('device')) { + args.addAll(['--device', device]); + } + final int exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: exampleDirectory); - await _configureFirebaseProject(); + if (exitCode != 0) { + printError('Test failure for $testName'); + errors.add('$testName failed tests'); + } + } + return errors; + } - int exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), + /// Checks that 'gradlew' exists in [androidDirectory], and if not runs a + /// Flutter build to generate it. + /// + /// Returns true if either gradlew was already present, or the build succeeds. + Future _ensureGradleWrapperExists(Directory androidDirectory) async { + if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { + print('Running flutter build apk...'); + final String experiment = getStringArg(kEnableExperiment); + final int exitCode = await processRunner.runAndStream( + 'flutter', [ - 'app:assembleAndroidTest', - '-Pverbose=true', - if (enableExperiment.isNotEmpty) - '-Pextra-front-end-options=$encodedEnableExperiment', - if (enableExperiment.isNotEmpty) - '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + 'build', + 'apk', + if (experiment.isNotEmpty) '--enable-experiment=$experiment', ], workingDir: androidDirectory); if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - - // Look for tests recursively in folders that start with 'test' and that - // live in the root or example folders. - bool isTestDir(FileSystemEntity dir) { - return dir is Directory && - (p.basename(dir.path).startsWith('test') || - p.basename(dir.path) == 'integration_test'); + return false; } + } + return true; + } - final List testDirs = - package.listSync().where(isTestDir).cast().toList(); - final Directory example = package.childDirectory('example'); - testDirs.addAll( - example.listSync().where(isTestDir).cast().toList()); - for (final Directory testDir in testDirs) { - bool isE2ETest(FileSystemEntity file) { - return file.path.endsWith('_e2e.dart') || - (file.parent.basename == 'integration_test' && - file.path.endsWith('_test.dart')); - } - - final List testFiles = testDir - .listSync(recursive: true, followLinks: true) - .where(isE2ETest) - .toList(); - for (final FileSystemEntity test in testFiles) { - exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - [ - 'app:assembleDebug', - '-Pverbose=true', - '-Ptarget=${test.path}', - if (enableExperiment.isNotEmpty) - '-Pextra-front-end-options=$encodedEnableExperiment', - if (enableExperiment.isNotEmpty) - '-Pextra-gen-snapshot-options=$encodedEnableExperiment', - ], - workingDir: androidDirectory); + /// Builds [target] using 'gradlew' in the given [directory]. Assumes + /// 'gradlew' already exists. + /// + /// [testFile] optionally does the Flutter build with the given test file as + /// the build target. + /// + /// Returns true if the command succeeds. + Future _runGradle( + Directory directory, + String target, { + File? testFile, + }) async { + final String experiment = getStringArg(kEnableExperiment); + final String? extraOptions = experiment.isNotEmpty + ? Uri.encodeComponent('--enable-experiment=$experiment') + : null; - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - final String buildId = getStringArg('build-id'); - final String testRunId = getStringArg('test-run-id'); - final String resultsDir = - 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; - final List args = [ - 'firebase', - 'test', - 'android', - 'run', - '--type', - 'instrumentation', - '--app', - 'build/app/outputs/apk/debug/app-debug.apk', - '--test', - 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', - '--timeout', - '5m', - '--results-bucket=${getStringArg('results-bucket')}', - '--results-dir=$resultsDir', - ]; - for (final String device in getStringListArg('device')) { - args.addAll(['--device', device]); - } - exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: exampleDirectory); + final int exitCode = await processRunner.runAndStream( + p.join(directory.path, _gradleWrapper), + [ + target, + '-Pverbose=true', + if (testFile != null) '-Ptarget=${testFile.path}', + if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', + if (extraOptions != null) + '-Pextra-gen-snapshot-options=$extraOptions', + ], + workingDir: directory); - if (exitCode != 0) { - failingPackages.add(packageName); - continue; - } - } - } + if (exitCode != 0) { + return false; } + return true; + } - _print('\n\n'); - if (failingPackages.isNotEmpty) { - _print( - 'The instrumentation tests for the following packages are failing (see above for' - 'details):'); - for (final String package in failingPackages) { - _print(' * $package'); - } - } - if (missingFlutterBuild.isNotEmpty) { - _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' - 'the following packages before executing tests again:'); - for (final String package in missingFlutterBuild) { - _print(' * $package'); - } - } + /// Finds and returns all integration test files for [package]. + Iterable _findIntegrationTestFiles(Directory package) sync* { + final Directory integrationTestDir = + package.childDirectory('example').childDirectory('integration_test'); - if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { - throw ToolExit(1); + if (!integrationTestDir.existsSync()) { + return; } - _print('All Firebase Test Lab tests successful!'); + yield* integrationTestDir + .listSync(recursive: true, followLinks: true) + .where((FileSystemEntity file) => + file is File && file.basename.endsWith('_test.dart')) + .cast(); } } diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_command_test.dart similarity index 62% rename from script/tool/test/firebase_test_lab_test.dart rename to script/tool/test/firebase_test_lab_command_test.dart index 32867c949b4..e317ba924bd 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -18,18 +18,15 @@ void main() { group('$FirebaseTestLabCommand', () { FileSystem fileSystem; late Directory packagesDir; - late List printedMessages; late CommandRunner runner; late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - printedMessages = []; processRunner = RecordingProcessRunner(); - final FirebaseTestLabCommand command = FirebaseTestLabCommand(packagesDir, - processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString())); + final FirebaseTestLabCommand command = + FirebaseTestLabCommand(packagesDir, processRunner: processRunner); runner = CommandRunner( 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); @@ -48,32 +45,31 @@ void main() { 'example/should_not_run_e2e.dart', 'example/android/app/src/androidTest/MainActivityTest.java', ]); - await expectLater( - () => runCapturingPrint(runner, ['firebase-test-lab']), - throwsA(const TypeMatcher())); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['firebase-test-lab'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( - printedMessages, + output, contains( '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.')); }); - test('runs e2e tests', () async { + test('runs integration tests', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'test/plugin_test.dart', - 'test/plugin_e2e.dart', - 'should_not_run_e2e.dart', - 'lib/test/should_not_run_e2e.dart', - 'example/test/plugin_e2e.dart', - 'example/test_driver/plugin_e2e.dart', - 'example/test_driver/plugin_e2e_test.dart', + 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/integration_test/should_not_run.dart', 'example/android/gradlew', - 'example/should_not_run_e2e.dart', 'example/android/app/src/androidTest/MainActivityTest.java', ]); - await runCapturingPrint(runner, [ + final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', 'model=flame,version=29', @@ -86,14 +82,17 @@ void main() { ]); expect( - printedMessages, - orderedEquals([ - '\nRUNNING FIREBASE TEST LAB TESTS for plugin', - '\nFirebase project configured.', - '\n\n', - 'All Firebase Test Lab tests successful!', + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Firebase project configured.'), + contains('Testing example/integration_test/bar_test.dart...'), + contains('Testing example/integration_test/foo_test.dart...'), ]), ); + expect(output, isNot(contains('test/plugin_test.dart'))); + expect(output, + isNot(contains('example/integration_test/should_not_run.dart'))); expect( processRunner.recordedCalls, @@ -111,7 +110,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/bar_test.dart' .split(' '), '/packages/plugin/example/android'), ProcessCall( @@ -121,7 +120,7 @@ void main() { '/packages/plugin/example'), ProcessCall( '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' .split(' '), '/packages/plugin/example/android'), ProcessCall( @@ -129,16 +128,91 @@ void main() { 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), + ]), + ); + }); + + test('skips packages with no androidTest directory', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No example with androidTest directory'), + ]), + ); + expect(output, + isNot(contains('Testing example/integration_test/foo_test.dart...'))); + + expect( + processRunner.recordedCalls, + orderedEquals([]), + ); + }); + + test('builds if gradlew is missing', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Running flutter build apk...'), + contains('Firebase project configured.'), + contains('Testing example/integration_test/foo_test.dart...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' - .split(' '), - '/packages/plugin/example/android'), + 'flutter', + 'build apk'.split(' '), + '/packages/plugin/example/android', + ), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' .split(' '), - '/packages/plugin/example'), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin/example/android'), ProcessCall( '/packages/plugin/example/android/gradlew', 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' @@ -146,7 +220,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -155,17 +229,8 @@ void main() { test('experimental flag', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ - 'test/plugin_test.dart', - 'test/plugin_e2e.dart', - 'should_not_run_e2e.dart', - 'lib/test/should_not_run_e2e.dart', - 'example/test/plugin_e2e.dart', - 'example/test_driver/plugin_e2e.dart', - 'example/test_driver/plugin_e2e_test.dart', 'example/integration_test/foo_test.dart', - 'example/integration_test/should_not_run.dart', 'example/android/gradlew', - 'example/should_not_run_e2e.dart', 'example/android/app/src/androidTest/MainActivityTest.java', ]); @@ -195,36 +260,6 @@ void main() { 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' .split(' '), '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/2/ --device model=flame,version=29' - .split(' '), - '/packages/plugin/example'), ProcessCall( '/packages/plugin/example/android/gradlew', 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' @@ -232,7 +267,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/3/ --device model=flame,version=29' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' .split(' '), '/packages/plugin/example'), ]), From 92d62149848d16bed54b6b87678bfa4a084124fa Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 30 Jun 2021 12:16:46 -0700 Subject: [PATCH 084/249] [flutter_plugin_tools] Overhaul drive-examples (#4099) Significantly restructures drive-examples: - Migrates it to the new package-looping base command - Enforces that only one platform is passed, since in practice multiple platforms never actually worked. (The logic is structured so that it will be easy to enable multi-platform if `flutter drive` gains multi-platform support.) - Fixes the issue where `--ios` and `--android` were semi-broken, by doing explicit device targeting for them rather than relying on the default device being the right kind - Extracts much of the logic to helpers so it's easier to understand the flow - Removes support for a legacy integration test file structure that is no longer used - Adds more test coverage; previously no failure cases were actually tested. Fixes https://github.com/flutter/flutter/issues/85147 Part of https://github.com/flutter/flutter/issues/83413 --- .../tool/lib/src/build_examples_command.dart | 3 - .../tool/lib/src/common/plugin_command.dart | 5 + .../tool/lib/src/drive_examples_command.dart | 431 ++++++++------ .../test/drive_examples_command_test.dart | 559 ++++++++++++++---- 4 files changed, 692 insertions(+), 306 deletions(-) diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index c8280f4e867..ee1445fa8b7 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -151,8 +150,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { String flutterBuildType, { List extraBuildFlags = const [], }) async { - final String flutterCommand = - const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; final String enableExperiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 9a96ab13443..43d0d0b822c 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -8,6 +8,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'core.dart'; import 'git_version_finder.dart'; @@ -85,6 +86,10 @@ abstract class PluginCommand extends Command { int? _shardIndex; int? _shardCount; + /// The command to use when running `flutter`. + String get flutterCommand => + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + /// The shard of the overall command execution that this instance should run. int get shardIndex { if (_shardIndex == null) { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 8a8cd6726d0..a4aa7c12913 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -2,17 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; +import 'dart:io'; + import 'package:file/file.dart'; import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +const int _exitNoPlatformFlags = 2; +const int _exitNoAvailableDevice = 3; + /// A command to run the example applications for packages via Flutter driver. -class DriveExamplesCommand extends PluginCommand { +class DriveExamplesCommand extends PackageLoopingCommand { /// Creates an instance of the drive command. DriveExamplesCommand( Directory packagesDir, { @@ -43,213 +48,259 @@ class DriveExamplesCommand extends PluginCommand { @override final String description = 'Runs driver tests for plugin example apps.\n\n' - 'For each *_test.dart in test_driver/ it drives an application with a ' - 'corresponding name in the test/ or test_driver/ directories.\n\n' - 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' - 'This command requires "flutter" to be in your path.\n\n' - 'If a file with a corresponding name cannot be found, this driver file' - 'will be used to drive the tests that match ' - 'integration_test/*_test.dart.'; + 'For each *_test.dart in test_driver/ it drives an application with ' + 'either the corresponding test in test_driver (for example, ' + 'test_driver/app_test.dart would match test_driver/app.dart), or the ' + '*_test.dart files in integration_test/.\n\n' + 'This command requires "flutter" to be in your path.'; + + Map> _targetDeviceFlags = const >{}; @override - Future run() async { - final List failingTests = []; - final List pluginsWithoutTests = []; - final bool isLinux = getBoolArg(kPlatformLinux); - final bool isMacos = getBoolArg(kPlatformMacos); - final bool isWeb = getBoolArg(kPlatformWeb); - final bool isWindows = getBoolArg(kPlatformWindows); - await for (final Directory plugin in getPlugins()) { - final String pluginName = plugin.basename; - if (pluginName.endsWith('_platform_interface') && - !plugin.childDirectory('example').existsSync()) { - // Platform interface packages generally aren't intended to have - // examples, and don't need integration tests, so silently skip them - // unless for some reason there is an example directory. - continue; + Future initializeRun() async { + final List platformSwitches = [ + kPlatformAndroid, + kPlatformIos, + kPlatformLinux, + kPlatformMacos, + kPlatformWeb, + kPlatformWindows, + ]; + final int platformCount = platformSwitches + .where((String platform) => getBoolArg(platform)) + .length; + // The flutter tool currently doesn't accept multiple device arguments: + // https://github.com/flutter/flutter/issues/35733 + // If that is implemented, this check can be relaxed. + if (platformCount != 1) { + printError( + 'Exactly one of ${platformSwitches.map((String platform) => '--$platform').join(', ')} ' + 'must be specified.'); + throw ToolExit(_exitNoPlatformFlags); + } + + String? androidDevice; + if (getBoolArg(kPlatformAndroid)) { + final List devices = await _getDevicesForPlatform('android'); + if (devices.isEmpty) { + printError('No Android devices available'); + throw ToolExit(_exitNoAvailableDevice); + } + androidDevice = devices.first; + } + + String? iosDevice; + if (getBoolArg(kPlatformIos)) { + final List devices = await _getDevicesForPlatform('ios'); + if (devices.isEmpty) { + printError('No iOS devices available'); + throw ToolExit(_exitNoAvailableDevice); + } + iosDevice = devices.first; + } + + _targetDeviceFlags = >{ + if (getBoolArg(kPlatformAndroid)) + kPlatformAndroid: ['-d', androidDevice!], + if (getBoolArg(kPlatformIos)) kPlatformIos: ['-d', iosDevice!], + if (getBoolArg(kPlatformLinux)) kPlatformLinux: ['-d', 'linux'], + if (getBoolArg(kPlatformMacos)) kPlatformMacos: ['-d', 'macos'], + if (getBoolArg(kPlatformWeb)) + kPlatformWeb: [ + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome' + ], + if (getBoolArg(kPlatformWindows)) + kPlatformWindows: ['-d', 'windows'], + }; + } + + @override + Future> runForPackage(Directory package) async { + if (package.basename.endsWith('_platform_interface') && + !package.childDirectory('example').existsSync()) { + // Platform interface packages generally aren't intended to have + // examples, and don't need integration tests, so skip rather than fail. + printSkip( + 'Platform interfaces are not expected to have integratino tests.'); + return PackageLoopingCommand.success; + } + + final List deviceFlags = []; + for (final MapEntry> entry + in _targetDeviceFlags.entries) { + if (pluginSupportsPlatform(entry.key, package)) { + deviceFlags.addAll(entry.value); + } else { + print('Skipping unsupported platform ${entry.key}...'); } - print('\n==========\nChecking $pluginName...'); - if (!(await _pluginSupportedOnCurrentPlatform(plugin))) { - print('Not supported for the target platform; skipping.'); + } + // If there is no supported target platform, skip the plugin. + if (deviceFlags.isEmpty) { + printSkip( + '${getPackageDescription(package)} does not support any requested platform.'); + return PackageLoopingCommand.success; + } + + int examplesFound = 0; + bool testsRan = false; + final List errors = []; + for (final Directory example in getExamplesForPlugin(package)) { + ++examplesFound; + final String exampleName = + p.relative(example.path, from: packagesDir.path); + + final List drivers = await _getDrivers(example); + if (drivers.isEmpty) { + print('No driver tests found for $exampleName'); continue; } - int examplesFound = 0; - bool testsRan = false; - final String flutterCommand = - const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; - for (final Directory example in getExamplesForPlugin(plugin)) { - ++examplesFound; - final String packageName = - p.relative(example.path, from: packagesDir.path); - final Directory driverTests = example.childDirectory('test_driver'); - if (!driverTests.existsSync()) { - print('No driver tests found for $packageName'); + + for (final File driver in drivers) { + final List testTargets = []; + + // Try to find a matching app to drive without the _test.dart + // TODO(stuartmorgan): Migrate all remaining uses of this legacy + // approach (currently only video_player) and remove support for it: + // https://github.com/flutter/flutter/issues/85224. + final File? legacyTestFile = _getLegacyTestFileForTestDriver(driver); + if (legacyTestFile != null) { + testTargets.add(legacyTestFile); + } else { + (await _getIntegrationTests(example)).forEach(testTargets.add); + } + + if (testTargets.isEmpty) { + final String driverRelativePath = + p.relative(driver.path, from: package.path); + printError( + 'Found $driverRelativePath, but no integration_test/*_test.dart files.'); + errors.add( + 'No test files for ${p.relative(driver.path, from: package.path)}'); continue; } - // Look for driver tests ending in _test.dart in test_driver/ - await for (final FileSystemEntity test in driverTests.list()) { - final String driverTestName = - p.relative(test.path, from: driverTests.path); - if (!driverTestName.endsWith('_test.dart')) { - continue; - } - // Try to find a matching app to drive without the _test.dart - final String deviceTestName = driverTestName.replaceAll( - RegExp(r'_test.dart$'), - '.dart', - ); - String deviceTestPath = p.join('test', deviceTestName); - if (!example.fileSystem - .file(p.join(example.path, deviceTestPath)) - .existsSync()) { - // If the app isn't in test/ folder, look in test_driver/ instead. - deviceTestPath = p.join('test_driver', deviceTestName); - } - - final List targetPaths = []; - if (example.fileSystem - .file(p.join(example.path, deviceTestPath)) - .existsSync()) { - targetPaths.add(deviceTestPath); - } else { - final Directory integrationTests = - example.childDirectory('integration_test'); - - if (await integrationTests.exists()) { - await for (final FileSystemEntity integrationTest - in integrationTests.list()) { - if (!integrationTest.basename.endsWith('_test.dart')) { - continue; - } - targetPaths - .add(p.relative(integrationTest.path, from: example.path)); - } - } - - if (targetPaths.isEmpty) { - print(''' -Unable to infer a target application for $driverTestName to drive. -Tried searching for the following: -1. test/$deviceTestName -2. test_driver/$deviceTestName -3. test_driver/*_test.dart -'''); - failingTests.add(p.relative(test.path, from: example.path)); - continue; - } - } - - final List driveArgs = ['drive']; - - final String enableExperiment = getStringArg(kEnableExperiment); - if (enableExperiment.isNotEmpty) { - driveArgs.add('--enable-experiment=$enableExperiment'); - } - - if (isLinux && isLinuxPlugin(plugin)) { - driveArgs.addAll([ - '-d', - 'linux', - ]); - } - if (isMacos && isMacOsPlugin(plugin)) { - driveArgs.addAll([ - '-d', - 'macos', - ]); - } - if (isWeb && isWebPlugin(plugin)) { - driveArgs.addAll([ - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - ]); - } - if (isWindows && isWindowsPlugin(plugin)) { - driveArgs.addAll([ - '-d', - 'windows', - ]); - } - - for (final String targetPath in targetPaths) { - testsRan = true; - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - ...driveArgs, - '--driver', - p.join('test_driver', driverTestName), - '--target', - targetPath, - ], - workingDir: example, - exitOnError: true); - if (exitCode != 0) { - failingTests.add(p.join(packageName, deviceTestPath)); - } - } + + testsRan = true; + final List failingTargets = await _driveTests( + example, driver, testTargets, + deviceFlags: deviceFlags); + for (final File failingTarget in failingTargets) { + errors.add(p.relative(failingTarget.path, from: package.path)); } } - if (!testsRan) { - pluginsWithoutTests.add(pluginName); - print( - 'No driver tests run for $pluginName ($examplesFound examples found)'); - } } - print('\n\n'); + if (!testsRan) { + printError('No driver tests were run ($examplesFound example(s) found).'); + errors.add('No tests ran (use --exclude if this is intentional).'); + } + return errors; + } + + Future> _getDevicesForPlatform(String platform) async { + final List deviceIds = []; - if (failingTests.isNotEmpty) { - print('The following driver tests are failing (see above for details):'); - for (final String test in failingTests) { - print(' * $test'); + final ProcessResult result = await processRunner.run( + flutterCommand, ['devices', '--machine'], + stdoutEncoding: utf8, exitOnError: true); + if (result.exitCode != 0) { + return deviceIds; + } + + final List> devices = + (jsonDecode(result.stdout as String) as List) + .cast>(); + for (final Map deviceInfo in devices) { + final String targetPlatform = + (deviceInfo['targetPlatform'] as String?) ?? ''; + if (targetPlatform.startsWith(platform)) { + final String? deviceId = deviceInfo['id'] as String?; + if (deviceId != null) { + deviceIds.add(deviceId); + } } - throw ToolExit(1); } + return deviceIds; + } + + Future> _getDrivers(Directory example) async { + final List drivers = []; - if (pluginsWithoutTests.isNotEmpty) { - print('The following plugins did not run any integration tests:'); - for (final String plugin in pluginsWithoutTests) { - print(' * $plugin'); + final Directory driverDir = example.childDirectory('test_driver'); + if (driverDir.existsSync()) { + await for (final FileSystemEntity driver in driverDir.list()) { + if (driver is File && driver.basename.endsWith('_test.dart')) { + drivers.add(driver); + } } - print('If this is intentional, they must be explicitly excluded.'); - throw ToolExit(1); } + return drivers; + } + + File? _getLegacyTestFileForTestDriver(File testDriver) { + final String testName = testDriver.basename.replaceAll( + RegExp(r'_test.dart$'), + '.dart', + ); + final File testFile = testDriver.parent.childFile(testName); - print('All driver tests successful!'); + return testFile.existsSync() ? testFile : null; } - Future _pluginSupportedOnCurrentPlatform( - FileSystemEntity plugin) async { - final bool isAndroid = getBoolArg(kPlatformAndroid); - final bool isIOS = getBoolArg(kPlatformIos); - final bool isLinux = getBoolArg(kPlatformLinux); - final bool isMacos = getBoolArg(kPlatformMacos); - final bool isWeb = getBoolArg(kPlatformWeb); - final bool isWindows = getBoolArg(kPlatformWindows); - if (isAndroid) { - return isAndroidPlugin(plugin); - } - if (isIOS) { - return isIosPlugin(plugin); - } - if (isLinux) { - return isLinuxPlugin(plugin); - } - if (isMacos) { - return isMacOsPlugin(plugin); - } - if (isWeb) { - return isWebPlugin(plugin); + Future> _getIntegrationTests(Directory example) async { + final List tests = []; + final Directory integrationTestDir = + example.childDirectory('integration_test'); + + if (integrationTestDir.existsSync()) { + await for (final FileSystemEntity file in integrationTestDir.list()) { + if (file is File && file.basename.endsWith('_test.dart')) { + tests.add(file); + } + } } - if (isWindows) { - return isWindowsPlugin(plugin); + return tests; + } + + /// For each file in [targets], uses + /// `flutter drive --driver [driver] --target ` + /// to drive [example], returning a list of any failing test targets. + /// + /// [deviceFlags] should contain the flags to run the test on a specific + /// target device (plus any supporting device-specific flags). E.g.: + /// - `['-d', 'macos']` for driving for macOS. + /// - `['-d', 'web-server', '--web-port=', '--browser-name=]` + /// for web + Future> _driveTests( + Directory example, + File driver, + List targets, { + required List deviceFlags, + }) async { + final List failures = []; + + final String enableExperiment = getStringArg(kEnableExperiment); + + for (final File target in targets) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'drive', + ...deviceFlags, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + '--driver', + p.relative(driver.path, from: example.path), + '--target', + p.relative(target.path, from: example.path), + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failures.add(target); + } } - // When we are here, no flags are specified. Only return true if the plugin - // supports Android for legacy command support. - // TODO(cyanglaz): Make Android flag also required like other platforms - // (breaking change). https://github.com/flutter/flutter/issues/58285 - return isAndroidPlugin(plugin); + return failures; } } diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 3175f716354..e441a0f68d9 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -12,8 +12,12 @@ import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; import 'util.dart'; +const String _fakeIosDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; +const String _fakeAndroidDevice = 'emulator-1234'; + void main() { group('test drive_example_command', () { late FileSystem fileSystem; @@ -35,52 +39,92 @@ void main() { runner.addCommand(command); }); - test('driving under folder "test"', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test/plugin.dart', - ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - }, + void setMockFlutterDevicesOutput({ + bool hasIosDevice = true, + bool hasAndroidDevice = true, + }) { + final List devices = [ + if (hasIosDevice) '{"id": "$_fakeIosDevice", "targetPlatform": "ios"}', + if (hasAndroidDevice) + '{"id": "$_fakeAndroidDevice", "targetPlatform": "android-x86"}', + ]; + final String output = '''[${devices.join(',')}]'''; + + final MockProcess mockDevicesProcess = MockProcess(); + mockDevicesProcess.exitCodeCompleter.complete(0); + mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures + processRunner.processToReturn = mockDevicesProcess; + processRunner.resultStdout = output; + } + + test('fails if no platforms are provided', () async { + setMockFlutterDevicesOutput(); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Exactly one of'), + ]), ); + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + test('fails if multiple platforms are provided', () async { + setMockFlutterDevicesOutput(); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--ios', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Exactly one of'), + ]), + ); + }); + + test('fails for iOS if no iOS devices are present', () async { + setMockFlutterDevicesOutput(hasIosDevice: false); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('No iOS devices'), ]), ); + }); - final String deviceTestPath = p.join('test', 'plugin.dart'); - final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + test('fails if Android if no Android devices are present', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); + output, + containsAllInOrder([ + contains('No Android devices'), + ]), + ); }); test('driving under folder "test_driver"', () async { @@ -100,16 +144,15 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); + setMockFlutterDevicesOutput(); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -118,10 +161,14 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), ProcessCall( flutterCommand, [ 'drive', + '-d', + _fakeIosDevice, '--driver', driverTestPath, '--target', @@ -133,6 +180,7 @@ void main() { test('driving under folder "test_driver" when test files are missing"', () async { + setMockFlutterDevicesOutput(); createFakePlugin( 'plugin', packagesDir, @@ -145,13 +193,27 @@ void main() { }, ); - await expectLater( - () => runCapturingPrint(runner, ['drive-examples']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No driver tests were run (1 example(s) found).'), + contains('No test files for example/test_driver/plugin_test.dart'), + ]), + ); }); test('a plugin without any integration test files is reported as an error', () async { + setMockFlutterDevicesOutput(); createFakePlugin( 'plugin', packagesDir, @@ -164,9 +226,22 @@ void main() { }, ); - await expectLater( - () => runCapturingPrint(runner, ['drive-examples']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No driver tests were run (1 example(s) found).'), + contains('No tests ran'), + ]), + ); }); test( @@ -190,16 +265,15 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); + setMockFlutterDevicesOutput(); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -208,10 +282,14 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), ProcessCall( flutterCommand, [ 'drive', + '-d', + _fakeIosDevice, '--driver', driverTestPath, '--target', @@ -222,6 +300,8 @@ void main() { flutterCommand, [ 'drive', + '-d', + _fakeIosDevice, '--driver', driverTestPath, '--target', @@ -244,11 +324,10 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform linux...'), + contains('No issues found!'), ]), ); @@ -280,10 +359,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -320,11 +398,10 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform macos...'), + contains('No issues found!'), ]), ); @@ -332,6 +409,7 @@ void main() { // implementation is a no-op. expect(processRunner.recordedCalls, []); }); + test('driving on a macOS plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', @@ -356,10 +434,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -396,11 +473,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -432,10 +507,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -474,11 +548,10 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform windows...'), + contains('No issues found!'), ]), ); @@ -510,10 +583,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -537,8 +609,8 @@ void main() { ])); }); - test('driving when plugin does not support mobile is no-op', () async { - createFakePlugin( + test('driving on an Android plugin', () async { + final Directory pluginDirectory = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -546,47 +618,134 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, + kPlatformAndroid: PlatformSupport.inline, }, ); + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + setMockFlutterDevicesOutput(); final List output = await runCapturingPrint(runner, [ 'drive-examples', + '--android', ]); expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + _fakeAndroidDevice, + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Android is no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }, + ); + + setMockFlutterDevicesOutput(); + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform android...'), + contains('No issues found!'), + ]), + ); + + // Output should be empty other than the device query. + expect(processRunner.recordedCalls, [ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), + ]); + }); + + test('driving when plugin does not support iOS is no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }, + ); + + setMockFlutterDevicesOutput(); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform ios...'), + contains('No issues found!'), + ]), + ); + + // Output should be empty other than the device query. + expect(processRunner.recordedCalls, [ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), + ]); }); test('platform interface plugins are silently skipped', () async { createFakePlugin('aplugin_platform_interface', packagesDir, examples: []); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); + setMockFlutterDevicesOutput(); + final List output = await runCapturingPrint( + runner, ['drive-examples', '--macos']); expect( output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for aplugin_platform_interface'), + contains( + 'SKIPPING: Platform interfaces are not expected to have integratino tests.'), + contains('No issues found!'), ]), ); - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. + // Output should be empty since it's skipped. expect(processRunner.recordedCalls, []); }); @@ -596,7 +755,7 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/plugin_test.dart', - 'example/test/plugin.dart', + 'example/test_driver/plugin.dart', ], platformSupport: { kPlatformAndroid: PlatformSupport.inline, @@ -607,20 +766,26 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); + setMockFlutterDevicesOutput(); await runCapturingPrint(runner, [ 'drive-examples', + '--ios', '--enable-experiment=exp1', ]); - final String deviceTestPath = p.join('test', 'plugin.dart'); + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), ProcessCall( flutterCommand, [ 'drive', + '-d', + _fakeIosDevice, '--enable-experiment=exp1', '--driver', driverTestPath, @@ -630,5 +795,173 @@ void main() { pluginExampleDirectory.path), ])); }); + + test('fails when no example is present', () async { + createFakePlugin( + 'plugin', + packagesDir, + examples: [], + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--web'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No driver tests were run (0 example(s) found).'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' No tests ran (use --exclude if this is intentional)'), + ]), + ); + }); + + test('fails when no driver is present', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + ], + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--web'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No driver tests found for plugin/example'), + contains('No driver tests were run (1 example(s) found).'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' No tests ran (use --exclude if this is intentional)'), + ]), + ); + }); + + test('fails when no integration tests are present', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/integration_test.dart', + ], + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--web'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Found example/test_driver/integration_test.dart, but no ' + 'integration_test/*_test.dart files.'), + contains('No driver tests were run (1 example(s) found).'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' No test files for example/test_driver/integration_test.dart\n' + ' No tests ran (use --exclude if this is intentional)'), + ]), + ); + }); + + test('reports test failures', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/integration_test.dart', + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + ], + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }, + ); + + // Simulate failure from `flutter drive`. + final MockProcess mockDriveProcess = MockProcess(); + mockDriveProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockDriveProcess; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['drive-examples', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' example/integration_test/bar_test.dart\n' + ' example/integration_test/foo_test.dart'), + ]), + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + final String driverTestPath = + p.join('test_driver', 'integration_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'bar_test.dart'), + ], + pluginExampleDirectory.path), + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'foo_test.dart'), + ], + pluginExampleDirectory.path), + ])); + }); }); } From edeb10a75295c32adf7158861202eb5cc7d7d0e5 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 1 Jul 2021 16:25:21 -0700 Subject: [PATCH 085/249] [flutter_plugin_tools] Add a summary for successful runs (#4118) Add a summary to the end of successful runs for everything using the new looping base command, similar to what we do for summarizing failures. This will make it easy to manually check results for PRs that we know should be changing the set of run packages (adding a new package, adding a new test type to a package, adding a new test type to the tool), as well as spot-checking when we see unexpected results (e.g., looking back and why a PR didn't fail CI when we discover that it should have). To support better surfacing skips, this restructures the return value of `runForPackage` to have "skip" as one of the options. As a result of it being a return value, packages that used `printSkip` to indicate that *parts* of the command were being skipped have been changed to no longer do that. Fixes https://github.com/flutter/flutter/issues/85626 --- script/tool/CHANGELOG.md | 2 + script/tool/lib/src/analyze_command.dart | 6 +- .../tool/lib/src/build_examples_command.dart | 180 ++++++++------ script/tool/lib/src/common/core.dart | 10 - .../src/common/package_looping_command.dart | 201 ++++++++++++--- .../tool/lib/src/drive_examples_command.dart | 14 +- .../lib/src/firebase_test_lab_command.dart | 26 +- script/tool/lib/src/java_test_command.dart | 10 +- .../tool/lib/src/lint_podspecs_command.dart | 14 +- .../tool/lib/src/pubspec_check_command.dart | 6 +- .../tool/lib/src/version_check_command.dart | 23 +- script/tool/lib/src/xctest_command.dart | 53 ++-- .../test/build_examples_command_test.dart | 67 ++--- .../common/package_looping_command_test.dart | 198 +++++++++++++-- .../test/drive_examples_command_test.dart | 2 +- .../test/firebase_test_lab_command_test.dart | 11 +- script/tool/test/java_test_command_test.dart | 16 ++ .../tool/test/lint_podspecs_command_test.dart | 13 + script/tool/test/xctest_command_test.dart | 233 ++++++++++++++++-- 19 files changed, 814 insertions(+), 271 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2b15ccdd2ac..d64655f5b86 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -3,6 +3,8 @@ - Modified the output format of many commands - **Breaking change**: `firebase-test-lab` no longer supports `*_e2e.dart` files, only `integration_test/*_test.dart`. +- Add a summary to the end of successful command runs for commands using the + new output format. ## 0.3.0 diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 3f6a2444ad9..29e78f3ea4f 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -106,13 +106,13 @@ class AnalyzeCommand extends PackageLoopingCommand { } @override - Future> runForPackage(Directory package) async { + Future runForPackage(Directory package) async { final int exitCode = await processRunner.runAndStream( _dartBinaryPath, ['analyze', '--fatal-infos'], workingDir: package); if (exitCode != 0) { - return PackageLoopingCommand.failure; + return PackageResult.fail(); } - return PackageLoopingCommand.success; + return PackageResult.success(); } } diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index ee1445fa8b7..32905c83db9 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -37,6 +37,43 @@ class BuildExamplesCommand extends PackageLoopingCommand { ); } + // Maps the switch this command uses to identify a platform to information + // about it. + static final Map _platforms = + { + _platformFlagApk: const _PlatformDetails( + 'Android', + pluginPlatform: kPlatformAndroid, + flutterBuildType: 'apk', + ), + kPlatformIos: const _PlatformDetails( + 'iOS', + pluginPlatform: kPlatformIos, + flutterBuildType: 'ios', + extraBuildFlags: ['--no-codesign'], + ), + kPlatformLinux: const _PlatformDetails( + 'Linux', + pluginPlatform: kPlatformLinux, + flutterBuildType: 'linux', + ), + kPlatformMacos: const _PlatformDetails( + 'macOS', + pluginPlatform: kPlatformMacos, + flutterBuildType: 'macos', + ), + kPlatformWeb: const _PlatformDetails( + 'web', + pluginPlatform: kPlatformWeb, + flutterBuildType: 'web', + ), + kPlatformWindows: const _PlatformDetails( + 'Windows', + pluginPlatform: kPlatformWindows, + flutterBuildType: 'windows', + ), + }; + @override final String name = 'build-examples'; @@ -47,102 +84,67 @@ class BuildExamplesCommand extends PackageLoopingCommand { @override Future initializeRun() async { - final List platformSwitches = [ - _platformFlagApk, - kPlatformIos, - kPlatformLinux, - kPlatformMacos, - kPlatformWeb, - kPlatformWindows, - ]; - if (!platformSwitches.any((String platform) => getBoolArg(platform))) { + final List platformFlags = _platforms.keys.toList(); + platformFlags.sort(); + if (!platformFlags.any((String platform) => getBoolArg(platform))) { printError( - 'None of ${platformSwitches.map((String platform) => '--$platform').join(', ')} ' + 'None of ${platformFlags.map((String platform) => '--$platform').join(', ')} ' 'were specified. At least one platform must be provided.'); throw ToolExit(_exitNoPlatformFlags); } } @override - Future> runForPackage(Directory package) async { + Future runForPackage(Directory package) async { final List errors = []; + final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries + .where( + (MapEntry entry) => getBoolArg(entry.key)) + .map((MapEntry entry) => entry.value); + final Set<_PlatformDetails> buildPlatforms = <_PlatformDetails>{}; + final Set<_PlatformDetails> unsupportedPlatforms = <_PlatformDetails>{}; + for (final _PlatformDetails platform in requestedPlatforms) { + if (pluginSupportsPlatform(platform.pluginPlatform, package)) { + buildPlatforms.add(platform); + } else { + unsupportedPlatforms.add(platform); + } + } + if (buildPlatforms.isEmpty) { + final String unsupported = requestedPlatforms.length == 1 + ? '${requestedPlatforms.first.label} is not supported' + : 'None of [${requestedPlatforms.map((_PlatformDetails p) => p.label).join(',')}] are supported'; + return PackageResult.skip('$unsupported by this plugin'); + } + print('Building for: ' + '${buildPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}'); + if (unsupportedPlatforms.isNotEmpty) { + print('Skipping unsupported platform(s): ' + '${unsupportedPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}'); + } + print(''); + for (final Directory example in getExamplesForPlugin(package)) { final String packageName = p.relative(example.path, from: packagesDir.path); - if (getBoolArg(kPlatformLinux)) { - print('\nBUILDING $packageName for Linux'); - if (isLinuxPlugin(package)) { - if (!await _buildExample(example, kPlatformLinux)) { - errors.add('$packageName (Linux)'); - } - } else { - printSkip('Linux is not supported by this plugin'); - } - } - - if (getBoolArg(kPlatformMacos)) { - print('\nBUILDING $packageName for macOS'); - if (isMacOsPlugin(package)) { - if (!await _buildExample(example, kPlatformMacos)) { - errors.add('$packageName (macOS)'); - } - } else { - printSkip('macOS is not supported by this plugin'); - } - } - - if (getBoolArg(kPlatformWeb)) { - print('\nBUILDING $packageName for web'); - if (isWebPlugin(package)) { - if (!await _buildExample(example, kPlatformWeb)) { - errors.add('$packageName (web)'); - } - } else { - printSkip('Web is not supported by this plugin'); - } - } - - if (getBoolArg(kPlatformWindows)) { - print('\nBUILDING $packageName for Windows'); - if (isWindowsPlugin(package)) { - if (!await _buildExample(example, kPlatformWindows)) { - errors.add('$packageName (Windows)'); - } - } else { - printSkip('Windows is not supported by this plugin'); + for (final _PlatformDetails platform in buildPlatforms) { + String buildPlatform = platform.label; + if (platform.label.toLowerCase() != platform.flutterBuildType) { + buildPlatform += ' (${platform.flutterBuildType})'; } - } - - if (getBoolArg(kPlatformIos)) { - print('\nBUILDING $packageName for iOS'); - if (isIosPlugin(package)) { - if (!await _buildExample( - example, - kPlatformIos, - extraBuildFlags: ['--no-codesign'], - )) { - errors.add('$packageName (iOS)'); - } - } else { - printSkip('iOS is not supported by this plugin'); - } - } - - if (getBoolArg(_platformFlagApk)) { - print('\nBUILDING APK for $packageName'); - if (isAndroidPlugin(package)) { - if (!await _buildExample(example, _platformFlagApk)) { - errors.add('$packageName (apk)'); - } - } else { - printSkip('Android is not supported by this plugin'); + print('\nBUILDING $packageName for $buildPlatform'); + if (!await _buildExample(example, platform.flutterBuildType, + extraBuildFlags: platform.extraBuildFlags)) { + errors.add('$packageName (${platform.label})'); } } } - return errors; + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); } Future _buildExample( @@ -166,3 +168,25 @@ class BuildExamplesCommand extends PackageLoopingCommand { return exitCode == 0; } } + +/// A collection of information related to a specific platform. +class _PlatformDetails { + const _PlatformDetails( + this.label, { + required this.pluginPlatform, + required this.flutterBuildType, + this.extraBuildFlags = const [], + }); + + /// The name to use in output. + final String label; + + /// The key in a pubspec's platform: entry. + final String pluginPlatform; + + /// The `flutter build` build type. + final String flutterBuildType; + + /// Any extra flags to pass to `flutter build`. + final List extraBuildFlags; +} diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 3b07baf5dc1..b2be8f56d17 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -58,16 +58,6 @@ void printSuccess(String successMessage) { print(Colorize(successMessage)..green()); } -/// Prints `warningMessage` in yellow. -/// -/// Warnings are not surfaced in CI summaries, so this is only useful for -/// highlighting something when someone is already looking though the log -/// messages. DO NOT RELY on someone noticing a warning; instead, use it for -/// things that might be useful to someone debugging an unexpected result. -void printWarning(String warningMessage) { - print(Colorize(warningMessage)..yellow()); -} - /// Prints `errorMessage` in red. void printError(String errorMessage) { print(Colorize(errorMessage)..red()); diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index cfe99313068..cd3c21db213 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -11,6 +11,48 @@ import 'core.dart'; import 'plugin_command.dart'; import 'process_runner.dart'; +/// Possible outcomes of a command run for a package. +enum RunState { + /// The command succeeded for the package. + succeeded, + + /// The command was skipped for the package. + skipped, + + /// The command failed for the package. + failed, +} + +/// The result of a [runForPackage] call. +class PackageResult { + /// A successful result. + PackageResult.success() : this._(RunState.succeeded); + + /// A run that was skipped as explained in [reason]. + PackageResult.skip(String reason) + : this._(RunState.skipped, [reason]); + + /// A run that failed. + /// + /// If [errors] are provided, they will be listed in the summary, otherwise + /// the summary will simply show that the package failed. + PackageResult.fail([List errors = const []]) + : this._(RunState.failed, errors); + + const PackageResult._(this.state, [this.details = const []]); + + /// The state the package run completed with. + final RunState state; + + /// Information about the result: + /// - For `succeeded`, this is empty. + /// - For `skipped`, it contains a single entry describing why the run was + /// skipped. + /// - For `failed`, it contains zero or more specific error details to be + /// shown in the summary. + final List details; +} + /// An abstract base class for a command that iterates over a set of packages /// controlled by a standard set of flags, running some actions on each package, /// and collecting and reporting the success/failure of those actions. @@ -22,6 +64,15 @@ abstract class PackageLoopingCommand extends PluginCommand { GitDir? gitDir, }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); + /// Packages that had at least one [logWarning] call. + final Set _packagesWithWarnings = {}; + + /// Number of warnings that happened outside of a [runForPackage] call. + int _otherWarningCount = 0; + + /// The package currently being run by [runForPackage]. + Directory? _currentPackage; + /// Called during [run] before any calls to [runForPackage]. This provides an /// opportunity to fail early if the command can't be run (e.g., because the /// arguments are invalid), and to set up any run-level state. @@ -33,7 +84,7 @@ abstract class PackageLoopingCommand extends PluginCommand { /// be included in the final error summary (e.g., a command that only has a /// single failure mode), or strings that should be listed for that package /// in the final summary. An empty list indicates success. - Future> runForPackage(Directory package); + Future runForPackage(Directory package); /// Called during [run] after all calls to [runForPackage]. This provides an /// opportunity to do any cleanup of run-level state. @@ -71,18 +122,19 @@ abstract class PackageLoopingCommand extends PluginCommand { // ---------------------------------------- - /// A convenience constant for [runForPackage] success that's more - /// self-documenting than the value. - static const List success = []; - - /// A convenience constant for [runForPackage] failure without additional - /// context that's more self-documenting than the value. - static const List failure = ['']; - - /// Prints a message using a standard format indicating that the package was - /// skipped, with an explanation of why. - void printSkip(String reason) { - print(Colorize('SKIPPING: $reason')..darkGray()); + /// Logs that a warning occurred, and prints `warningMessage` in yellow. + /// + /// Warnings are not surfaced in CI summaries, so this is only useful for + /// highlighting something when someone is already looking though the log + /// messages. DO NOT RELY on someone noticing a warning; instead, use it for + /// things that might be useful to someone debugging an unexpected result. + void logWarning(String warningMessage) { + print(Colorize(warningMessage)..yellow()); + if (_currentPackage != null) { + _packagesWithWarnings.add(_currentPackage!); + } else { + ++_otherWarningCount; + } } /// Returns the identifying name to use for [package]. @@ -110,41 +162,44 @@ abstract class PackageLoopingCommand extends PluginCommand { @override Future run() async { + _packagesWithWarnings.clear(); + _otherWarningCount = 0; + _currentPackage = null; + await initializeRun(); final List packages = includeSubpackages ? await getPackages().toList() : await getPlugins().toList(); - final Map> results = >{}; + final Map results = {}; for (final Directory package in packages) { + _currentPackage = package; _printPackageHeading(package); - results[package] = await runForPackage(package); + final PackageResult result = await runForPackage(package); + if (result.state == RunState.skipped) { + print(Colorize('${indentation}SKIPPING: ${result.details.first}') + ..darkGray()); + } + results[package] = result; } + _currentPackage = null; completeRun(); // If there were any errors reported, summarize them and exit. - if (results.values.any((List failures) => failures.isNotEmpty)) { - const String indentation = ' '; - printError(failureListHeader); - for (final Directory package in packages) { - final List errors = results[package]!; - if (errors.isNotEmpty) { - final String errorIndentation = indentation * 2; - String errorDetails = errors.join('\n$errorIndentation'); - if (errorDetails.isNotEmpty) { - errorDetails = ':\n$errorIndentation$errorDetails'; - } - printError( - '$indentation${getPackageDescription(package)}$errorDetails'); - } - } - printError(failureListFooter); + if (results.values + .any((PackageResult result) => result.state == RunState.failed)) { + _printFailureSummary(packages, results); throw ToolExit(exitCommandFoundErrors); } - printSuccess('\n\nNo issues found!'); + // Otherwise, print a summary of what ran for ease of auditing that all the + // expected tests ran. + _printRunSummary(packages, results); + + print('\n'); + printSuccess('No issues found!'); } /// Prints the status message indicating that the command is being run for @@ -167,4 +222,86 @@ abstract class PackageLoopingCommand extends PluginCommand { } print(Colorize(heading)..cyan()); } + + /// Prints a summary of packges run, packages skipped, and warnings. + void _printRunSummary( + List packages, Map results) { + final Set skippedPackages = results.entries + .where((MapEntry entry) => + entry.value.state == RunState.skipped) + .map((MapEntry entry) => entry.key) + .toSet(); + final int skipCount = skippedPackages.length; + // Split the warnings into those from packages that ran, and those that + // were skipped. + final Set _skippedPackagesWithWarnings = + _packagesWithWarnings.intersection(skippedPackages); + final int skippedWarningCount = _skippedPackagesWithWarnings.length; + final int runWarningCount = + _packagesWithWarnings.length - skippedWarningCount; + + final String runWarningSummary = + runWarningCount > 0 ? ' ($runWarningCount with warnings)' : ''; + final String skippedWarningSummary = + runWarningCount > 0 ? ' ($skippedWarningCount with warnings)' : ''; + print('------------------------------------------------------------'); + if (hasLongOutput) { + _printPerPackageRunOverview(packages, skipped: skippedPackages); + } + print( + 'Ran for ${packages.length - skipCount} package(s)$runWarningSummary'); + if (skipCount > 0) { + print('Skipped $skipCount package(s)$skippedWarningSummary'); + } + if (_otherWarningCount > 0) { + print('$_otherWarningCount warnings not associated with a package'); + } + } + + /// Prints a one-line-per-package overview of the run results for each + /// package. + void _printPerPackageRunOverview(List packages, + {required Set skipped}) { + print('Run overview:'); + for (final Directory package in packages) { + final bool hadWarning = _packagesWithWarnings.contains(package); + Colorize summary; + if (skipped.contains(package)) { + if (hadWarning) { + summary = Colorize('skipped (with warning)')..lightYellow(); + } else { + summary = Colorize('skipped')..darkGray(); + } + } else { + if (hadWarning) { + summary = Colorize('ran (with warning)')..yellow(); + } else { + summary = Colorize('ran')..green(); + } + } + print(' ${getPackageDescription(package)} - $summary'); + } + print(''); + } + + /// Prints a summary of all of the failures from [results]. + void _printFailureSummary( + List packages, Map results) { + const String indentation = ' '; + printError(failureListHeader); + for (final Directory package in packages) { + final PackageResult result = results[package]!; + if (result.state == RunState.failed) { + final String errorIndentation = indentation * 2; + String errorDetails = ''; + if (result.details.isNotEmpty) { + errorDetails = + ':\n$errorIndentation${result.details.join('\n$errorIndentation')}'; + } + printError( + '$indentation${getPackageDescription(package)}$errorDetails'); + } + } + printError(failureListFooter); + } } diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index a4aa7c12913..dc9774d8462 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -118,14 +118,13 @@ class DriveExamplesCommand extends PackageLoopingCommand { } @override - Future> runForPackage(Directory package) async { + Future runForPackage(Directory package) async { if (package.basename.endsWith('_platform_interface') && !package.childDirectory('example').existsSync()) { // Platform interface packages generally aren't intended to have // examples, and don't need integration tests, so skip rather than fail. - printSkip( - 'Platform interfaces are not expected to have integratino tests.'); - return PackageLoopingCommand.success; + return PackageResult.skip( + 'Platform interfaces are not expected to have integration tests.'); } final List deviceFlags = []; @@ -139,9 +138,8 @@ class DriveExamplesCommand extends PackageLoopingCommand { } // If there is no supported target platform, skip the plugin. if (deviceFlags.isEmpty) { - printSkip( + return PackageResult.skip( '${getPackageDescription(package)} does not support any requested platform.'); - return PackageLoopingCommand.success; } int examplesFound = 0; @@ -195,7 +193,9 @@ class DriveExamplesCommand extends PackageLoopingCommand { printError('No driver tests were run ($examplesFound example(s) found).'); errors.add('No tests ran (use --exclude if this is intentional).'); } - return errors; + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); } Future> _getDevicesForPlatform(String platform) async { diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 9f4982b2783..8d1ba995f17 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -100,19 +100,20 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { 'project', getStringArg('project'), ]); + print(''); if (exitCode == 0) { - print('\nFirebase project configured.'); + print('Firebase project configured.'); return; } else { - print( - '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + logWarning( + 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'); } } _firebaseProjectConfigured!.complete(null); } @override - Future> runForPackage(Directory package) async { + Future runForPackage(Directory package) async { if (!package .childDirectory('example') .childDirectory('android') @@ -120,29 +121,26 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { .childDirectory('src') .childDirectory('androidTest') .existsSync()) { - printSkip('No example with androidTest directory'); - return PackageLoopingCommand.success; + return PackageResult.skip('No example with androidTest directory'); } - final List errors = []; - final Directory exampleDirectory = package.childDirectory('example'); final Directory androidDirectory = exampleDirectory.childDirectory('android'); // Ensures that gradle wrapper exists if (!await _ensureGradleWrapperExists(androidDirectory)) { - errors.add('Unable to build example apk'); - return errors; + PackageResult.fail(['Unable to build example apk']); } await _configureFirebaseProject(); if (!await _runGradle(androidDirectory, 'app:assembleAndroidTest')) { - errors.add('Unable to assemble androidTest'); - return errors; + PackageResult.fail(['Unable to assemble androidTest']); } + final List errors = []; + // Used within the loop to ensure a unique GCS output location for each // test file's run. int resultsCounter = 0; @@ -186,7 +184,9 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { errors.add('$testName failed tests'); } } - return errors; + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); } /// Checks that 'gradlew' exists in [androidDirectory], and if not runs a diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index 77b8aa70a6e..1534fccff33 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -28,7 +28,7 @@ class JavaTestCommand extends PackageLoopingCommand { 'command.'; @override - Future> runForPackage(Directory package) async { + Future runForPackage(Directory package) async { final Iterable examplesWithTests = getExamplesForPlugin(package) .where((Directory d) => isFlutterPackage(d) && @@ -44,6 +44,10 @@ class JavaTestCommand extends PackageLoopingCommand { .childDirectory('test') .existsSync())); + if (examplesWithTests.isEmpty) { + return PackageResult.skip('No Java unit tests.'); + } + final List errors = []; for (final Directory example in examplesWithTests) { final String exampleName = p.relative(example.path, from: package.path); @@ -66,6 +70,8 @@ class JavaTestCommand extends PackageLoopingCommand { errors.add('$exampleName tests failed.'); } } - return errors; + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); } } diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 2b4beeb92a1..0bdb7c972cc 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -63,14 +63,22 @@ class LintPodspecsCommand extends PackageLoopingCommand { } @override - Future> runForPackage(Directory package) async { + Future runForPackage(Directory package) async { final List errors = []; - for (final File podspec in await _podspecsToLint(package)) { + + final List podspecs = await _podspecsToLint(package); + if (podspecs.isEmpty) { + return PackageResult.skip('No podspecs.'); + } + + for (final File podspec in podspecs) { if (!await _lintPodspec(podspec)) { errors.add(p.basename(podspec.path)); } } - return errors; + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); } Future> _podspecsToLint(Directory package) async { diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index d257638971d..7d39c7322b7 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -56,14 +56,14 @@ class PubspecCheckCommand extends PackageLoopingCommand { bool get includeSubpackages => true; @override - Future> runForPackage(Directory package) async { + Future runForPackage(Directory package) async { final File pubspec = package.childFile('pubspec.yaml'); final bool passesCheck = !pubspec.existsSync() || await _checkPubspec(pubspec, packageName: package.basename); if (!passesCheck) { - return PackageLoopingCommand.failure; + return PackageResult.fail(); } - return PackageLoopingCommand.success; + return PackageResult.success(); } Future _checkPubspec( diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 2584d70c5fc..b26a7adc9b5 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -111,18 +111,15 @@ class VersionCheckCommand extends PackageLoopingCommand { Future initializeRun() async {} @override - Future> runForPackage(Directory package) async { - final List errors = []; - + Future runForPackage(Directory package) async { final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { - errors.add('Invalid pubspec.yaml.'); - return errors; // No remaining checks make sense. + // No remaining checks make sense, so fail immediately. + return PackageResult.fail(['Invalid pubspec.yaml.']); } if (pubspec.publishTo == 'none') { - printSkip('${indentation}Found "publish_to: none".'); - return PackageLoopingCommand.success; + return PackageResult.skip('Found "publish_to: none".'); } final Version? currentPubspecVersion = pubspec.version; @@ -130,10 +127,12 @@ class VersionCheckCommand extends PackageLoopingCommand { printError('${indentation}No version found in pubspec.yaml. A package ' 'that intentionally has no version should be marked ' '"publish_to: none".'); - errors.add('No pubspec.yaml version.'); - return errors; // No remaining checks make sense. + // No remaining checks make sense, so fail immediately. + PackageResult.fail(['No pubspec.yaml version.']); } + final List errors = []; + if (!await _hasValidVersionChange(package, pubspec: pubspec)) { errors.add('Disallowed version change.'); } @@ -142,7 +141,9 @@ class VersionCheckCommand extends PackageLoopingCommand { errors.add('pubspec.yaml and CHANGELOG.md have different versions'); } - return errors; + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); } @override @@ -210,7 +211,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} if (previousVersion == Version.none) { print('${indentation}Unable to find previous version ' '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); - printWarning( + logWarning( '${indentation}If this plugin is not new, something has gone wrong.'); return true; } diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 3f50feef6fe..c7a454ab75a 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -86,37 +86,58 @@ class XCTestCommand extends PackageLoopingCommand { } @override - Future> runForPackage(Directory package) async { - final List failures = []; - final bool testIos = getBoolArg(kPlatformIos); - final bool testMacos = getBoolArg(kPlatformMacos); - // Only provide the failing platform(s) in the summary if testing multiple - // platforms, otherwise it's just noise. - final bool provideErrorDetails = testIos && testMacos; + Future runForPackage(Directory package) async { + final bool testIos = getBoolArg(kPlatformIos) && + pluginSupportsPlatform(kPlatformIos, package, + requiredMode: PlatformSupport.inline); + final bool testMacos = getBoolArg(kPlatformMacos) && + pluginSupportsPlatform(kPlatformMacos, package, + requiredMode: PlatformSupport.inline); + + final bool multiplePlatformsRequested = + getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos); + if (!(testIos || testMacos)) { + String description; + if (multiplePlatformsRequested) { + description = 'Neither iOS nor macOS is'; + } else if (getBoolArg(kPlatformIos)) { + description = 'iOS is not'; + } else { + description = 'macOS is not'; + } + return PackageResult.skip( + '$description implemented by this plugin package.'); + } + if (multiplePlatformsRequested && (!testIos || !testMacos)) { + print('Only running for ${testIos ? 'iOS' : 'macOS'}\n'); + } + + final List failures = []; if (testIos && !await _testPlugin(package, 'iOS', extraXcrunFlags: _iosDestinationFlags)) { - failures.add(provideErrorDetails ? 'iOS' : ''); + failures.add('iOS'); } if (testMacos && !await _testPlugin(package, 'macOS')) { - failures.add(provideErrorDetails ? 'macOS' : ''); + failures.add('macOS'); } - return failures; + + // Only provide the failing platform in the failure details if testing + // multiple platforms, otherwise it's just noise. + return failures.isEmpty + ? PackageResult.success() + : PackageResult.fail( + multiplePlatformsRequested ? failures : []); } /// Runs all applicable tests for [plugin], printing status and returning - /// success if the tests passed (or did not exist). + /// success if the tests passed. Future _testPlugin( Directory plugin, String platform, { List extraXcrunFlags = const [], }) async { - if (!pluginSupportsPlatform(platform.toLowerCase(), plugin, - requiredMode: PlatformSupport.inline)) { - printSkip('$platform is not implemented by this plugin package.\n'); - return true; - } bool passing = true; for (final Directory example in getExamplesForPlugin(plugin)) { // Running tests and static analyzer. diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index c6febdc26fb..218f448242b 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -44,21 +44,16 @@ void main() { test('building for iOS when plugin is not set up for iOS results in no-op', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + createFakePlugin('plugin', packagesDir, extraFiles: ['example/test']); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint(runner, ['build-examples', '--ios']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - contains('BUILDING $packageName for iOS'), + contains('Running for plugin'), contains('iOS is not supported by this plugin'), ]), ); @@ -113,23 +108,17 @@ void main() { test( 'building for Linux when plugin is not set up for Linux results in no-op', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/test', ]); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint( runner, ['build-examples', '--linux']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - contains('BUILDING $packageName for Linux'), + contains('Running for plugin'), contains('Linux is not supported by this plugin'), ]), ); @@ -176,23 +165,17 @@ void main() { test('building for macos with no implementation results in no-op', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/test', ]); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint( runner, ['build-examples', '--macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - contains('BUILDING $packageName for macOS'), + contains('Running for plugin'), contains('macOS is not supported by this plugin'), ]), ); @@ -239,24 +222,18 @@ void main() { }); test('building for web with no implementation results in no-op', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/test', ]); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint(runner, ['build-examples', '--web']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - contains('BUILDING $packageName for web'), - contains('Web is not supported by this plugin'), + contains('Running for plugin'), + contains('web is not supported by this plugin'), ]), ); @@ -304,29 +281,23 @@ void main() { test( 'building for Windows when plugin is not set up for Windows results in no-op', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/test', ]); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint( runner, ['build-examples', '--windows']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - contains('BUILDING $packageName for Windows'), + contains('Running for plugin'), contains('Windows is not supported by this plugin'), ]), ); - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. + // Output should be empty since running build-examples --windows with no + // Windows implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -368,23 +339,17 @@ void main() { test( 'building for Android when plugin is not set up for Android results in no-op', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/test', ]); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint(runner, ['build-examples', '--apk']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - contains('\nBUILDING APK for $packageName'), + contains('Running for plugin'), contains('Android is not supported by this plugin'), ]), ); @@ -419,7 +384,7 @@ void main() { expect( output, containsAllInOrder([ - '\nBUILDING APK for $packageName', + '\nBUILDING $packageName for Android (apk)', ]), ); diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 1012f764a62..ee5aba5c5f5 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -19,14 +19,20 @@ import '../util.dart'; import 'plugin_command_test.mocks.dart'; // Constants for colorized output start and end. +const String _startErrorColor = '\x1B[31m'; const String _startHeadingColor = '\x1B[36m'; const String _startSkipColor = '\x1B[90m'; +const String _startSkipWithWarningColor = '\x1B[93m'; const String _startSuccessColor = '\x1B[32m'; -const String _startErrorColor = '\x1B[31m'; +const String _startWarningColor = '\x1B[33m'; const String _endColor = '\x1B[0m'; // The filename within a package containing errors to return from runForPackage. const String _errorFile = 'errors'; +// The filename within a package indicating that it should be skipped. +const String _skipFile = 'skip'; +// The filename within a package containing warnings to log during runForPackage. +const String _warningFile = 'warnings'; void main() { late FileSystem fileSystem; @@ -48,6 +54,8 @@ void main() { bool hasLongOutput = true, bool includeSubpackages = false, bool failsDuringInit = false, + bool warnsDuringInit = false, + bool warnsDuringCleanup = false, String? customFailureListHeader, String? customFailureListFooter, }) { @@ -68,6 +76,8 @@ void main() { hasLongOutput: hasLongOutput, includeSubpackages: includeSubpackages, failsDuringInit: failsDuringInit, + warnsDuringInit: warnsDuringInit, + warnsDuringCleanup: warnsDuringCleanup, customFailureListHeader: customFailureListHeader, customFailureListFooter: customFailureListFooter, gitDir: gitDir, @@ -216,7 +226,8 @@ void main() { expect( output, containsAllInOrder([ - '$_startSuccessColor\n\nNo issues found!$_endColor', + '\n', + '${_startSuccessColor}No issues found!$_endColor', ])); }); @@ -314,24 +325,153 @@ void main() { '${_startErrorColor}See above for full details.$_endColor', ])); }); - }); - group('utility', () { - test('printSkip has expected output', () async { + test('logs skips', () async { + createFakePackage('package_a', packagesDir); + final Directory skipPackage = createFakePackage('package_b', packagesDir); + skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); + final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir); + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + '$_startSkipColor SKIPPING: For a reason$_endColor', + ])); + }); + + test('logs warnings', () async { + final Directory warnPackage = createFakePackage('package_a', packagesDir); + warnPackage + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startWarningColor}Warning 1$_endColor', + '${_startWarningColor}Warning 2$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + ])); + }); + + test('prints run summary on success', () async { + final Directory warnPackage1 = + createFakePackage('package_a', packagesDir); + warnPackage1 + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); + final Directory skipPackage = createFakePackage('package_c', packagesDir); + skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); + final Directory skipAndWarnPackage = + createFakePackage('package_d', packagesDir); + skipAndWarnPackage.childFile(_warningFile).writeAsStringSync('Warning'); + skipAndWarnPackage.childFile(_skipFile).writeAsStringSync('See warning'); + final Directory warnPackage2 = + createFakePackage('package_e', packagesDir); + warnPackage2 + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_f', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); - final List printBuffer = []; - Zone.current.fork(specification: ZoneSpecification( - print: (_, __, ___, String message) { - printBuffer.add(message); - }, - )).run(() => command.printSkip('For a reason')); + expect( + output, + containsAllInOrder([ + '------------------------------------------------------------', + 'Ran for 4 package(s) (2 with warnings)', + 'Skipped 2 package(s) (1 with warnings)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + // The long-form summary should not be printed for short-form commands. + expect(output, isNot(contains('Run summary:'))); + expect(output, isNot(contains(contains('package a - ran')))); + }); - expect(printBuffer.first, - '${_startSkipColor}SKIPPING: For a reason$_endColor'); + test('prints long-form run summary for long-output commands', () async { + final Directory warnPackage1 = + createFakePackage('package_a', packagesDir); + warnPackage1 + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); + final Directory skipPackage = createFakePackage('package_c', packagesDir); + skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); + final Directory skipAndWarnPackage = + createFakePackage('package_d', packagesDir); + skipAndWarnPackage.childFile(_warningFile).writeAsStringSync('Warning'); + skipAndWarnPackage.childFile(_skipFile).writeAsStringSync('See warning'); + final Directory warnPackage2 = + createFakePackage('package_e', packagesDir); + warnPackage2 + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_f', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '------------------------------------------------------------', + 'Run overview:', + ' package_a - ${_startWarningColor}ran (with warning)$_endColor', + ' package_b - ${_startSuccessColor}ran$_endColor', + ' package_c - ${_startSkipColor}skipped$_endColor', + ' package_d - ${_startSkipWithWarningColor}skipped (with warning)$_endColor', + ' package_e - ${_startWarningColor}ran (with warning)$_endColor', + ' package_f - ${_startSuccessColor}ran$_endColor', + '', + 'Ran for 4 package(s) (2 with warnings)', + 'Skipped 2 package(s) (1 with warnings)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); }); + test('handles warnings outside of runForPackage', () async { + createFakePackage('package_a', packagesDir); + + final TestPackageLoopingCommand command = createTestCommand( + hasLongOutput: false, + warnsDuringCleanup: true, + warnsDuringInit: true, + ); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '${_startWarningColor}Warning during initializeRun$_endColor', + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startWarningColor}Warning during completeRun$_endColor', + '------------------------------------------------------------', + 'Ran for 1 package(s)', + '2 warnings not associated with a package', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + }); + + group('utility', () { test('getPackageDescription prints packageDir-relative paths by default', () async { final TestPackageLoopingCommand command = @@ -380,6 +520,8 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { this.customFailureListHeader, this.customFailureListFooter, this.failsDuringInit = false, + this.warnsDuringInit = false, + this.warnsDuringCleanup = false, ProcessRunner processRunner = const ProcessRunner(), GitDir? gitDir, }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); @@ -390,6 +532,8 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { final String? customFailureListFooter; final bool failsDuringInit; + final bool warnsDuringInit; + final bool warnsDuringCleanup; @override bool hasLongOutput; @@ -413,20 +557,38 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { @override Future initializeRun() async { + if (warnsDuringInit) { + logWarning('Warning during initializeRun'); + } if (failsDuringInit) { throw ToolExit(2); } } @override - Future> runForPackage(Directory package) async { + Future runForPackage(Directory package) async { checkedPackages.add(package.path); + final File warningFile = package.childFile(_warningFile); + if (warningFile.existsSync()) { + final List warnings = warningFile.readAsLinesSync(); + warnings.forEach(logWarning); + } + final File skipFile = package.childFile(_skipFile); + if (skipFile.existsSync()) { + return PackageResult.skip(skipFile.readAsStringSync()); + } final File errorFile = package.childFile(_errorFile); if (errorFile.existsSync()) { - final List errors = errorFile.readAsLinesSync(); - return errors.isNotEmpty ? errors : PackageLoopingCommand.failure; + return PackageResult.fail(errorFile.readAsLinesSync()); + } + return PackageResult.success(); + } + + @override + Future completeRun() async { + if (warnsDuringInit) { + logWarning('Warning during completeRun'); } - return PackageLoopingCommand.success; } } diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index e441a0f68d9..97a39c6b2bf 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -740,7 +740,7 @@ void main() { containsAllInOrder([ contains('Running for aplugin_platform_interface'), contains( - 'SKIPPING: Platform interfaces are not expected to have integratino tests.'), + 'SKIPPING: Platform interfaces are not expected to have integration tests.'), contains('No issues found!'), ]), ); diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index e317ba924bd..4285a0fee35 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -38,11 +38,8 @@ void main() { mockProcess.exitCodeCompleter.complete(1); processRunner.processToReturn = mockProcess; createFakePlugin('plugin', packagesDir, extraFiles: [ - 'lib/test/should_not_run_e2e.dart', - 'example/test_driver/plugin_e2e.dart', - 'example/test_driver/plugin_e2e_test.dart', + 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/should_not_run_e2e.dart', 'example/android/app/src/androidTest/MainActivityTest.java', ]); @@ -55,8 +52,10 @@ void main() { expect(commandError, isA()); expect( output, - contains( - '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.')); + containsAllInOrder([ + contains( + 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'), + ])); }); test('runs integration tests', () async { diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index 894a5c3fce7..227327ab4e6 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.dart @@ -151,5 +151,21 @@ void main() { ]), ); }); + + test('Skips when running no tests', () async { + createFakePlugin( + 'plugin1', + packagesDir, + ); + + final List output = + await runCapturingPrint(runner, ['java-test']); + + expect( + output, + containsAllInOrder( + [contains('SKIPPING: No Java unit tests.')]), + ); + }); }); } diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index a6a5502913a..90a662d7500 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -187,5 +187,18 @@ void main() { ], )); }); + + test('skips when there are no podspecs', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = + await runCapturingPrint(runner, ['podspecs']); + + expect( + output, + containsAllInOrder( + [contains('SKIPPING: No podspecs.')], + )); + }); }); } diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index b12ad852cda..9db4dac904a 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -150,21 +150,16 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('running with correct destination, exclude 1 plugin', () async { - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.inline - }); - final Directory pluginDirectory2 = - createFakePlugin('plugin2', packagesDir, extraFiles: [ + test('running with correct destination', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/test', ], platformSupport: { kPlatformIos: PlatformSupport.inline }); - final Directory pluginExampleDirectory2 = - pluginDirectory2.childDirectory('example'); + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(0); @@ -176,16 +171,14 @@ void main() { '--ios', _kDestination, 'foo_destination', - '--exclude', - 'plugin1' ]); - expect(output, isNot(contains(contains('Running for plugin1')))); - expect(output, contains(contains('Running for plugin2'))); expect( output, - contains( - contains('Successfully ran iOS xctest for plugin2/example'))); + containsAllInOrder([ + contains('Running for plugin'), + contains('Successfully ran iOS xctest for plugin/example') + ])); expect( processRunner.recordedCalls, @@ -206,7 +199,7 @@ void main() { 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], - pluginExampleDirectory2.path), + pluginExampleDirectory.path), ])); }); @@ -350,5 +343,211 @@ void main() { ])); }); }); + + group('combined', () { + test('runs both iOS and macOS when supported', () async { + final Directory pluginDirectory1 = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformIos: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAll([ + contains('Successfully ran iOS xctest for plugin/example'), + contains('Successfully ran macOS xctest for plugin/example'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only macOS for a macOS plugin', () async { + final Directory pluginDirectory1 = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('Only running for macOS'), + contains('Successfully ran macOS xctest for plugin/example'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only iOS for a iOS plugin', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('Only running for iOS'), + contains('Successfully ran iOS xctest for plugin/example') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when neither are supported', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ]); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains( + 'SKIPPING: Neither iOS nor macOS is implemented by this plugin package.'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + }); }); } From 4b77aaff422d3e43bfd51eb738278ffe15e46a95 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 1 Jul 2021 18:05:54 -0700 Subject: [PATCH 086/249] [flutter_plugin_tools] Minor test cleanup (#4120) - Updates the remaining tests (other than one that still needs to be converted to the new base command, which will be fixed then) that aren't using runCapturingPrint to do so to reduce test log spam. - Simplifies and standardizes the matcher used for ToolExit in tests. --- script/tool/test/analyze_command_test.dart | 6 +-- .../tool/test/common/plugin_command_test.dart | 41 ++++++++++--------- .../create_all_plugins_app_command_test.dart | 6 +-- .../test/publish_plugin_command_test.dart | 16 ++++---- .../tool/test/pubspec_check_command_test.dart | 16 ++++---- script/tool/test/test_command_test.dart | 12 +++--- .../tool/test/version_check_command_test.dart | 8 ++-- script/tool/test/xctest_command_test.dart | 4 +- 8 files changed, 56 insertions(+), 53 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index bdf9910f0b1..757adb62267 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -125,7 +125,7 @@ void main() { extraFiles: ['analysis_options.yaml']); await expectLater(() => runCapturingPrint(runner, ['analyze']), - throwsA(const TypeMatcher())); + throwsA(isA())); }); test('fails .analysis_options', () async { @@ -133,7 +133,7 @@ void main() { extraFiles: ['.analysis_options']); await expectLater(() => runCapturingPrint(runner, ['analyze']), - throwsA(const TypeMatcher())); + throwsA(isA())); }); test('takes an allow list', () async { @@ -168,7 +168,7 @@ void main() { await expectLater( () => runCapturingPrint( runner, ['analyze', '--custom-analysis', '']), - throwsA(const TypeMatcher())); + throwsA(isA())); }); }); } diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index deb8e4f56e2..0c949da07db 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -65,7 +65,7 @@ void main() { test('all plugins from file system', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run(['sample']); + await runCapturingPrint(runner, ['sample']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); @@ -74,7 +74,7 @@ void main() { final Directory plugin2 = createFakePlugin('plugin2', packagesDir); final Directory plugin3 = createFakePlugin('plugin3', thirdPartyPackagesDir); - await runner.run(['sample']); + await runCapturingPrint(runner, ['sample']); expect(plugins, unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); }); @@ -82,7 +82,7 @@ void main() { test('exclude plugins when plugins flag is specified', () async { createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run( + await runCapturingPrint(runner, ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); expect(plugins, unorderedEquals([plugin2.path])); }); @@ -90,14 +90,15 @@ void main() { test('exclude plugins when plugins flag isn\'t specified', () async { createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); - await runner.run(['sample', '--exclude=plugin1,plugin2']); + await runCapturingPrint( + runner, ['sample', '--exclude=plugin1,plugin2']); expect(plugins, unorderedEquals([])); }); test('exclude federated plugins when plugins flag is specified', () async { createFakePlugin('plugin1', packagesDir.childDirectory('federated')); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--plugins=federated/plugin1,plugin2', '--exclude=federated/plugin1' @@ -109,7 +110,7 @@ void main() { () async { createFakePlugin('plugin1', packagesDir.childDirectory('federated')); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--plugins=federated/plugin1,plugin2', '--exclude=federated' @@ -121,7 +122,7 @@ void main() { test('all plugins should be tested if there are no changes.', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -136,7 +137,7 @@ void main() { gitDiffResponse = 'AUTHORS'; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -152,7 +153,7 @@ packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -168,7 +169,7 @@ packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -185,7 +186,7 @@ packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -202,7 +203,7 @@ packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -219,7 +220,7 @@ packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -236,7 +237,7 @@ packages/plugin1/CHANGELOG '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -249,7 +250,7 @@ packages/plugin1/CHANGELOG gitDiffResponse = 'packages/plugin1/plugin1.dart'; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -266,7 +267,7 @@ packages/plugin1/ios/plugin1.m '''; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -284,7 +285,7 @@ packages/plugin2/ios/plugin2.m final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -305,7 +306,7 @@ packages/plugin1/plugin1_web/plugin1_web.dart createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' @@ -325,7 +326,7 @@ packages/plugin3/plugin3.dart createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--plugins=plugin1,plugin2', '--base-sha=master', @@ -345,7 +346,7 @@ packages/plugin3/plugin3.dart createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); - await runner.run([ + await runCapturingPrint(runner, [ 'sample', '--exclude=plugin2,plugin3', '--base-sha=master', diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 5bde5e0dc00..073024a17bb 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -45,7 +45,7 @@ void main() { createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); - await runner.run(['all-plugins-app']); + await runCapturingPrint(runner, ['all-plugins-app']); final List pubspec = appDir.childFile('pubspec.yaml').readAsLinesSync(); @@ -63,7 +63,7 @@ void main() { createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); - await runner.run(['all-plugins-app']); + await runCapturingPrint(runner, ['all-plugins-app']); final List pubspec = appDir.childFile('pubspec.yaml').readAsLinesSync(); @@ -80,7 +80,7 @@ void main() { test('pubspec is compatible with null-safe app code', () async { createFakePlugin('plugina', packagesDir); - await runner.run(['all-plugins-app']); + await runCapturingPrint(runner, ['all-plugins-app']); final String pubspec = appDir.childFile('pubspec.yaml').readAsStringSync(); diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index f060cd2fbfd..497579b02f8 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -77,7 +77,7 @@ void main() { group('Initial validation', () { test('requires a package flag', () async { await expectLater(() => commandRunner.run(['publish-plugin']), - throwsA(const TypeMatcher())); + throwsA(isA())); expect( printedMessages.last, contains('Must specify a package to publish.')); }); @@ -90,7 +90,7 @@ void main() { 'iamerror', '--no-push-tags' ]), - throwsA(const TypeMatcher())); + throwsA(isA())); expect(printedMessages.last, contains('iamerror does not exist')); }); @@ -105,7 +105,7 @@ void main() { testPluginName, '--no-push-tags' ]), - throwsA(const TypeMatcher())); + throwsA(isA())); expect( printedMessages, @@ -119,7 +119,7 @@ void main() { await expectLater( () => commandRunner .run(['publish-plugin', '--package', testPluginName]), - throwsA(const TypeMatcher())); + throwsA(isA())); expect(processRunner.results.last.stderr, contains('No such remote')); }); @@ -248,7 +248,7 @@ void main() { '--no-push-tags', '--no-tag-release', ]), - throwsA(const TypeMatcher())); + throwsA(isA())); expect(printedMessages, contains('Publish foo failed.')); }); @@ -301,7 +301,7 @@ void main() { testPluginName, '--no-push-tags', ]), - throwsA(const TypeMatcher())); + throwsA(isA())); expect(printedMessages, contains('Publish foo failed.')); final String? tag = (await gitDir.runCommand( @@ -327,7 +327,7 @@ void main() { '--package', testPluginName, ]), - throwsA(const TypeMatcher())); + throwsA(isA())); expect(printedMessages, contains('Tag push canceled.')); }); @@ -958,7 +958,7 @@ void main() { await expectLater( () => commandRunner.run( ['publish-plugin', '--all-changed', '--base-sha=HEAD~']), - throwsA(const TypeMatcher())); + throwsA(isA())); expect(processRunner.pushTagsArgs, isEmpty); }); diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 38182a4d183..9e633e21b4a 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -176,7 +176,7 @@ ${devDependenciesSection()} await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); @@ -196,7 +196,7 @@ ${devDependenciesSection()} await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); @@ -216,7 +216,7 @@ ${devDependenciesSection()} await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); @@ -236,7 +236,7 @@ ${devDependenciesSection()} await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); @@ -256,7 +256,7 @@ ${environmentSection()} await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); @@ -276,7 +276,7 @@ ${devDependenciesSection()} await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); @@ -296,7 +296,7 @@ ${dependenciesSection()} await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); @@ -316,7 +316,7 @@ ${dependenciesSection()} await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); }); diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index fdccae3d552..861a485f928 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -36,7 +36,7 @@ void main() { final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); - await runner.run(['test']); + await runCapturingPrint(runner, ['test']); expect( processRunner.recordedCalls, @@ -54,7 +54,7 @@ void main() { final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); - await runner.run(['test']); + await runCapturingPrint(runner, ['test']); expect( processRunner.recordedCalls, @@ -71,7 +71,8 @@ void main() { final Directory packageDir = createFakePackage('b', packagesDir, extraFiles: ['test/empty_test.dart']); - await runner.run(['test', '--enable-experiment=exp1']); + await runCapturingPrint( + runner, ['test', '--enable-experiment=exp1']); expect( processRunner.recordedCalls, @@ -99,7 +100,7 @@ void main() { }, ); - await runner.run(['test']); + await runCapturingPrint(runner, ['test']); expect( processRunner.recordedCalls, @@ -118,7 +119,8 @@ void main() { final Directory packageDir = createFakePackage('b', packagesDir, extraFiles: ['test/empty_test.dart']); - await runner.run(['test', '--enable-experiment=exp1']); + await runCapturingPrint( + runner, ['test', '--enable-experiment=exp1']); expect( processRunner.recordedCalls, diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 4d884692046..6fbed9c691b 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -120,7 +120,7 @@ void main() { await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); expect(gitDirCommands.length, equals(1)); expect( @@ -188,7 +188,7 @@ void main() { await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); @@ -202,7 +202,7 @@ void main() { await expectLater( result, - throwsA(const TypeMatcher()), + throwsA(isA()), ); }); @@ -244,7 +244,7 @@ void main() { runner, ['version-check', '--base-sha=master']); await expectLater( output, - throwsA(const TypeMatcher()), + throwsA(isA()), ); expect(gitDirCommands.length, equals(1)); expect( diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 9db4dac904a..61d30312027 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -106,7 +106,7 @@ void main() { test('Fails if no platforms are provided', () async { expect( - () => runner.run(['xctest']), + () => runCapturingPrint(runner, ['xctest']), throwsA(isA()), ); }); @@ -227,7 +227,7 @@ void main() { // will get this result and they should still be able to parse them correctly. processRunner.resultStdout = jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); - await runner.run(['xctest', '--ios']); + await runCapturingPrint(runner, ['xctest', '--ios']); expect( processRunner.recordedCalls, From b9f601cc7b2e70168ea9087c1531e0a323e18baf Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 2 Jul 2021 07:50:24 -0700 Subject: [PATCH 087/249] [flutter_plugin_tools] Remove most exitOnError:true usage (#4130) The intent of package-looping commands is that we always want to run them for all the targeted packages, accumulating failures, and then report them all at the end, so we don't end up with the problem of only finding errors one at a time. However, some of them were using `exitOnError: true` for the underlying commands, causing the first error to be fatal to the test. This PR: - Removes all use of `exitOnError: true` from the package-looping commands. (It's still used in `format`, but fixing that is a larger issue, and less important due to the way `format` is currently structured.) - Fixes the mock process runner used in our tests to correctly simulate `exitOrError: true`; the fact that it didn't was hiding this problem in test (e.g., `drive-examples` had a test that asserted that all failures were summarized correctly, which passed because in tests it was behaving as if `exitOnError` were false) - Adjusts the mock process runner to allow setting a list of mock result for a specific executable instead of one result for all calls to anything, in order to fix some tests that were broken by the two changes above and unfixable without this (e.g., a test of a command that had been calling one executable with `exitOnError: true` then another with ``exitOnError: false`, where the test was asserting things about the second call failing, which only worked because the first call's failure wasn't actually checked). To limit the scope of the PR, the old method of setting a single result for all calls is still supported for now as a fallback. - Fixes the fact that the mock `run` and `runAndStream` had opposite default exit code behavior when no mock process was set (since that caused me a lot of confusion while fixing the above until I figured it out). --- script/tool/CHANGELOG.md | 2 + script/tool/lib/src/analyze_command.dart | 17 ++++++-- .../tool/lib/src/drive_examples_command.dart | 5 +-- .../lib/src/firebase_test_lab_command.dart | 9 +++- .../tool/lib/src/lint_podspecs_command.dart | 8 +++- .../tool/lib/src/publish_plugin_command.dart | 7 ++-- script/tool/lib/src/xctest_command.dart | 2 +- .../test/drive_examples_command_test.dart | 32 ++++++++++++++- .../test/firebase_test_lab_command_test.dart | 31 +++++++++++++- .../tool/test/lint_podspecs_command_test.dart | 34 ++++++++++++++- script/tool/test/util.dart | 41 +++++++++++++++---- 11 files changed, 160 insertions(+), 28 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index d64655f5b86..db9ecf49302 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,8 @@ files, only `integration_test/*_test.dart`. - Add a summary to the end of successful command runs for commands using the new output format. +- Fixed some cases where a failure in a command for a single package would + immediately abort the test. ## 0.3.0 diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 29e78f3ea4f..adaeb1e616e 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -12,6 +12,7 @@ import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; const int _exitBadCustomAnalysisFile = 2; +const int _exitPackagesGetFailed = 3; /// A command to run Dart analysis on packages. class AnalyzeCommand extends PackageLoopingCommand { @@ -75,7 +76,7 @@ class AnalyzeCommand extends PackageLoopingCommand { /// Ensures that the dependent packages have been fetched for all packages /// (including their sub-packages) that will be analyzed. - Future _runPackagesGetOnTargetPackages() async { + Future _runPackagesGetOnTargetPackages() async { final List packageDirectories = await getPackages().toList(); final Set packagePaths = packageDirectories.map((Directory dir) => dir.path).toSet(); @@ -87,9 +88,14 @@ class AnalyzeCommand extends PackageLoopingCommand { packagePaths.contains(directory.parent.path); }); for (final Directory package in packageDirectories) { - await processRunner.runAndStream('flutter', ['packages', 'get'], - workingDir: package, exitOnError: true); + final int exitCode = await processRunner.runAndStream( + 'flutter', ['packages', 'get'], + workingDir: package); + if (exitCode != 0) { + return false; + } } + return true; } @override @@ -98,7 +104,10 @@ class AnalyzeCommand extends PackageLoopingCommand { _validateAnalysisOptions(); print('Fetching dependencies...'); - await _runPackagesGetOnTargetPackages(); + if (!await _runPackagesGetOnTargetPackages()) { + printError('Unabled to get dependencies.'); + throw ToolExit(_exitPackagesGetFailed); + } // Use the Dart SDK override if one was passed in. final String? dartSdk = argResults![_analysisSdk] as String?; diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index dc9774d8462..1e19535f4a2 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -203,7 +203,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { final ProcessResult result = await processRunner.run( flutterCommand, ['devices', '--machine'], - stdoutEncoding: utf8, exitOnError: true); + stdoutEncoding: utf8); if (result.exitCode != 0) { return deviceIds; } @@ -295,8 +295,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { '--target', p.relative(target.path, from: example.path), ], - workingDir: example, - exitOnError: true); + workingDir: example); if (exitCode != 0) { failures.add(target); } diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 8d1ba995f17..3d68c85fbd1 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -13,6 +13,8 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +const int _exitGcloudAuthFailed = 2; + /// A command to run tests via Firebase test lab. class FirebaseTestLabCommand extends PackageLoopingCommand { /// Creates an instance of the test runner command. @@ -84,16 +86,19 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { if (serviceKey.isEmpty) { print('No --service-key provided; skipping gcloud authorization'); } else { - await processRunner.run( + final io.ProcessResult result = await processRunner.run( 'gcloud', [ 'auth', 'activate-service-account', '--key-file=$serviceKey', ], - exitOnError: true, logOnError: true, ); + if (result.exitCode != 0) { + printError('Unable to activate gcloud account.'); + throw ToolExit(_exitGcloudAuthFailed); + } final int exitCode = await processRunner.runAndStream('gcloud', [ 'config', 'set', diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 0bdb7c972cc..82cce0bd13e 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -14,6 +14,7 @@ import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; const int _exitUnsupportedPlatform = 2; +const int _exitPodNotInstalled = 3; /// Lint the CocoaPod podspecs and run unit tests. /// @@ -53,13 +54,16 @@ class LintPodspecsCommand extends PackageLoopingCommand { throw ToolExit(_exitUnsupportedPlatform); } - await processRunner.run( + final ProcessResult result = await processRunner.run( 'which', ['pod'], workingDir: packagesDir, - exitOnError: true, logOnError: true, ); + if (result.exitCode != 0) { + printError('Unable to find "pod". Make sure it is in your path.'); + throw ToolExit(_exitPodNotInstalled); + } } @override diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 740178829ca..6de53ba2690 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -355,7 +355,6 @@ Safe to ignore if the package is deleted in this commit. 'git', ['tag', tag], workingDir: packageDir, - exitOnError: false, logOnError: true, ); if (result.exitCode != 0) { @@ -402,7 +401,6 @@ Safe to ignore if the package is deleted in this commit. ['status', '--porcelain', '--ignored', packageDir.absolute.path], workingDir: packageDir, logOnError: true, - exitOnError: false, ); if (statusResult.exitCode != 0) { return false; @@ -423,9 +421,11 @@ Safe to ignore if the package is deleted in this commit. 'git', ['remote', 'get-url', remote], workingDir: packagesDir, - exitOnError: true, logOnError: true, ); + if (getRemoteUrlResult.exitCode != 0) { + return null; + } return getRemoteUrlResult.stdout as String?; } @@ -498,7 +498,6 @@ Safe to ignore if the package is deleted in this commit. 'git', ['push', remote.name, tag], workingDir: packagesDir, - exitOnError: false, logOnError: true, ); if (result.exitCode != 0) { diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index c7a454ab75a..cd3b674f8d3 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -184,7 +184,7 @@ class XCTestCommand extends PackageLoopingCommand { '$_kXCRunCommand ${xctestArgs.join(' ')}'; print(completeTestCommand); return processRunner.runAndStream(_kXCRunCommand, xctestArgs, - workingDir: example, exitOnError: false); + workingDir: example); } Future _findAvailableIphoneSimulator() async { diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 97a39c6b2bf..eeac96e56e6 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.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. +import 'dart:io' as io; + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -53,7 +55,9 @@ void main() { final MockProcess mockDevicesProcess = MockProcess(); mockDevicesProcess.exitCodeCompleter.complete(0); mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures - processRunner.processToReturn = mockDevicesProcess; + processRunner.mockProcessesForExecutable['flutter'] = [ + mockDevicesProcess + ]; processRunner.resultStdout = output; } @@ -110,7 +114,31 @@ void main() { ); }); - test('fails if Android if no Android devices are present', () async { + test('fails for iOS if getting devices fails', () async { + setMockFlutterDevicesOutput(hasIosDevice: false); + + // Simulate failure from `flutter devices`. + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockProcess; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No iOS devices'), + ]), + ); + }); + + test('fails for Android if no Android devices are present', () async { + setMockFlutterDevicesOutput(hasAndroidDevice: false); Error? commandError; final List output = await runCapturingPrint( runner, ['drive-examples', '--android'], diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 4285a0fee35..711c383f2d5 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -33,10 +33,12 @@ void main() { runner.addCommand(command); }); - test('retries gcloud set', () async { + test('fails if gcloud auth fails', () async { final MockProcess mockProcess = MockProcess(); mockProcess.exitCodeCompleter.complete(1); - processRunner.processToReturn = mockProcess; + processRunner.mockProcessesForExecutable['gcloud'] = [ + mockProcess + ]; createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', @@ -50,6 +52,31 @@ void main() { }); expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to activate gcloud account.'), + ])); + }); + + test('retries gcloud set', () async { + final MockProcess mockAuthProcess = MockProcess(); + mockAuthProcess.exitCodeCompleter.complete(0); + final MockProcess mockConfigProcess = MockProcess(); + mockConfigProcess.exitCodeCompleter.complete(1); + processRunner.mockProcessesForExecutable['gcloud'] = [ + mockAuthProcess, + mockConfigProcess, + ]; + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final List output = + await runCapturingPrint(runner, ['firebase-test-lab']); + expect( output, containsAllInOrder([ diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 90a662d7500..c61d6a9d928 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.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. +import 'dart:io' as io; + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -160,6 +162,34 @@ void main() { expect(output, contains('Linting plugin1.podspec')); }); + test('fails if pod is missing', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + + // Simulate failure from `which pod`. + final MockProcess mockWhichProcess = MockProcess(); + mockWhichProcess.exitCodeCompleter.complete(1); + processRunner.mockProcessesForExecutable['which'] = [ + mockWhichProcess + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspecs'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains('Unable to find "pod". Make sure it is in your path.'), + ], + )); + }); + test('fails if linting fails', () async { createFakePlugin('plugin1', packagesDir, extraFiles: ['plugin1.podspec']); @@ -167,7 +197,9 @@ void main() { // Simulate failure from `pod`. final MockProcess mockDriveProcess = MockProcess(); mockDriveProcess.exitCodeCompleter.complete(1); - processRunner.processToReturn = mockDriveProcess; + processRunner.mockProcessesForExecutable['pod'] = [ + mockDriveProcess + ]; Error? commandError; final List output = await runCapturingPrint( diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index e71a26fa4eb..b65b1fcaa84 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -252,15 +252,22 @@ Future> runCapturingPrint( /// A mock [ProcessRunner] which records process calls. class RecordingProcessRunner extends ProcessRunner { - io.Process? processToReturn; final List recordedCalls = []; + /// Maps an executable to a list of processes that should be used for each + /// successive call to it via [run], [runAndStream], or [start]. + final Map> mockProcessesForExecutable = + >{}; + /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. String? resultStdout; /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. String? resultStderr; + // Deprecated--do not add new uses. Use mockProcessesForExecutable instead. + io.Process? processToReturn; + @override Future runAndStream( String executable, @@ -269,11 +276,17 @@ class RecordingProcessRunner extends ProcessRunner { bool exitOnError = false, }) async { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - return Future.value( - processToReturn == null ? 0 : await processToReturn!.exitCode); + final io.Process? processToReturn = _getProcessToReturn(executable); + final int exitCode = + processToReturn == null ? 0 : await processToReturn.exitCode; + if (exitOnError && (exitCode != 0)) { + throw io.ProcessException(executable, args); + } + return Future.value(exitCode); } - /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. + /// Returns [io.ProcessResult] created from [mockProcessesForExecutable], + /// [resultStdout], and [resultStderr]. @override Future run( String executable, @@ -286,12 +299,16 @@ class RecordingProcessRunner extends ProcessRunner { }) async { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - final io.Process? process = processToReturn; + final io.Process? process = _getProcessToReturn(executable); final io.ProcessResult result = process == null - ? io.ProcessResult(1, 1, '', '') + ? io.ProcessResult(1, 0, '', '') : io.ProcessResult(process.pid, await process.exitCode, resultStdout ?? process.stdout, resultStderr ?? process.stderr); + if (exitOnError && (result.exitCode != 0)) { + throw io.ProcessException(executable, args); + } + return Future.value(result); } @@ -299,7 +316,17 @@ class RecordingProcessRunner extends ProcessRunner { Future start(String executable, List args, {Directory? workingDirectory}) async { recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); - return Future.value(processToReturn); + return Future.value(_getProcessToReturn(executable)); + } + + io.Process? _getProcessToReturn(String executable) { + io.Process? process; + final List? processes = mockProcessesForExecutable[executable]; + if (processes != null && processes.isNotEmpty) { + process = mockProcessesForExecutable[executable]!.removeAt(0); + } + // Fall back to `processToReturn` for backwards compatibility. + return process ?? processToReturn; } } From 6fe9a8ef828a41e0bdc05e6b8dcf3ea2bad249a4 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 2 Jul 2021 09:30:49 -0700 Subject: [PATCH 088/249] [flutter_plugin_tools] Migrate publish-check to the new base command (#4119) To support this command's --machine flag, which moves all normal output into a field in a JSON struct, adds a way of capturing output and providing it to the command subclass on completion. Part of flutter/flutter#83413 --- .../src/common/package_looping_command.dart | 85 +++++++--- .../tool/lib/src/publish_check_command.dart | 160 +++++++----------- .../common/package_looping_command_test.dart | 37 ++++ .../tool/test/publish_check_command_test.dart | 63 ++++--- 4 files changed, 209 insertions(+), 136 deletions(-) diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index cd3c21db213..de1e3b861f5 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.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. +import 'dart:async'; + import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; import 'package:git/git.dart'; @@ -90,6 +92,10 @@ abstract class PackageLoopingCommand extends PluginCommand { /// opportunity to do any cleanup of run-level state. Future completeRun() async {} + /// If [captureOutput], this is called just before exiting with all captured + /// [output]. + Future handleCapturedOutput(List output) async {} + /// Whether or not the output (if any) of [runForPackage] is long, or short. /// /// This changes the logging that happens at the start of each package's @@ -120,6 +126,14 @@ abstract class PackageLoopingCommand extends PluginCommand { /// context. String get failureListFooter => 'See above for full details.'; + /// If true, all printing (including the summary) will be redirected to a + /// buffer, and provided in a call to [handleCapturedOutput] at the end of + /// the run. + /// + /// Capturing output will disable any colorizing of output from this base + /// class. + bool get captureOutput => false; + // ---------------------------------------- /// Logs that a warning occurred, and prints `warningMessage` in yellow. @@ -162,6 +176,26 @@ abstract class PackageLoopingCommand extends PluginCommand { @override Future run() async { + bool succeeded; + if (captureOutput) { + final List output = []; + final ZoneSpecification logSwitchSpecification = ZoneSpecification( + print: (Zone self, ZoneDelegate parent, Zone zone, String message) { + output.add(message); + }); + succeeded = await runZoned>(_runInternal, + zoneSpecification: logSwitchSpecification); + await handleCapturedOutput(output); + } else { + succeeded = await _runInternal(); + } + + if (!succeeded) { + throw ToolExit(exitCommandFoundErrors); + } + } + + Future _runInternal() async { _packagesWithWarnings.clear(); _otherWarningCount = 0; _currentPackage = null; @@ -178,8 +212,9 @@ abstract class PackageLoopingCommand extends PluginCommand { _printPackageHeading(package); final PackageResult result = await runForPackage(package); if (result.state == RunState.skipped) { - print(Colorize('${indentation}SKIPPING: ${result.details.first}') - ..darkGray()); + final String message = + '${indentation}SKIPPING: ${result.details.first}'; + captureOutput ? print(message) : print(Colorize(message)..darkGray()); } results[package] = result; } @@ -187,11 +222,12 @@ abstract class PackageLoopingCommand extends PluginCommand { completeRun(); + print('\n'); // If there were any errors reported, summarize them and exit. if (results.values .any((PackageResult result) => result.state == RunState.failed)) { _printFailureSummary(packages, results); - throw ToolExit(exitCommandFoundErrors); + return false; } // Otherwise, print a summary of what ran for ease of auditing that all the @@ -199,7 +235,16 @@ abstract class PackageLoopingCommand extends PluginCommand { _printRunSummary(packages, results); print('\n'); - printSuccess('No issues found!'); + _printSuccess('No issues found!'); + return true; + } + + void _printSuccess(String message) { + captureOutput ? print(message) : printSuccess(message); + } + + void _printError(String message) { + captureOutput ? print(message) : printError(message); } /// Prints the status message indicating that the command is being run for @@ -220,7 +265,7 @@ abstract class PackageLoopingCommand extends PluginCommand { } else { heading = '$heading...'; } - print(Colorize(heading)..cyan()); + captureOutput ? print(heading) : print(Colorize(heading)..cyan()); } /// Prints a summary of packges run, packages skipped, and warnings. @@ -265,19 +310,21 @@ abstract class PackageLoopingCommand extends PluginCommand { print('Run overview:'); for (final Directory package in packages) { final bool hadWarning = _packagesWithWarnings.contains(package); - Colorize summary; + Styles style; + String summary; if (skipped.contains(package)) { - if (hadWarning) { - summary = Colorize('skipped (with warning)')..lightYellow(); - } else { - summary = Colorize('skipped')..darkGray(); - } + summary = 'skipped'; + style = hadWarning ? Styles.LIGHT_YELLOW : Styles.DARK_GRAY; } else { - if (hadWarning) { - summary = Colorize('ran (with warning)')..yellow(); - } else { - summary = Colorize('ran')..green(); - } + summary = 'ran'; + style = hadWarning ? Styles.YELLOW : Styles.GREEN; + } + if (hadWarning) { + summary += ' (with warning)'; + } + + if (!captureOutput) { + summary = (Colorize(summary)..apply(style)).toString(); } print(' ${getPackageDescription(package)} - $summary'); } @@ -288,7 +335,7 @@ abstract class PackageLoopingCommand extends PluginCommand { void _printFailureSummary( List packages, Map results) { const String indentation = ' '; - printError(failureListHeader); + _printError(failureListHeader); for (final Directory package in packages) { final PackageResult result = results[package]!; if (result.state == RunState.failed) { @@ -298,10 +345,10 @@ abstract class PackageLoopingCommand extends PluginCommand { errorDetails = ':\n$errorIndentation${result.details.join('\n$errorIndentation')}'; } - printError( + _printError( '$indentation${getPackageDescription(package)}$errorDetails'); } } - printError(failureListFooter); + _printError(failureListFooter); } } diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 82a76609e98..ccafabfddd1 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -6,19 +6,18 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; -import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; import 'package:http/http.dart' as http; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_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 { +class PublishCheckCommand extends PackageLoopingCommand { /// Creates an instance of the publish command. PublishCheckCommand( Directory packagesDir, { @@ -52,12 +51,6 @@ class PublishCheckCommand extends PluginCommand { static const String _statusKey = 'status'; static const String _humanMessageKey = 'humanMessage'; - final List _validStatus = [ - _statusNeedsPublish, - _statusMessageNoPublish, - _statusMessageError - ]; - @override final String name = 'publish-check'; @@ -67,68 +60,55 @@ class PublishCheckCommand extends PluginCommand { final PubVersionFinder _pubVersionFinder; - // The output JSON when the _machineFlag is on. - final Map _machineOutput = {}; - - final List _humanMessages = []; + /// The overall result of the run for machine-readable output. This is the + /// highest value that occurs during the run. + _PublishCheckResult _overallResult = _PublishCheckResult.nothingToPublish; @override - Future run() async { - final ZoneSpecification logSwitchSpecification = ZoneSpecification( - print: (Zone self, ZoneDelegate parent, Zone zone, String message) { - final bool logMachineMessage = getBoolArg(_machineFlag); - if (logMachineMessage && message != _prettyJson(_machineOutput)) { - _humanMessages.add(message); - } else { - parent.print(zone, message); - } - }); + bool get captureOutput => getBoolArg(_machineFlag); - await runZoned(_runCommand, zoneSpecification: logSwitchSpecification); + @override + Future initializeRun() async { + _overallResult = _PublishCheckResult.nothingToPublish; } - Future _runCommand() async { - final List failedPackages = []; - - String status = _statusMessageNoPublish; - await for (final Directory plugin in getPlugins()) { - final _PublishCheckResult result = await _passesPublishCheck(plugin); - switch (result) { - case _PublishCheckResult._notPublished: - if (failedPackages.isEmpty) { - status = _statusNeedsPublish; - } - break; - case _PublishCheckResult._published: - break; - case _PublishCheckResult._error: - failedPackages.add(plugin); - status = _statusMessageError; - break; - } + @override + Future runForPackage(Directory package) async { + final _PublishCheckResult? result = await _passesPublishCheck(package); + if (result == null) { + return PackageResult.skip('Package is marked as unpublishable.'); } + if (result.index > _overallResult.index) { + _overallResult = result; + } + return result == _PublishCheckResult.error + ? PackageResult.fail() + : PackageResult.success(); + } + + @override + Future completeRun() async { _pubVersionFinder.httpClient.close(); + } - if (failedPackages.isNotEmpty) { - final String error = - 'The following ${failedPackages.length} package(s) failed the ' - 'publishing check:'; - final String joinedFailedPackages = failedPackages.join('\n'); - _printImportantStatusMessage('$error\n$joinedFailedPackages', - isError: true); - } else { - _printImportantStatusMessage('All packages passed publish check!', - isError: false); - } + @override + Future handleCapturedOutput(List output) async { + final Map machineOutput = { + _statusKey: _statusStringForResult(_overallResult), + _humanMessageKey: output, + }; - if (getBoolArg(_machineFlag)) { - _setStatus(status); - _machineOutput[_humanMessageKey] = _humanMessages; - print(_prettyJson(_machineOutput)); - } + print(const JsonEncoder.withIndent(' ').convert(machineOutput)); + } - if (failedPackages.isNotEmpty) { - throw ToolExit(1); + String _statusStringForResult(_PublishCheckResult result) { + switch (result) { + case _PublishCheckResult.nothingToPublish: + return _statusMessageNoPublish; + case _PublishCheckResult.needsPublishing: + return _statusNeedsPublish; + case _PublishCheckResult.error: + return _statusMessageError; } } @@ -146,6 +126,7 @@ class PublishCheckCommand extends PluginCommand { } Future _hasValidPublishCheckRun(Directory package) async { + print('Running pub publish --dry-run:'); final io.Process process = await processRunner.start( 'flutter', ['pub', 'publish', '--', '--dry-run'], @@ -198,92 +179,79 @@ class PublishCheckCommand extends PluginCommand { 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); } - Future<_PublishCheckResult> _passesPublishCheck(Directory package) async { + /// Returns the result of the publish check, or null if the package is marked + /// as unpublishable. + Future<_PublishCheckResult?> _passesPublishCheck(Directory package) async { final String packageName = package.basename; - print('Checking that $packageName can be published.'); - final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { print('no pubspec'); - return _PublishCheckResult._error; + return _PublishCheckResult.error; } else if (pubspec.publishTo == 'none') { - print('Package $packageName is marked as unpublishable. Skipping.'); - return _PublishCheckResult._published; + return null; } final Version? version = pubspec.version; final _PublishCheckResult alreadyPublishedResult = - await _checkIfAlreadyPublished( + await _checkPublishingStatus( packageName: packageName, version: version); - if (alreadyPublishedResult == _PublishCheckResult._published) { + if (alreadyPublishedResult == _PublishCheckResult.nothingToPublish) { print( 'Package $packageName version: $version has already be published on pub.'); return alreadyPublishedResult; - } else if (alreadyPublishedResult == _PublishCheckResult._error) { + } else if (alreadyPublishedResult == _PublishCheckResult.error) { print('Check pub version failed $packageName'); - return _PublishCheckResult._error; + return _PublishCheckResult.error; } if (await _hasValidPublishCheckRun(package)) { print('Package $packageName is able to be published.'); - return _PublishCheckResult._notPublished; + return _PublishCheckResult.needsPublishing; } else { print('Unable to publish $packageName'); - return _PublishCheckResult._error; + return _PublishCheckResult.error; } } // Check if `packageName` already has `version` published on pub. - Future<_PublishCheckResult> _checkIfAlreadyPublished( + Future<_PublishCheckResult> _checkPublishingStatus( {required String packageName, required Version? version}) async { final PubVersionFinderResponse pubVersionFinderResponse = await _pubVersionFinder.getPackageVersion(package: packageName); switch (pubVersionFinderResponse.result) { case PubVersionFinderResult.success: return pubVersionFinderResponse.versions.contains(version) - ? _PublishCheckResult._published - : _PublishCheckResult._notPublished; + ? _PublishCheckResult.nothingToPublish + : _PublishCheckResult.needsPublishing; case PubVersionFinderResult.fail: print(''' Error fetching version on pub for $packageName. HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} HTTP response: ${pubVersionFinderResponse.httpResponse.body} '''); - return _PublishCheckResult._error; + return _PublishCheckResult.error; case PubVersionFinderResult.noPackageFound: - return _PublishCheckResult._notPublished; + return _PublishCheckResult.needsPublishing; } } - void _setStatus(String status) { - assert(_validStatus.contains(status)); - _machineOutput[_statusKey] = status; - } - - String _prettyJson(Map map) { - return const JsonEncoder.withIndent(' ').convert(_machineOutput); - } - void _printImportantStatusMessage(String message, {required bool isError}) { final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; if (getBoolArg(_machineFlag)) { print(statusMessage); } else { - final Colorize colorizedMessage = Colorize(statusMessage); if (isError) { - colorizedMessage.red(); + printError(statusMessage); } else { - colorizedMessage.green(); + printSuccess(statusMessage); } - print(colorizedMessage); } } } +/// Possible outcomes of of a publishing check. enum _PublishCheckResult { - _notPublished, - - _published, - - _error, + nothingToPublish, + needsPublishing, + error, } diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index ee5aba5c5f5..917fbc0fd67 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -56,6 +56,7 @@ void main() { bool failsDuringInit = false, bool warnsDuringInit = false, bool warnsDuringCleanup = false, + bool captureOutput = false, String? customFailureListHeader, String? customFailureListFooter, }) { @@ -80,6 +81,7 @@ void main() { warnsDuringCleanup: warnsDuringCleanup, customFailureListHeader: customFailureListHeader, customFailureListFooter: customFailureListFooter, + captureOutput: captureOutput, gitDir: gitDir, ); } @@ -254,6 +256,7 @@ void main() { expect( output, containsAllInOrder([ + '\n', '${_startErrorColor}The following packages had errors:$_endColor', '$_startErrorColor package_b$_endColor', '$_startErrorColor package_d$_endColor', @@ -285,6 +288,7 @@ void main() { expect( output, containsAllInOrder([ + '\n', '${_startErrorColor}This is a custom header$_endColor', '$_startErrorColor package_b$_endColor', '$_startErrorColor package_d$_endColor', @@ -319,6 +323,7 @@ void main() { expect( output, containsAllInOrder([ + '\n', '${_startErrorColor}The following packages had errors:$_endColor', '$_startErrorColor package_b:\n just one detail$_endColor', '$_startErrorColor package_d:\n first detail\n second detail$_endColor', @@ -326,6 +331,28 @@ void main() { ])); }); + test('is captured, not printed, when requested', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true, captureOutput: true); + final List output = await runCommand(command); + + expect(output, isEmpty); + + // None of the output should be colorized when captured. + const String separator = + '============================================================'; + expect( + command.capturedOutput, + containsAllInOrder([ + '\n$separator\n|| Running for package_a\n$separator\n', + '\n$separator\n|| Running for package_b\n$separator\n', + 'No issues found!', + ])); + }); + test('logs skips', () async { createFakePackage('package_a', packagesDir); final Directory skipPackage = createFakePackage('package_b', packagesDir); @@ -522,11 +549,13 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { this.failsDuringInit = false, this.warnsDuringInit = false, this.warnsDuringCleanup = false, + this.captureOutput = false, ProcessRunner processRunner = const ProcessRunner(), GitDir? gitDir, }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); final List checkedPackages = []; + final List capturedOutput = []; final String? customFailureListHeader; final String? customFailureListFooter; @@ -549,6 +578,9 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { String get failureListFooter => customFailureListFooter ?? super.failureListFooter; + @override + bool captureOutput; + @override final String name = 'loop-test'; @@ -590,6 +622,11 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { logWarning('Warning during completeRun'); } } + + @override + Future handleCapturedOutput(List output) async { + capturedOutput.addAll(output); + } } class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 26938cc9279..a02770ec2e7 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -51,7 +51,7 @@ void main() { processRunner.processesToReturn.add( MockProcess()..exitCodeCompleter.complete(0), ); - await runner.run(['publish-check']); + await runCapturingPrint(runner, ['publish-check']); expect( processRunner.recordedCalls, @@ -78,7 +78,7 @@ void main() { processRunner.processesToReturn.add(process); expect( - () => runner.run(['publish-check']), + () => runCapturingPrint(runner, ['publish-check']), throwsA(isA()), ); }); @@ -90,7 +90,7 @@ void main() { final MockProcess process = MockProcess(); processRunner.processesToReturn.add(process); - expect(() => runner.run(['publish-check']), + expect(() => runCapturingPrint(runner, ['publish-check']), throwsA(isA())); }); @@ -109,7 +109,9 @@ void main() { processRunner.processesToReturn.add(process); - expect(runner.run(['publish-check', '--allow-pre-release']), + expect( + runCapturingPrint( + runner, ['publish-check', '--allow-pre-release']), completes); }); @@ -128,7 +130,8 @@ void main() { processRunner.processesToReturn.add(process); - expect(runner.run(['publish-check']), throwsA(isA())); + expect(runCapturingPrint(runner, ['publish-check']), + throwsA(isA())); }); test('Success message on stderr is not printed as an error', () async { @@ -197,16 +200,23 @@ void main() { final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); - // ignore: use_raw_strings - expect(output.first, ''' + expect(output.first, r''' { "status": "no-publish", "humanMessage": [ - "Checking that no_publish_a can be published.", + "\n============================================================\n|| Running for no_publish_a\n============================================================\n", "Package no_publish_a version: 0.1.0 has already be published on pub.", - "Checking that no_publish_b can be published.", + "\n============================================================\n|| Running for no_publish_b\n============================================================\n", "Package no_publish_b version: 0.2.0 has already be published on pub.", - "SUCCESS: All packages passed publish check!" + "\n", + "------------------------------------------------------------", + "Run overview:", + " no_publish_a - ran", + " no_publish_b - ran", + "", + "Ran for 2 package(s)", + "\n", + "No issues found!" ] }'''); }); @@ -257,16 +267,24 @@ void main() { final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); - // ignore: use_raw_strings - expect(output.first, ''' + expect(output.first, r''' { "status": "needs-publish", "humanMessage": [ - "Checking that no_publish_a can be published.", + "\n============================================================\n|| Running for no_publish_a\n============================================================\n", "Package no_publish_a version: 0.1.0 has already be published on pub.", - "Checking that no_publish_b can be published.", + "\n============================================================\n|| Running for no_publish_b\n============================================================\n", + "Running pub publish --dry-run:", "Package no_publish_b is able to be published.", - "SUCCESS: All packages passed publish check!" + "\n", + "------------------------------------------------------------", + "Run overview:", + " no_publish_a - ran", + " no_publish_b - ran", + "", + "Ran for 2 package(s)", + "\n", + "No issues found!" ] }'''); }); @@ -328,19 +346,22 @@ void main() { }); expect(hasError, isTrue); - // ignore: use_raw_strings - expect(output.first, ''' + expect(output.first, r''' { "status": "error", "humanMessage": [ - "Checking that no_publish_a can be published.", - "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException: line 1, column 1: Not a map\\n ╷\\n1 │ bad-yaml\\n │ ^^^^^^^^\\n ╵}", + "\n============================================================\n|| Running for no_publish_a\n============================================================\n", + "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException: line 1, column 1: Not a map\n ╷\n1 │ bad-yaml\n │ ^^^^^^^^\n ╵}", "no pubspec", - "Checking that no_publish_b can be published.", + "\n============================================================\n|| Running for no_publish_b\n============================================================\n", "url https://pub.dev/packages/no_publish_b.json", "no_publish_b.json", + "Running pub publish --dry-run:", "Package no_publish_b is able to be published.", - "ERROR: The following 1 package(s) failed the publishing check:\\nMemoryDirectory: '/packages/no_publish_a'" + "\n", + "The following packages had errors:", + " no_publish_a", + "See above for full details." ] }'''); }); From d2761ab1deef3c799780220f0ac4caa8856f051e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 2 Jul 2021 10:49:04 -0700 Subject: [PATCH 089/249] [flutter_plugin_tool] Add more failure test coverage (#4132) Many commands had insufficient failure testing. This adds new tests that ensure that for every Process call, at least one test fails if a failure from that process were ignored (with the exception of calls in the `publish` command, which has a custom process mocking system, so was out of scope here; it already has more coverage than most tests did though.) For a few existing failure tests, adds output checks to ensure that they are testing for the *right* failures. Other changes: - Adds convenience constructors to MockProcess for the common cases of a mock process that just exits with a 0 or 1 status, to reduce test verbosity. - Fixes a few bugs that were found by the new tests. - Minor test cleanup, especially cases where a mock process was being set up just to make all calls succeed, which is the default as of recent changes. --- script/tool/lib/src/analyze_command.dart | 4 +- .../lib/src/firebase_test_lab_command.dart | 6 +- script/tool/lib/src/java_test_command.dart | 6 +- .../tool/lib/src/version_check_command.dart | 2 +- script/tool/test/analyze_command_test.dart | 98 +++++++--- .../test/build_examples_command_test.dart | 180 ++++++++---------- .../test/drive_examples_command_test.dart | 19 +- .../test/firebase_test_lab_command_test.dart | 167 +++++++++++++++- script/tool/test/java_test_command_test.dart | 16 +- .../tool/test/lint_podspecs_command_test.dart | 47 +++-- script/tool/test/mocks.dart | 12 ++ .../tool/test/publish_check_command_test.dart | 25 +-- script/tool/test/test_command_test.dart | 74 +++++++ script/tool/test/xctest_command_test.dart | 121 ++++++++---- 14 files changed, 557 insertions(+), 220 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index adaeb1e616e..b8458da5228 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -66,7 +66,7 @@ class AnalyzeCommand extends PackageLoopingCommand { } printError( - 'Found an extra analysis_options.yaml in ${file.absolute.path}.'); + 'Found an extra analysis_options.yaml at ${file.absolute.path}.'); printError( 'If this was deliberate, pass the package to the analyze command ' 'with the --$_customAnalysisFlag flag and try again.'); @@ -105,7 +105,7 @@ class AnalyzeCommand extends PackageLoopingCommand { print('Fetching dependencies...'); if (!await _runPackagesGetOnTargetPackages()) { - printError('Unabled to get dependencies.'); + printError('Unable to get dependencies.'); throw ToolExit(_exitPackagesGetFailed); } diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 3d68c85fbd1..8253ceeda86 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -135,13 +135,13 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { // Ensures that gradle wrapper exists if (!await _ensureGradleWrapperExists(androidDirectory)) { - PackageResult.fail(['Unable to build example apk']); + return PackageResult.fail(['Unable to build example apk']); } await _configureFirebaseProject(); if (!await _runGradle(androidDirectory, 'app:assembleAndroidTest')) { - PackageResult.fail(['Unable to assemble androidTest']); + return PackageResult.fail(['Unable to assemble androidTest']); } final List errors = []; @@ -236,7 +236,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { : null; final int exitCode = await processRunner.runAndStream( - p.join(directory.path, _gradleWrapper), + directory.childFile(_gradleWrapper).path, [ target, '-Pverbose=true', diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index 1534fccff33..352197be305 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -54,7 +54,8 @@ class JavaTestCommand extends PackageLoopingCommand { print('\nRUNNING JAVA TESTS for $exampleName'); final Directory androidDirectory = example.childDirectory('android'); - if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { + final File gradleFile = androidDirectory.childFile(_gradleWrapper); + if (!gradleFile.existsSync()) { printError('ERROR: Run "flutter build apk" on $exampleName, or run ' 'this tool\'s "build-examples --apk" command, ' 'before executing tests.'); @@ -63,8 +64,7 @@ class JavaTestCommand extends PackageLoopingCommand { } final int exitCode = await processRunner.runAndStream( - p.join(androidDirectory.path, _gradleWrapper), - ['testDebugUnitTest', '--info'], + gradleFile.path, ['testDebugUnitTest', '--info'], workingDir: androidDirectory); if (exitCode != 0) { errors.add('$exampleName tests failed.'); diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index b26a7adc9b5..f0902f01683 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -128,7 +128,7 @@ class VersionCheckCommand extends PackageLoopingCommand { 'that intentionally has no version should be marked ' '"publish_to: none".'); // No remaining checks make sense, so fail immediately. - PackageResult.fail(['No pubspec.yaml version.']); + return PackageResult.fail(['No pubspec.yaml version.']); } final List errors = []; diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 757adb62267..768463f0a5a 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.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. +import 'dart:io' as io; + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -33,9 +35,6 @@ void main() { final Directory plugin1Dir = createFakePlugin('a', packagesDir); final Directory plugin2Dir = createFakePlugin('b', packagesDir); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; await runCapturingPrint(runner, ['analyze']); expect( @@ -55,9 +54,6 @@ void main() { test('skips flutter pub get for examples', () async { final Directory plugin1Dir = createFakePlugin('a', packagesDir); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; await runCapturingPrint(runner, ['analyze']); expect( @@ -74,9 +70,6 @@ void main() { final Directory plugin1Dir = createFakePlugin('a', packagesDir); final Directory plugin2Dir = createFakePlugin('example', packagesDir); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; await runCapturingPrint(runner, ['analyze']); expect( @@ -96,9 +89,6 @@ void main() { test('uses a separate analysis sdk', () async { final Directory pluginDir = createFakePlugin('a', packagesDir); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; await runCapturingPrint( runner, ['analyze', '--analysis-sdk', 'foo/bar/baz']); @@ -124,25 +114,46 @@ void main() { createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); - await expectLater(() => runCapturingPrint(runner, ['analyze']), - throwsA(isA())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found an extra analysis_options.yaml at /packages/foo/analysis_options.yaml'), + ]), + ); }); test('fails .analysis_options', () async { createFakePlugin('foo', packagesDir, extraFiles: ['.analysis_options']); - await expectLater(() => runCapturingPrint(runner, ['analyze']), - throwsA(isA())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found an extra analysis_options.yaml at /packages/foo/.analysis_options'), + ]), + ); }); test('takes an allow list', () async { final Directory pluginDir = createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; await runCapturingPrint( runner, ['analyze', '--custom-analysis', 'foo']); @@ -161,14 +172,55 @@ void main() { createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; - await expectLater( () => runCapturingPrint( runner, ['analyze', '--custom-analysis', '']), throwsA(isA())); }); }); + + test('fails if "packages get" fails', () async { + createFakePlugin('foo', packagesDir); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess.failing() // flutter packages get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to get dependencies'), + ]), + ); + }); + + test('fails if "analyze" fails', () async { + createFakePlugin('foo', packagesDir); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess.failing() // dart analyze + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' foo'), + ]), + ); + }); } diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 218f448242b..c0c90a15c71 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.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. +import 'dart:io' as io; + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -12,6 +14,7 @@ import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { @@ -36,16 +39,49 @@ void main() { }); test('fails if no plaform flags are passed', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['build-examples'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( - () => runCapturingPrint(runner, ['build-examples']), - throwsA(isA()), - ); + output, + containsAllInOrder([ + contains('At least one platform must be provided'), + ])); + }); + + test('fails if building fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess.failing() // flutter packages get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin:\n' + ' plugin/example (iOS)'), + ])); }); test('building for iOS when plugin is not set up for iOS results in no-op', () async { - createFakePlugin('plugin', packagesDir, - extraFiles: ['example/test']); + createFakePlugin('plugin', packagesDir); final List output = await runCapturingPrint(runner, ['build-examples', '--ios']); @@ -64,16 +100,10 @@ void main() { }); test('building for ios', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - ], - platformSupport: { - kPlatformIos: PlatformSupport.inline - }, - ); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -108,9 +138,7 @@ void main() { test( 'building for Linux when plugin is not set up for Linux results in no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ]); + createFakePlugin('plugin', packagesDir); final List output = await runCapturingPrint( runner, ['build-examples', '--linux']); @@ -129,16 +157,10 @@ void main() { }); test('building for Linux', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - ], - platformSupport: { - kPlatformLinux: PlatformSupport.inline, - }, - ); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformLinux: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -165,9 +187,7 @@ void main() { test('building for macos with no implementation results in no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ]); + createFakePlugin('plugin', packagesDir); final List output = await runCapturingPrint( runner, ['build-examples', '--macos']); @@ -186,17 +206,10 @@ void main() { }); test('building for macos', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - 'example/macos/macos.swift', - ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }, - ); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -222,9 +235,7 @@ void main() { }); test('building for web with no implementation results in no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ]); + createFakePlugin('plugin', packagesDir); final List output = await runCapturingPrint(runner, ['build-examples', '--web']); @@ -243,17 +254,10 @@ void main() { }); test('building for web', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - 'example/web/index.html', - ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, - }, - ); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -281,9 +285,7 @@ void main() { test( 'building for Windows when plugin is not set up for Windows results in no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ]); + createFakePlugin('plugin', packagesDir); final List output = await runCapturingPrint( runner, ['build-examples', '--windows']); @@ -302,16 +304,10 @@ void main() { }); test('building for windows', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - ], - platformSupport: { - kPlatformWindows: PlatformSupport.inline - }, - ); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformWindows: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -339,9 +335,7 @@ void main() { test( 'building for Android when plugin is not set up for Android results in no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ]); + createFakePlugin('plugin', packagesDir); final List output = await runCapturingPrint(runner, ['build-examples', '--apk']); @@ -360,16 +354,10 @@ void main() { }); test('building for android', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - ); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -397,16 +385,10 @@ void main() { }); test('enable-experiment flag for Android', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - ); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -425,16 +407,10 @@ void main() { }); test('enable-experiment flag for ios', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - ], - platformSupport: { - kPlatformIos: PlatformSupport.inline - }, - ); + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline + }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index eeac96e56e6..d22d95f14d5 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -52,8 +52,7 @@ void main() { ]; final String output = '''[${devices.join(',')}]'''; - final MockProcess mockDevicesProcess = MockProcess(); - mockDevicesProcess.exitCodeCompleter.complete(0); + final MockProcess mockDevicesProcess = MockProcess.succeeding(); mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures processRunner.mockProcessesForExecutable['flutter'] = [ mockDevicesProcess @@ -115,12 +114,10 @@ void main() { }); test('fails for iOS if getting devices fails', () async { - setMockFlutterDevicesOutput(hasIosDevice: false); - // Simulate failure from `flutter devices`. - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(1); - processRunner.processToReturn = mockProcess; + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess.failing() + ]; Error? commandError; final List output = await runCapturingPrint( @@ -935,9 +932,11 @@ void main() { ); // Simulate failure from `flutter drive`. - final MockProcess mockDriveProcess = MockProcess(); - mockDriveProcess.exitCodeCompleter.complete(1); - processRunner.processToReturn = mockDriveProcess; + processRunner.mockProcessesForExecutable['flutter'] = [ + // No mock for 'devices', since it's running for macOS. + MockProcess.failing(), // 'drive' #1 + MockProcess.failing(), // 'drive' #2 + ]; Error? commandError; final List output = diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 711c383f2d5..0199eba9598 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -34,10 +34,8 @@ void main() { }); test('fails if gcloud auth fails', () async { - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(1); processRunner.mockProcessesForExecutable['gcloud'] = [ - mockProcess + MockProcess.failing() ]; createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -60,13 +58,9 @@ void main() { }); test('retries gcloud set', () async { - final MockProcess mockAuthProcess = MockProcess(); - mockAuthProcess.exitCodeCompleter.complete(0); - final MockProcess mockConfigProcess = MockProcess(); - mockConfigProcess.exitCodeCompleter.complete(1); processRunner.mockProcessesForExecutable['gcloud'] = [ - mockAuthProcess, - mockConfigProcess, + MockProcess.succeeding(), // auth + MockProcess.failing(), // config ]; createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -158,6 +152,52 @@ void main() { ); }); + test('fails if a test fails', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + processRunner.mockProcessesForExecutable['gcloud'] = [ + MockProcess.succeeding(), // auth + MockProcess.succeeding(), // config + MockProcess.failing(), // integration test #1 + MockProcess.succeeding(), // integration test #2 + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Testing example/integration_test/bar_test.dart...'), + contains('Testing example/integration_test/foo_test.dart...'), + contains('plugin:\n' + ' example/integration_test/bar_test.dart failed tests'), + ]), + ); + }); + test('skips packages with no androidTest directory', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -253,6 +293,115 @@ void main() { ); }); + test('fails if building to generate gradlew fails', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess.failing() // flutter build + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to build example apk'), + ])); + }); + + test('fails if assembleAndroidTest fails', () async { + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to assemble androidTest'), + ])); + }); + + test('fails if assembleDebug fails', () async { + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess.succeeding(), // assembleAndroidTest + MockProcess.failing(), // assembleDebug + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Could not build example/integration_test/foo_test.dart'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' example/integration_test/foo_test.dart failed to build'), + ])); + }); + test('experimental flag', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index 227327ab4e6..9ae95971098 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.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. +import 'dart:io' as io; + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -118,7 +120,7 @@ void main() { }); test('fails when a test fails', () async { - createFakePlugin( + final Directory pluginDir = createFakePlugin( 'plugin1', packagesDir, platformSupport: { @@ -130,10 +132,14 @@ void main() { ], ); - // Simulate failure from `gradlew`. - final MockProcess mockDriveProcess = MockProcess(); - mockDriveProcess.exitCodeCompleter.complete(1); - processRunner.processToReturn = mockDriveProcess; + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess.failing() + ]; Error? commandError; final List output = await runCapturingPrint( diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index c61d6a9d928..1236ec0f501 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -38,10 +38,6 @@ void main() { runner = CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); runner.addCommand(command); - final MockProcess mockLintProcess = MockProcess(); - mockLintProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockLintProcess; - processRunner.recordedCalls.clear(); }); test('only runs on macOS', () async { @@ -79,6 +75,10 @@ void main() { ], ); + processRunner.mockProcessesForExecutable['pod'] = [ + MockProcess.succeeding(), + MockProcess.succeeding(), + ]; processRunner.resultStdout = 'Foo'; processRunner.resultStderr = 'Bar'; @@ -167,10 +167,8 @@ void main() { extraFiles: ['plugin1.podspec']); // Simulate failure from `which pod`. - final MockProcess mockWhichProcess = MockProcess(); - mockWhichProcess.exitCodeCompleter.complete(1); processRunner.mockProcessesForExecutable['which'] = [ - mockWhichProcess + MockProcess.failing(), ]; Error? commandError; @@ -190,15 +188,42 @@ void main() { )); }); - test('fails if linting fails', () async { + test('fails if linting as a framework fails', () async { createFakePlugin('plugin1', packagesDir, extraFiles: ['plugin1.podspec']); // Simulate failure from `pod`. - final MockProcess mockDriveProcess = MockProcess(); - mockDriveProcess.exitCodeCompleter.complete(1); processRunner.mockProcessesForExecutable['pod'] = [ - mockDriveProcess + MockProcess.failing(), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspecs'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains('The following packages had errors:'), + contains('plugin1:\n' + ' plugin1.podspec') + ], + )); + }); + + test('fails if linting as a static library fails', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + + // Simulate failure from the second call to `pod`. + processRunner.mockProcessesForExecutable['pod'] = [ + MockProcess.succeeding(), + MockProcess.failing(), ]; Error? commandError; diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index ba6a03da7bc..02b00658398 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -17,6 +17,18 @@ class MockPlatform extends Mock implements Platform { } class MockProcess extends Mock implements io.Process { + MockProcess(); + + /// A mock process that terminates with exitCode 0. + MockProcess.succeeding() { + exitCodeCompleter.complete(0); + } + + /// A mock process that terminates with exitCode 1. + MockProcess.failing() { + exitCodeCompleter.complete(1); + } + final Completer exitCodeCompleter = Completer(); final StreamController> stdoutController = StreamController>(); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index a02770ec2e7..5140316b451 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -46,10 +46,10 @@ void main() { createFakePlugin('plugin_tools_test_package_b', packagesDir); processRunner.processesToReturn.add( - MockProcess()..exitCodeCompleter.complete(0), + MockProcess.succeeding(), ); processRunner.processesToReturn.add( - MockProcess()..exitCodeCompleter.complete(0), + MockProcess.succeeding(), ); await runCapturingPrint(runner, ['publish-check']); @@ -70,10 +70,9 @@ void main() { test('fail on negative test', () async { createFakePlugin('plugin_tools_test_package_a', packagesDir); - final MockProcess process = MockProcess(); + final MockProcess process = MockProcess.failing(); process.stdoutController.close(); // ignore: unawaited_futures process.stderrController.close(); // ignore: unawaited_futures - process.exitCodeCompleter.complete(1); processRunner.processesToReturn.add(process); @@ -100,13 +99,11 @@ void main() { const String preReleaseOutput = 'Package has 1 warning.' 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; - final MockProcess process = MockProcess(); + final MockProcess process = MockProcess.failing(); process.stdoutController.add(preReleaseOutput.codeUnits); process.stdoutController.close(); // ignore: unawaited_futures process.stderrController.close(); // ignore: unawaited_futures - process.exitCodeCompleter.complete(1); - processRunner.processesToReturn.add(process); expect( @@ -121,13 +118,11 @@ void main() { const String preReleaseOutput = 'Package has 1 warning.' 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; - final MockProcess process = MockProcess(); + final MockProcess process = MockProcess.failing(); process.stdoutController.add(preReleaseOutput.codeUnits); process.stdoutController.close(); // ignore: unawaited_futures process.stderrController.close(); // ignore: unawaited_futures - process.exitCodeCompleter.complete(1); - processRunner.processesToReturn.add(process); expect(runCapturingPrint(runner, ['publish-check']), @@ -139,13 +134,11 @@ void main() { const String publishOutput = 'Package has 0 warnings.'; - final MockProcess process = MockProcess(); + final MockProcess process = MockProcess.succeeding(); process.stderrController.add(publishOutput.codeUnits); process.stdoutController.close(); // ignore: unawaited_futures process.stderrController.close(); // ignore: unawaited_futures - process.exitCodeCompleter.complete(0); - processRunner.processesToReturn.add(process); final List output = @@ -195,7 +188,7 @@ void main() { createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); processRunner.processesToReturn.add( - MockProcess()..exitCodeCompleter.complete(0), + MockProcess.succeeding(), ); final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -261,7 +254,7 @@ void main() { createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); processRunner.processesToReturn.add( - MockProcess()..exitCodeCompleter.complete(0), + MockProcess.succeeding(), ); final List output = await runCapturingPrint( @@ -334,7 +327,7 @@ void main() { await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); processRunner.processesToReturn.add( - MockProcess()..exitCodeCompleter.complete(0), + MockProcess.succeeding(), ); bool hasError = false; diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 861a485f928..831910aad1f 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.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. +import 'dart:io' as io; + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -10,6 +12,7 @@ import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/test_command.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { @@ -49,6 +52,32 @@ void main() { ); }); + test('fails when Flutter tests fail', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['test/empty_test.dart']); + createFakePlugin('plugin2', packagesDir, + extraFiles: ['test/empty_test.dart']); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess.failing(), // plugin 1 test + MockProcess.succeeding(), // plugin 2 test + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Tests for the following packages are failing'), + contains(' * plugin1'), + ])); + }); + test('skips testing plugins without test directory', () async { createFakePlugin('plugin1', packagesDir); final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, @@ -90,6 +119,51 @@ void main() { ); }); + test('fails when getting non-Flutter package dependencies fails', () async { + createFakePackage('a_package', packagesDir, + extraFiles: ['test/empty_test.dart']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess.failing(), // dart pub get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Tests for the following packages are failing'), + ])); + }); + + test('fails when non-Flutter tests fail', () async { + createFakePackage('a_package', packagesDir, + extraFiles: ['test/empty_test.dart']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess.succeeding(), // dart pub get + MockProcess.failing(), // dart pub run test + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Tests for the following packages are failing'), + ])); + }); + test('runs on Chrome for web plugins', () async { final Directory pluginDir = createFakePlugin( 'plugin', diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 61d30312027..10329b18980 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:convert'; +import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; @@ -105,9 +106,18 @@ void main() { }); test('Fails if no platforms are provided', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xctest'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( - () => runCapturingPrint(runner, ['xctest']), - throwsA(isA()), + output, + containsAllInOrder([ + contains('At least one platform flag must be provided'), + ]), ); }); @@ -119,9 +129,6 @@ void main() { kPlatformMacos: PlatformSupport.inline, }); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); expect( @@ -138,9 +145,6 @@ void main() { kPlatformIos: PlatformSupport.federated }); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); expect( @@ -161,9 +165,7 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; + processRunner.processToReturn = MockProcess.succeeding(); processRunner.resultStdout = '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ @@ -215,14 +217,12 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; final Map schemeCommandResult = { 'project': { 'targets': ['bar_scheme', 'foo_scheme'] } }; + processRunner.processToReturn = MockProcess.succeeding(); // For simplicity of the test, we combine all the mock results into a single mock result, each internal command // will get this result and they should still be able to parse them correctly. processRunner.resultStdout = @@ -253,6 +253,43 @@ void main() { pluginExampleDirectory.path), ])); }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'xctest', + '--ios', + _kDestination, + 'foo_destination', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages are failing XCTests:'), + contains(' plugin'), + ])); + }); }); group('macOS', () { @@ -265,9 +302,6 @@ void main() { ], ); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; final List output = await runCapturingPrint(runner, ['xctest', '--macos', _kDestination, 'foo_destination']); expect( @@ -284,9 +318,6 @@ void main() { kPlatformMacos: PlatformSupport.federated, }); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; final List output = await runCapturingPrint(runner, ['xctest', '--macos', _kDestination, 'foo_destination']); expect( @@ -307,9 +338,7 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; + processRunner.processToReturn = MockProcess.succeeding(); processRunner.resultStdout = '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ @@ -342,6 +371,36 @@ void main() { pluginExampleDirectory.path), ])); }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xctest', '--macos'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages are failing XCTests:'), + contains(' plugin'), + ]), + ); + }); }); group('combined', () { @@ -357,9 +416,7 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; + processRunner.processToReturn = MockProcess.succeeding(); processRunner.resultStdout = '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ @@ -426,9 +483,7 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; + processRunner.processToReturn = MockProcess.succeeding(); processRunner.resultStdout = '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ @@ -478,9 +533,7 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; + processRunner.processToReturn = MockProcess.succeeding(); processRunner.resultStdout = '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ @@ -526,9 +579,7 @@ void main() { 'example/test', ]); - final MockProcess mockProcess = MockProcess(); - mockProcess.exitCodeCompleter.complete(0); - processRunner.processToReturn = mockProcess; + processRunner.processToReturn = MockProcess.succeeding(); processRunner.resultStdout = '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ From b7ff215760b769563f308bfd135e9d34be9f3cda Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sun, 4 Jul 2021 09:26:28 -0700 Subject: [PATCH 090/249] [flutter_plugin_tools] Add --packages, and deprecated --plugins (#4134) Most of the tool operates on packages in general, and the targetting done currently by the `--plugins` flag is not actually restricted to plugins, so this makes the name less confusing. Part of https://github.com/flutter/flutter/issues/83413 --- script/tool/CHANGELOG.md | 2 + script/tool/README.md | 10 +-- .../tool/lib/src/common/plugin_command.dart | 12 ++-- .../tool/test/common/plugin_command_test.dart | 70 ++++++++++++++++--- script/tool/test/list_command_test.dart | 10 +-- 5 files changed, 79 insertions(+), 25 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index db9ecf49302..9e9538ce554 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -7,6 +7,8 @@ new output format. - Fixed some cases where a failure in a command for a single package would immediately abort the test. +- Deprecated `--plugins` in favor of new `--packages`. `--plugins` continues to + work for now, but will be removed in the future. ## 0.3.0 diff --git a/script/tool/README.md b/script/tool/README.md index c0ee8756e16..5629dc50646 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -58,21 +58,21 @@ Note that the `plugins` argument, despite the name, applies to any package. ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart format --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart format --packages plugin_name ``` ### Run the Dart Static Analyzer ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart analyze --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart analyze --packages plugin_name ``` ### Run Dart Unit Tests ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart test --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart test --packages plugin_name ``` ### Run XCTests @@ -80,9 +80,9 @@ dart run ./script/tool/bin/flutter_plugin_tools.dart test --plugins plugin_name ```sh cd # For iOS: -dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --ios --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --ios --packages plugin_name # For macOS: -dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --macos --plugins plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --macos --packages plugin_name ``` ### Publish a Release diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 43d0d0b822c..74f607dde7c 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -24,11 +24,12 @@ abstract class PluginCommand extends Command { GitDir? gitDir, }) : _gitDir = gitDir { argParser.addMultiOption( - _pluginsArg, + _packagesArg, splitCommas: true, help: - 'Specifies which plugins the command should run on (before sharding).', - valueHelp: 'plugin1,plugin2,...', + 'Specifies which packages the command should run on (before sharding).\n', + valueHelp: 'package1,package2,...', + aliases: [_pluginsArg], ); argParser.addOption( _shardIndexArg, @@ -51,7 +52,7 @@ abstract class PluginCommand extends Command { ); argParser.addFlag(_runOnChangedPackagesArg, help: 'Run the command on changed packages/plugins.\n' - 'If the $_pluginsArg is specified, this flag is ignored.\n' + 'If the $_packagesArg 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' @@ -63,6 +64,7 @@ abstract class PluginCommand extends Command { } static const String _pluginsArg = 'plugins'; + static const String _packagesArg = 'packages'; static const String _shardIndexArg = 'shardIndex'; static const String _shardCountArg = 'shardCount'; static const String _excludeArg = 'exclude'; @@ -203,7 +205,7 @@ abstract class PluginCommand extends Command { /// 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)); + Set plugins = Set.from(getStringListArg(_packagesArg)); final Set excludedPlugins = Set.from(getStringListArg(_excludeArg)); final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 0c949da07db..3f1f1adc4c1 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -69,6 +69,22 @@ void main() { expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); }); + test('includes both plugins and packages', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final Directory package3 = createFakePackage('package3', packagesDir); + final Directory package4 = createFakePackage('package4', packagesDir); + await runCapturingPrint(runner, ['sample']); + expect( + plugins, + unorderedEquals([ + plugin1.path, + plugin2.path, + package3.path, + package4.path, + ])); + }); + test('all plugins includes third_party/packages', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); @@ -79,15 +95,48 @@ void main() { unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); }); - test('exclude plugins when plugins flag is specified', () async { + test('--packages limits packages', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + createFakePackage('package3', packagesDir); + final Directory package4 = createFakePackage('package4', packagesDir); + await runCapturingPrint( + runner, ['sample', '--packages=plugin1,package4']); + expect( + plugins, + unorderedEquals([ + plugin1.path, + package4.path, + ])); + }); + + test('--plugins acts as an alias to --packages', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + createFakePackage('package3', packagesDir); + final Directory package4 = createFakePackage('package4', packagesDir); + await runCapturingPrint( + runner, ['sample', '--plugins=plugin1,package4']); + expect( + plugins, + unorderedEquals([ + plugin1.path, + package4.path, + ])); + }); + + test('exclude packages when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, - ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); + await runCapturingPrint(runner, [ + 'sample', + '--packages=plugin1,plugin2', + '--exclude=plugin1' + ]); expect(plugins, unorderedEquals([plugin2.path])); }); - test('exclude plugins when plugins flag isn\'t specified', () async { + test('exclude packages when packages flag isn\'t specified', () async { createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint( @@ -95,24 +144,24 @@ void main() { expect(plugins, unorderedEquals([])); }); - test('exclude federated plugins when plugins flag is specified', () async { + test('exclude federated plugins when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir.childDirectory('federated')); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ 'sample', - '--plugins=federated/plugin1,plugin2', + '--packages=federated/plugin1,plugin2', '--exclude=federated/plugin1' ]); expect(plugins, unorderedEquals([plugin2.path])); }); - test('exclude entire federated plugins when plugins flag is specified', + test('exclude entire federated plugins when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir.childDirectory('federated')); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ 'sample', - '--plugins=federated/plugin1,plugin2', + '--packages=federated/plugin1,plugin2', '--exclude=federated' ]); expect(plugins, unorderedEquals([plugin2.path])); @@ -315,7 +364,8 @@ packages/plugin1/plugin1_web/plugin1_web.dart expect(plugins, unorderedEquals([plugin1.path])); }); - test('--plugins flag overrides the behavior of --run-on-changed-packages', + test( + '--packages flag overrides the behavior of --run-on-changed-packages', () async { gitDiffResponse = ''' packages/plugin1/plugin1.dart @@ -328,7 +378,7 @@ packages/plugin3/plugin3.dart createFakePlugin('plugin3', packagesDir); await runCapturingPrint(runner, [ 'sample', - '--plugins=plugin1,plugin2', + '--packages=plugin1,plugin2', '--base-sha=master', '--run-on-changed-packages' ]); diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index 22f00ea046c..836d06671c2 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -139,7 +139,7 @@ void main() { ); }); - test('can filter plugins with the --plugins argument', () async { + test('can filter plugins with the --packages argument', () async { createFakePlugin('plugin1', packagesDir); // Create a federated plugin by creating a directory under the packages @@ -157,7 +157,7 @@ void main() { createFakePubspec(macLibrary); List plugins = await runCapturingPrint( - runner, ['list', '--plugins=plugin1']); + runner, ['list', '--packages=plugin1']); expect( plugins, unorderedEquals([ @@ -166,7 +166,7 @@ void main() { ); plugins = await runCapturingPrint( - runner, ['list', '--plugins=my_plugin']); + runner, ['list', '--packages=my_plugin']); expect( plugins, unorderedEquals([ @@ -177,7 +177,7 @@ void main() { ); plugins = await runCapturingPrint( - runner, ['list', '--plugins=my_plugin/my_plugin_web']); + runner, ['list', '--packages=my_plugin/my_plugin_web']); expect( plugins, unorderedEquals([ @@ -186,7 +186,7 @@ void main() { ); plugins = await runCapturingPrint(runner, - ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); + ['list', '--packages=my_plugin/my_plugin_web,plugin1']); expect( plugins, unorderedEquals([ From d2bac9116ba2df9f0978b78f224f412a7068db24 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 7 Jul 2021 08:32:03 -0700 Subject: [PATCH 091/249] [flutter_plugin_tools] Migrate 'test' to new base command (#4133) Migrates `test` to the new package looping base command. Refactors the bulk of the logic to helpers for easier understanding of the flow. Part of flutter/flutter#83413 --- script/tool/lib/src/test_command.dart | 123 ++++++++++++------------ script/tool/test/test_command_test.dart | 11 ++- 2 files changed, 66 insertions(+), 68 deletions(-) diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index b7bf261caa8..d06a2841812 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -3,15 +3,14 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:path/path.dart' as p; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; /// A command to run Dart unit tests for packages. -class TestCommand extends PluginCommand { +class TestCommand extends PackageLoopingCommand { /// Creates an instance of the test command. TestCommand( Directory packagesDir, { @@ -20,7 +19,10 @@ class TestCommand extends PluginCommand { argParser.addOption( kEnableExperiment, defaultsTo: '', - help: 'Runs the tests in Dart VM with the given experiments enabled.', + help: + 'Runs Dart unit tests in Dart VM with the given experiments enabled. ' + 'See https://github.com/dart-lang/sdk/blob/master/docs/process/experimental-flags.md ' + 'for details.', ); } @@ -32,72 +34,65 @@ class TestCommand extends PluginCommand { 'This command requires "flutter" to be in your path.'; @override - Future run() async { - final List failingPackages = []; - await for (final Directory packageDir in getPackages()) { - final String packageName = - p.relative(packageDir.path, from: packagesDir.path); - if (!packageDir.childDirectory('test').existsSync()) { - print('SKIPPING $packageName - no test subdirectory'); - continue; - } + Future runForPackage(Directory package) async { + if (!package.childDirectory('test').existsSync()) { + return PackageResult.skip('No test/ directory.'); + } - print('RUNNING $packageName tests...'); + bool passed; + if (isFlutterPackage(package)) { + passed = await _runFlutterTests(package); + } else { + passed = await _runDartTests(package); + } + return passed ? PackageResult.success() : PackageResult.fail(); + } - final String enableExperiment = getStringArg(kEnableExperiment); + /// Runs the Dart tests for a Flutter package, returning true on success. + Future _runFlutterTests(Directory package) async { + final String experiment = getStringArg(kEnableExperiment); - // `flutter test` automatically gets packages. `pub run test` does not. :( - int exitCode = 0; - if (isFlutterPackage(packageDir)) { - final List args = [ - 'test', - '--color', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ]; + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'test', + '--color', + if (experiment.isNotEmpty) '--enable-experiment=$experiment', + // TODO(ditman): Remove this once all plugins are migrated to 'drive'. + if (isWebPlugin(package)) '--platform=chrome', + ], + workingDir: package, + ); + return exitCode == 0; + } - if (isWebPlugin(packageDir)) { - args.add('--platform=chrome'); - } - exitCode = await processRunner.runAndStream( - 'flutter', - args, - workingDir: packageDir, - ); - } else { - exitCode = await processRunner.runAndStream( - 'dart', - ['pub', 'get'], - workingDir: packageDir, - ); - if (exitCode == 0) { - exitCode = await processRunner.runAndStream( - 'dart', - [ - 'pub', - 'run', - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - 'test', - ], - workingDir: packageDir, - ); - } - } - if (exitCode != 0) { - failingPackages.add(packageName); - } + /// Runs the Dart tests for a non-Flutter package, returning true on success. + Future _runDartTests(Directory package) async { + // Unlike `flutter test`, `pub run test` does not automatically get + // packages + int exitCode = await processRunner.runAndStream( + 'dart', + ['pub', 'get'], + workingDir: package, + ); + if (exitCode != 0) { + printError('Unable to fetch dependencies.'); + return false; } - print('\n\n'); - if (failingPackages.isNotEmpty) { - print('Tests for the following packages are failing (see above):'); - for (final String package in failingPackages) { - print(' * $package'); - } - throw ToolExit(1); - } + final String experiment = getStringArg(kEnableExperiment); + + exitCode = await processRunner.runAndStream( + 'dart', + [ + 'pub', + 'run', + if (experiment.isNotEmpty) '--enable-experiment=$experiment', + 'test', + ], + workingDir: package, + ); - print('All tests are passing!'); + return exitCode == 0; } } diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 831910aad1f..ac0ac4b3dd4 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -73,8 +73,8 @@ void main() { expect( output, containsAllInOrder([ - contains('Tests for the following packages are failing'), - contains(' * plugin1'), + contains('The following packages had errors:'), + contains(' plugin1'), ])); }); @@ -137,7 +137,9 @@ void main() { expect( output, containsAllInOrder([ - contains('Tests for the following packages are failing'), + contains('Unable to fetch dependencies'), + contains('The following packages had errors:'), + contains(' a_package'), ])); }); @@ -160,7 +162,8 @@ void main() { expect( output, containsAllInOrder([ - contains('Tests for the following packages are failing'), + contains('The following packages had errors:'), + contains(' a_package'), ])); }); From a63c0eb59f1e585054969ebdb4690cf8d9ba3721 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 7 Jul 2021 20:02:35 -0700 Subject: [PATCH 092/249] [flutter_plugin_tools] Work around banner in drive-examples (#4142) * [flutter_plugin_tools] Work around banner in drive-examples Strip off any unexpected output before parsing `flutter devices --machine` output. Works around a bug in the Flutter tool that can result in banners being printed in `--machine` mode. Fixes https://github.com/flutter/flutter/issues/86052 --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/drive_examples_command.dart | 9 ++++- .../test/drive_examples_command_test.dart | 37 ++++++++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 9e9538ce554..3f31a4953f6 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -9,6 +9,7 @@ immediately abort the test. - Deprecated `--plugins` in favor of new `--packages`. `--plugins` continues to work for now, but will be removed in the future. +- Make `drive-examples` device detection robust against Flutter tool banners. ## 0.3.0 diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 1e19535f4a2..df74119e401 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -208,9 +208,14 @@ class DriveExamplesCommand extends PackageLoopingCommand { return deviceIds; } + String output = result.stdout as String; + // --machine doesn't currently prevent the tool from printing banners; + // see https://github.com/flutter/flutter/issues/86055. This workaround + // can be removed once that is fixed. + output = output.substring(output.indexOf('[')); + final List> devices = - (jsonDecode(result.stdout as String) as List) - .cast>(); + (jsonDecode(output) as List).cast>(); for (final Map deviceInfo in devices) { final String targetPlatform = (deviceInfo['targetPlatform'] as String?) ?? ''; diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index d22d95f14d5..681a9e0e584 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -44,13 +44,22 @@ void main() { void setMockFlutterDevicesOutput({ bool hasIosDevice = true, bool hasAndroidDevice = true, + bool includeBanner = false, }) { + const String updateBanner = ''' +╔════════════════════════════════════════════════════════════════════════════╗ +║ A new version of Flutter is available! ║ +║ ║ +║ To update to the latest version, run "flutter upgrade". ║ +╚════════════════════════════════════════════════════════════════════════════╝ +'''; final List devices = [ if (hasIosDevice) '{"id": "$_fakeIosDevice", "targetPlatform": "ios"}', if (hasAndroidDevice) '{"id": "$_fakeAndroidDevice", "targetPlatform": "android-x86"}', ]; - final String output = '''[${devices.join(',')}]'''; + final String output = + '''${includeBanner ? updateBanner : ''}[${devices.join(',')}]'''; final MockProcess mockDevicesProcess = MockProcess.succeeding(); mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures @@ -113,6 +122,32 @@ void main() { ); }); + test('handles flutter tool banners when checking devices', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/integration_test.dart', + 'example/integration_test/foo_test.dart', + ], + platformSupport: { + kPlatformIos: PlatformSupport.inline, + }, + ); + + setMockFlutterDevicesOutput(includeBanner: true); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + }); + test('fails for iOS if getting devices fails', () async { // Simulate failure from `flutter devices`. processRunner.mockProcessesForExecutable['flutter'] = [ From 7aec160f92f31bb148d3aa46aa2c12d0c817af79 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 8 Jul 2021 12:39:30 -0700 Subject: [PATCH 093/249] [flutter_plugin_tools] Only check target packages in analyze (#4146) Makes validating that there are no unexpected analysis_options.yaml files part of the per-package loop, rather than a pre-check, so that it only runs against the target packages. This makes it easier to run locally on specific packages, since it's not necessary to pass the allow list for every package when targetting just one package. --- script/tool/lib/src/analyze_command.dart | 17 +++++++++-------- script/tool/test/analyze_command_test.dart | 4 ++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index b8458da5228..2c4fc1b8376 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -11,7 +11,6 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; -const int _exitBadCustomAnalysisFile = 2; const int _exitPackagesGetFailed = 3; /// A command to run Dart analysis on packages. @@ -48,8 +47,8 @@ class AnalyzeCommand extends PackageLoopingCommand { final bool hasLongOutput = false; /// Checks that there are no unexpected analysis_options.yaml files. - void _validateAnalysisOptions() { - final List files = packagesDir.listSync(recursive: true); + bool _hasUnexpecetdAnalysisOptions(Directory package) { + final List files = package.listSync(recursive: true); for (final FileSystemEntity file in files) { if (file.basename != 'analysis_options.yaml' && file.basename != '.analysis_options') { @@ -60,7 +59,8 @@ class AnalyzeCommand extends PackageLoopingCommand { (String directory) => directory != null && directory.isNotEmpty && - p.isWithin(p.join(packagesDir.path, directory), file.path)); + p.isWithin( + packagesDir.childDirectory(directory).path, file.path)); if (allowed) { continue; } @@ -70,8 +70,9 @@ class AnalyzeCommand extends PackageLoopingCommand { printError( 'If this was deliberate, pass the package to the analyze command ' 'with the --$_customAnalysisFlag flag and try again.'); - throw ToolExit(_exitBadCustomAnalysisFile); + return true; } + return false; } /// Ensures that the dependent packages have been fetched for all packages @@ -100,9 +101,6 @@ class AnalyzeCommand extends PackageLoopingCommand { @override Future initializeRun() async { - print('Verifying analysis settings...'); - _validateAnalysisOptions(); - print('Fetching dependencies...'); if (!await _runPackagesGetOnTargetPackages()) { printError('Unable to get dependencies.'); @@ -116,6 +114,9 @@ class AnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(Directory package) async { + if (_hasUnexpecetdAnalysisOptions(package)) { + return PackageResult.fail(['Unexpected local analysis options']); + } final int exitCode = await processRunner.runAndStream( _dartBinaryPath, ['analyze', '--fatal-infos'], workingDir: package); diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 768463f0a5a..adeaabaaca5 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -126,6 +126,8 @@ void main() { containsAllInOrder([ contains( 'Found an extra analysis_options.yaml at /packages/foo/analysis_options.yaml'), + contains(' foo:\n' + ' Unexpected local analysis options'), ]), ); }); @@ -146,6 +148,8 @@ void main() { containsAllInOrder([ contains( 'Found an extra analysis_options.yaml at /packages/foo/.analysis_options'), + contains(' foo:\n' + ' Unexpected local analysis options'), ]), ); }); From ac0eed1ac13e63fb4333bfe264e2388192d6b05c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 8 Jul 2021 12:44:31 -0700 Subject: [PATCH 094/249] [flutter_plugin_tools] Improve and test 'format' (#4145) - Adds unit tests, as there are currently none. - Adds more graceful failure handling. - Adds an internal ignore list to skip files that don't need to be formatted that showed up during local testing. - Adds a note explaining that it's intentially not using the new base command due to performance issues. --- script/tool/lib/src/format_command.dart | 141 ++++++--- script/tool/test/format_command_test.dart | 344 ++++++++++++++++++++++ 2 files changed, 440 insertions(+), 45 deletions(-) create mode 100644 script/tool/test/format_command_test.dart diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 5f060d715bf..9d39d93b911 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -14,6 +14,11 @@ import 'common/core.dart'; import 'common/plugin_command.dart'; import 'common/process_runner.dart'; +const int _exitClangFormatFailed = 3; +const int _exitFlutterFormatFailed = 4; +const int _exitJavaFormatFailed = 5; +const int _exitGitFailed = 6; + 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'); @@ -43,14 +48,18 @@ class FormatCommand extends PluginCommand { Future run() async { final String googleFormatterPath = await _getGoogleFormatterPath(); - await _formatDart(); - await _formatJava(googleFormatterPath); - await _formatCppAndObjectiveC(); + // This class is not based on PackageLoopingCommand because running the + // formatters separately for each package is an order of magnitude slower, + // due to the startup overhead of the formatters. + final Iterable files = await _getFilteredFilePaths(getFiles()); + await _formatDart(files); + await _formatJava(files, googleFormatterPath); + await _formatCppAndObjectiveC(files); if (getBoolArg('fail-on-change')) { final bool modified = await _didModifyAnything(); if (modified) { - throw ToolExit(1); + throw ToolExit(exitCommandFoundErrors); } } } @@ -60,9 +69,12 @@ class FormatCommand extends PluginCommand { 'git', ['ls-files', '--modified'], workingDir: packagesDir, - exitOnError: true, logOnError: true, ); + if (modifiedFiles.exitCode != 0) { + printError('Unable to determine changed files.'); + throw ToolExit(_exitGitFailed); + } print('\n\n'); @@ -79,66 +91,105 @@ class FormatCommand extends PluginCommand { 'pub global run flutter_plugin_tools format" or copy-paste ' 'this command into your terminal:'); - print('patch -p1 <['diff'], workingDir: packagesDir, - exitOnError: true, logOnError: true, ); + if (diff.exitCode != 0) { + printError('Unable to determine diff.'); + throw ToolExit(_exitGitFailed); + } + print('patch -p1 < _formatCppAndObjectiveC() async { - print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); - final Iterable allFiles = [ - ...await _getFilesWithExtension('.h'), - ...await _getFilesWithExtension('.m'), - ...await _getFilesWithExtension('.mm'), - ...await _getFilesWithExtension('.cc'), - ...await _getFilesWithExtension('.cpp'), - ]; - // Split this into multiple invocations to avoid a - // 'ProcessException: Argument list too long'. - final Iterable> batches = partition(allFiles, 100); - for (final List batch in batches) { - await processRunner.runAndStream(getStringArg('clang-format'), - ['-i', '--style=Google', ...batch], - workingDir: packagesDir, exitOnError: true); + Future _formatCppAndObjectiveC(Iterable files) async { + final Iterable clangFiles = _getPathsWithExtensions( + files, {'.h', '.m', '.mm', '.cc', '.cpp'}); + if (clangFiles.isNotEmpty) { + print('Formatting .cc, .cpp, .h, .m, and .mm files...'); + final Iterable> batches = partition(clangFiles, 100); + int exitCode = 0; + for (final List batch in batches) { + batch.sort(); // For ease of testing; partition changes the order. + exitCode = await processRunner.runAndStream( + getStringArg('clang-format'), + ['-i', '--style=Google', ...batch], + workingDir: packagesDir); + if (exitCode != 0) { + break; + } + } + if (exitCode != 0) { + printError( + 'Failed to format C, C++, and Objective-C files: exit code $exitCode.'); + throw ToolExit(_exitClangFormatFailed); + } } } - Future _formatJava(String googleFormatterPath) async { - print('Formatting all .java files...'); - final Iterable javaFiles = await _getFilesWithExtension('.java'); - await processRunner.runAndStream('java', - ['-jar', googleFormatterPath, '--replace', ...javaFiles], - workingDir: packagesDir, exitOnError: true); + Future _formatJava( + Iterable files, String googleFormatterPath) async { + final Iterable javaFiles = + _getPathsWithExtensions(files, {'.java'}); + if (javaFiles.isNotEmpty) { + print('Formatting .java files...'); + final int exitCode = await processRunner.runAndStream('java', + ['-jar', googleFormatterPath, '--replace', ...javaFiles], + workingDir: packagesDir); + if (exitCode != 0) { + printError('Failed to format Java files: exit code $exitCode.'); + throw ToolExit(_exitJavaFormatFailed); + } + } } - Future _formatDart() async { - // This actually should be fine for non-Flutter Dart projects, no need to - // specifically shell out to dartfmt -w in that case. - print('Formatting all .dart files...'); - final Iterable dartFiles = await _getFilesWithExtension('.dart'); - if (dartFiles.isEmpty) { - print( - 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); - } else { - await processRunner.runAndStream( + Future _formatDart(Iterable files) async { + final Iterable dartFiles = + _getPathsWithExtensions(files, {'.dart'}); + if (dartFiles.isNotEmpty) { + print('Formatting .dart files...'); + // `flutter format` doesn't require the project to actually be a Flutter + // project. + final int exitCode = await processRunner.runAndStream( 'flutter', ['format', ...dartFiles], - workingDir: packagesDir, exitOnError: true); + workingDir: packagesDir); + if (exitCode != 0) { + printError('Failed to format Dart files: exit code $exitCode.'); + throw ToolExit(_exitFlutterFormatFailed); + } } } - Future> _getFilesWithExtension(String extension) async => - getFiles() - .where((File file) => p.extension(file.path) == extension) - .map((File file) => file.path) - .toList(); + Future> _getFilteredFilePaths(Stream files) async { + // Returns a pattern to check for [directories] as a subset of a file path. + RegExp pathFragmentForDirectories(List directories) { + final String s = p.separator; + return RegExp('(?:^|$s)${p.joinAll(directories)}$s'); + } + + return files + .map((File file) => file.path) + .where((String path) => + // Ignore files in build/ directories (e.g., headers of frameworks) + // to avoid useless extra work in local repositories. + !path.contains( + pathFragmentForDirectories(['example', 'build'])) && + // Ignore files in Pods, which are not part of the repository. + !path.contains(pathFragmentForDirectories(['Pods'])) && + // Ignore .dart_tool/, which can have various intermediate files. + !path.contains(pathFragmentForDirectories(['.dart_tool']))) + .toList(); + } + + Iterable _getPathsWithExtensions( + Iterable files, Set extensions) { + return files.where((String path) => extensions.contains(p.extension(path))); + } Future _getGoogleFormatterPath() async { final String javaFormatterPath = p.join( diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart new file mode 100644 index 00000000000..e7f4d795eb9 --- /dev/null +++ b/script/tool/test/format_command_test.dart @@ -0,0 +1,344 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/format_command.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + late String javaFormatPath; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final FormatCommand analyzeCommand = + FormatCommand(packagesDir, processRunner: processRunner); + + // Create the java formatter file that the command checks for, to avoid + // a download. + javaFormatPath = p.join(p.dirname(p.fromUri(io.Platform.script)), + 'google-java-format-1.3-all-deps.jar'); + fileSystem.file(javaFormatPath).createSync(recursive: true); + + runner = CommandRunner('format_command', 'Test for format_command'); + runner.addCommand(analyzeCommand); + }); + + List _getAbsolutePaths( + Directory package, List relativePaths) { + return relativePaths + .map((String path) => p.join(package.path, path)) + .toList(); + } + + test('formats .dart files', () async { + const List files = [ + 'lib/a.dart', + 'lib/src/b.dart', + 'lib/src/c.dart', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + ['format', ..._getAbsolutePaths(pluginDir, files)], + packagesDir.path), + ])); + }); + + test('fails if flutter format fails', () async { + const List files = [ + 'lib/a.dart', + 'lib/src/b.dart', + 'lib/src/c.dart', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess.failing() + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Failed to format Dart files: exit code 1.'), + ])); + }); + + test('formats .java files', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'java', + [ + '-jar', + javaFormatPath, + '--replace', + ..._getAbsolutePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + + test('fails if Java formatter fails', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['java'] = [ + MockProcess.failing() + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Failed to format Java files: exit code 1.'), + ])); + }); + + test('formats c-ish files', () async { + const List files = [ + 'ios/Classes/Foo.h', + 'ios/Classes/Foo.m', + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + 'macos/Classes/Foo.mm', + 'windows/foo_plugin.cpp', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'clang-format', + [ + '-i', + '--style=Google', + ..._getAbsolutePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + + test('fails if clang-format fails', () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['clang-format'] = [ + MockProcess.failing() + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Failed to format C, C++, and Objective-C files: exit code 1.'), + ])); + }); + + test('skips known non-repo files', () async { + const List skipFiles = [ + '/example/build/SomeFramework.framework/Headers/SomeFramework.h', + '/example/Pods/APod.framework/Headers/APod.h', + '.dart_tool/internals/foo.cc', + '.dart_tool/internals/Bar.java', + '.dart_tool/internals/baz.dart', + ]; + const List clangFiles = ['ios/Classes/Foo.h']; + const List dartFiles = ['lib/a.dart']; + const List javaFiles = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java' + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: [ + ...skipFiles, + // Include some files that should be formatted to validate that it's + // correctly filtering even when running the commands. + ...clangFiles, + ...dartFiles, + ...javaFiles, + ], + ); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall( + 'clang-format', + [ + '-i', + '--style=Google', + ..._getAbsolutePaths(pluginDir, clangFiles) + ], + packagesDir.path), + ProcessCall( + 'flutter', + ['format', ..._getAbsolutePaths(pluginDir, dartFiles)], + packagesDir.path), + ProcessCall( + 'java', + [ + '-jar', + javaFormatPath, + '--replace', + ..._getAbsolutePaths(pluginDir, javaFiles) + ], + packagesDir.path), + ])); + }); + + test('fails if files are changed with --file-on-change', () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess.succeeding(), + ]; + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; + processRunner.resultStdout = changedFilePath; + Error? commandError; + final List output = + await runCapturingPrint(runner, ['format', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('These files are not formatted correctly'), + contains(changedFilePath), + contains('patch -p1 < files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess.failing() + ]; + Error? commandError; + final List output = + await runCapturingPrint(runner, ['format', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to determine changed files.'), + ])); + }); + + test('reports git diff failures', () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess.succeeding(), // ls-files + MockProcess.failing(), // diff + ]; + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; + processRunner.resultStdout = changedFilePath; + Error? commandError; + final List output = + await runCapturingPrint(runner, ['format', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('These files are not formatted correctly'), + contains(changedFilePath), + contains('Unable to determine diff.'), + ])); + }); +} From 77460f03f27d61df40ca175da57d4e504664939e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 9 Jul 2021 16:38:13 -0700 Subject: [PATCH 095/249] [flutter_plugin_tools] Make unit tests pass on Windows (#4149) The purpose of this PR is to make running all unit tests on Windows pass (vs failing a large portion of the tests as currently happens). This does not mean that the commands actually work when run on Windows, or that Windows support is tested, only that it's possible to actually run the tests themselves. This is prep for actually supporting parts of the tool on Windows in future PRs. Major changes: - Make the tests significantly more hermetic: - Make almost all tools take a `Platform` constructor argument that can be used to inject a mock platform to control what OS the command acts like it is running on under test. - Add a path `Context` object to the base command, whose style matches the `Platform`, and use that almost everywhere instead of the top-level `path` functions. - In cases where Posix behavior is always required (such as parsing `git` output), explicitly use the `posix` context object for `path` functions. - Start laying the groundwork for actual Windows support: - Replace all uses of `flutter` as a command with a getter that returns `flutter` or `flutter.bat` as appropriate. - For user messages that include relative paths, use a helper that always uses Posix-style relative paths for consistent output. This bumps the version since quite a few changes have built up, and having a cut point before starting to make more changes to the commands to support Windows seems like a good idea. Part of https://github.com/flutter/flutter/issues/86113 --- script/tool/lib/src/analyze_command.dart | 13 +- .../tool/lib/src/build_examples_command.dart | 7 +- .../src/common/package_looping_command.dart | 19 ++- .../tool/lib/src/common/plugin_command.dart | 18 +- .../src/create_all_plugins_app_command.dart | 22 ++- .../tool/lib/src/drive_examples_command.dart | 18 +- .../lib/src/firebase_test_lab_command.dart | 14 +- script/tool/lib/src/format_command.dart | 22 ++- script/tool/lib/src/java_test_command.dart | 7 +- .../tool/lib/src/license_check_command.dart | 4 +- .../tool/lib/src/lint_podspecs_command.dart | 12 +- script/tool/lib/src/list_command.dart | 6 +- .../tool/lib/src/publish_check_command.dart | 6 +- .../tool/lib/src/publish_plugin_command.dart | 8 +- .../tool/lib/src/pubspec_check_command.dart | 9 +- script/tool/lib/src/test_command.dart | 4 +- .../tool/lib/src/version_check_command.dart | 18 +- script/tool/lib/src/xctest_command.dart | 7 +- script/tool/test/analyze_command_test.dart | 9 +- .../test/build_examples_command_test.dart | 85 +++++----- .../common/package_looping_command_test.dart | 37 ++++- .../tool/test/common/plugin_command_test.dart | 9 +- .../test/drive_examples_command_test.dart | 155 ++++++++---------- .../test/firebase_test_lab_command_test.dart | 9 +- script/tool/test/format_command_test.dart | 15 +- script/tool/test/java_test_command_test.dart | 24 ++- .../tool/test/lint_podspecs_command_test.dart | 17 +- script/tool/test/list_command_test.dart | 6 +- script/tool/test/mocks.dart | 17 +- .../tool/test/publish_check_command_test.dart | 19 ++- .../test/publish_plugin_command_test.dart | 3 +- .../tool/test/pubspec_check_command_test.dart | 10 +- script/tool/test/test_command_test.dart | 32 ++-- script/tool/test/util.dart | 6 + .../tool/test/version_check_command_test.dart | 5 +- script/tool/test/xctest_command_test.dart | 6 +- 36 files changed, 428 insertions(+), 250 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 2c4fc1b8376..e56b95d88eb 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:file/file.dart'; -import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -19,7 +19,8 @@ class AnalyzeCommand extends PackageLoopingCommand { AnalyzeCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, processRunner: processRunner) { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addMultiOption(_customAnalysisFlag, help: 'Directories (comma separated) that are allowed to have their own analysis options.', @@ -57,9 +58,8 @@ class AnalyzeCommand extends PackageLoopingCommand { final bool allowed = (getStringListArg(_customAnalysisFlag)).any( (String directory) => - directory != null && directory.isNotEmpty && - p.isWithin( + path.isWithin( packagesDir.childDirectory(directory).path, file.path)); if (allowed) { continue; @@ -90,7 +90,7 @@ class AnalyzeCommand extends PackageLoopingCommand { }); for (final Directory package in packageDirectories) { final int exitCode = await processRunner.runAndStream( - 'flutter', ['packages', 'get'], + flutterCommand, ['packages', 'get'], workingDir: package); if (exitCode != 0) { return false; @@ -109,7 +109,8 @@ class AnalyzeCommand extends PackageLoopingCommand { // Use the Dart SDK override if one was passed in. final String? dartSdk = argResults![_analysisSdk] as String?; - _dartBinaryPath = dartSdk == null ? 'dart' : p.join(dartSdk, 'bin', 'dart'); + _dartBinaryPath = + dartSdk == null ? 'dart' : path.join(dartSdk, 'bin', 'dart'); } @override diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 32905c83db9..0cac09980c9 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:file/file.dart'; -import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -23,7 +23,8 @@ class BuildExamplesCommand extends PackageLoopingCommand { BuildExamplesCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, processRunner: processRunner) { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addFlag(kPlatformLinux); argParser.addFlag(kPlatformMacos); argParser.addFlag(kPlatformWeb); @@ -127,7 +128,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { for (final Directory example in getExamplesForPlugin(package)) { final String packageName = - p.relative(example.path, from: packagesDir.path); + getRelativePosixPath(example, from: packagesDir); for (final _PlatformDetails platform in buildPlatforms) { String buildPlatform = platform.label; diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index de1e3b861f5..9f4039ec707 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -8,6 +8,7 @@ import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'core.dart'; import 'plugin_command.dart'; @@ -63,8 +64,10 @@ abstract class PackageLoopingCommand extends PluginCommand { PackageLoopingCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), GitDir? gitDir, - }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); + }) : super(packagesDir, + processRunner: processRunner, platform: platform, gitDir: gitDir); /// Packages that had at least one [logWarning] call. final Set _packagesWithWarnings = {}; @@ -158,8 +161,8 @@ abstract class PackageLoopingCommand extends PluginCommand { /// an exact format (e.g., published name, or basename) is required, that /// should be used instead. String getPackageDescription(Directory package) { - String packageName = p.relative(package.path, from: packagesDir.path); - final List components = p.split(packageName); + String packageName = getRelativePosixPath(package, from: packagesDir); + final List components = p.posix.split(packageName); // For the common federated plugin pattern of `foo/foo_subpackage`, drop // the first part since it's not useful. if (components.length == 2 && @@ -169,6 +172,16 @@ abstract class PackageLoopingCommand extends PluginCommand { return packageName; } + /// Returns the relative path from [from] to [entity] in Posix style. + /// + /// This should be used when, for example, printing package-relative paths in + /// status or error messages. + String getRelativePosixPath( + FileSystemEntity entity, { + required Directory from, + }) => + p.posix.joinAll(path.split(path.relative(entity.path, from: from.path))); + /// The suggested indentation for printed output. String get indentation => hasLongOutput ? '' : ' '; diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 74f607dde7c..ecdcb0565d3 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -21,6 +21,7 @@ abstract class PluginCommand extends Command { PluginCommand( this.packagesDir, { this.processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), GitDir? gitDir, }) : _gitDir = gitDir { argParser.addMultiOption( @@ -79,6 +80,11 @@ abstract class PluginCommand extends Command { /// This can be overridden for testing. final ProcessRunner processRunner; + /// The current platform. + /// + /// This can be overridden for testing. + final Platform platform; + /// The git directory to use. If unset, [gitDir] populates it from the /// packages directory's enclosing repository. /// @@ -88,9 +94,11 @@ abstract class PluginCommand extends Command { int? _shardIndex; int? _shardCount; + /// A context that matches the default for [platform]. + p.Context get path => platform.isWindows ? p.windows : p.posix; + /// The command to use when running `flutter`. - String get flutterCommand => - const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + String get flutterCommand => platform.isWindows ? 'flutter.bat' : 'flutter'; /// The shard of the overall command execution that this instance should run. int get shardIndex { @@ -240,9 +248,9 @@ abstract class PluginCommand extends Command { // 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); + path.relative(subdir.path, from: dir.path); + final String packageName = path.basename(subdir.path); + final String basenamePath = path.basename(entity.path); if (!excludedPlugins.contains(basenamePath) && !excludedPlugins.contains(packageName) && !excludedPlugins.contains(relativePath) && 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 fab41bcf4ec..ed701445608 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -5,6 +5,7 @@ import 'dart:io' as io; import 'package:file/file.dart'; +import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; @@ -51,7 +52,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { Future _createApp() async { final io.ProcessResult result = io.Process.runSync( - 'flutter', + flutterCommand, [ 'create', '--template=app', @@ -156,7 +157,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { {}; await for (final Directory package in getPlugins()) { - final String pluginName = package.path.split('/').last; + final String pluginName = package.basename; final File pubspecFile = package.childFile('pubspec.yaml'); final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); @@ -172,6 +173,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { ### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. name: ${pubspec.name} description: ${pubspec.description} +publish_to: none version: ${pubspec.version} @@ -197,7 +199,21 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); } else if (entry.value is PathDependency) { final PathDependency dep = entry.value as PathDependency; - buffer.write(' ${entry.key}: \n path: ${dep.path}'); + String depPath = dep.path; + if (path.style == p.Style.windows) { + // Posix-style path separators are preferred in pubspec.yaml (and + // using a consistent format makes unit testing simpler), so convert. + final List components = path.split(depPath); + final String firstComponent = components.first; + // path.split leaves a \ on drive components that isn't necessary, + // and confuses pub, so remove it. + if (firstComponent.endsWith(r':\')) { + components[0] = + firstComponent.substring(0, firstComponent.length - 1); + } + depPath = p.posix.joinAll(components); + } + buffer.write(' ${entry.key}: \n path: $depPath'); } else { throw UnimplementedError( 'Not available for type: ${entry.value.runtimeType}', diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index df74119e401..7e800ed5486 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -6,7 +6,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:file/file.dart'; -import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -22,7 +22,8 @@ class DriveExamplesCommand extends PackageLoopingCommand { DriveExamplesCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, processRunner: processRunner) { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addFlag(kPlatformAndroid, help: 'Runs the Android implementation of the examples'); argParser.addFlag(kPlatformIos, @@ -148,7 +149,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { for (final Directory example in getExamplesForPlugin(package)) { ++examplesFound; final String exampleName = - p.relative(example.path, from: packagesDir.path); + getRelativePosixPath(example, from: packagesDir); final List drivers = await _getDrivers(example); if (drivers.isEmpty) { @@ -172,11 +173,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (testTargets.isEmpty) { final String driverRelativePath = - p.relative(driver.path, from: package.path); + getRelativePosixPath(driver, from: package); printError( 'Found $driverRelativePath, but no integration_test/*_test.dart files.'); - errors.add( - 'No test files for ${p.relative(driver.path, from: package.path)}'); + errors.add('No test files for $driverRelativePath'); continue; } @@ -185,7 +185,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { example, driver, testTargets, deviceFlags: deviceFlags); for (final File failingTarget in failingTargets) { - errors.add(p.relative(failingTarget.path, from: package.path)); + errors.add(getRelativePosixPath(failingTarget, from: package)); } } } @@ -296,9 +296,9 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', '--driver', - p.relative(driver.path, from: example.path), + getRelativePosixPath(driver, from: example), '--target', - p.relative(target.path, from: example.path), + getRelativePosixPath(target, from: example), ], workingDir: example); if (exitCode != 0) { diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 8253ceeda86..5e4d9f08008 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -6,7 +6,7 @@ import 'dart:async'; import 'dart:io' as io; import 'package:file/file.dart'; -import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:uuid/uuid.dart'; import 'common/core.dart'; @@ -21,7 +21,8 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { FirebaseTestLabCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, processRunner: processRunner) { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addOption( 'project', defaultsTo: 'flutter-infra', @@ -29,8 +30,9 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { ); final String? homeDir = io.Platform.environment['HOME']; argParser.addOption('service-key', - defaultsTo: - homeDir == null ? null : p.join(homeDir, 'gcloud-service-key.json'), + defaultsTo: homeDir == null + ? null + : path.join(homeDir, 'gcloud-service-key.json'), help: 'The path to the service key for gcloud authentication.\n' r'If not provided, \$HOME/gcloud-service-key.json will be ' r'assumed if $HOME is set.'); @@ -150,7 +152,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { // test file's run. int resultsCounter = 0; for (final File test in _findIntegrationTestFiles(package)) { - final String testName = p.relative(test.path, from: package.path); + final String testName = getRelativePosixPath(test, from: package); print('Testing $testName...'); if (!await _runGradle(androidDirectory, 'app:assembleDebug', testFile: test)) { @@ -203,7 +205,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { print('Running flutter build apk...'); final String experiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( - 'flutter', + flutterCommand, [ 'build', 'apk', diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 9d39d93b911..7954fd044ce 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -7,7 +7,7 @@ import 'dart:io' as io; import 'package:file/file.dart'; import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:quiver/iterables.dart'; import 'common/core.dart'; @@ -28,7 +28,8 @@ class FormatCommand extends PluginCommand { FormatCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, processRunner: processRunner) { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addFlag('fail-on-change', hide: true); argParser.addOption('clang-format', defaultsTo: 'clang-format', @@ -156,7 +157,7 @@ class FormatCommand extends PluginCommand { // `flutter format` doesn't require the project to actually be a Flutter // project. final int exitCode = await processRunner.runAndStream( - 'flutter', ['format', ...dartFiles], + flutterCommand, ['format', ...dartFiles], workingDir: packagesDir); if (exitCode != 0) { printError('Failed to format Dart files: exit code $exitCode.'); @@ -168,8 +169,12 @@ class FormatCommand extends PluginCommand { Future> _getFilteredFilePaths(Stream files) async { // Returns a pattern to check for [directories] as a subset of a file path. RegExp pathFragmentForDirectories(List directories) { - final String s = p.separator; - return RegExp('(?:^|$s)${p.joinAll(directories)}$s'); + String s = path.separator; + // Escape the separator for use in the regex. + if (s == r'\') { + s = r'\\'; + } + return RegExp('(?:^|$s)${path.joinAll(directories)}$s'); } return files @@ -188,12 +193,13 @@ class FormatCommand extends PluginCommand { Iterable _getPathsWithExtensions( Iterable files, Set extensions) { - return files.where((String path) => extensions.contains(p.extension(path))); + return files.where( + (String filePath) => extensions.contains(path.extension(filePath))); } Future _getGoogleFormatterPath() async { - final String javaFormatterPath = p.join( - p.dirname(p.fromUri(io.Platform.script)), + final String javaFormatterPath = path.join( + path.dirname(path.fromUri(platform.script)), 'google-java-format-1.3-all-deps.jar'); final File javaFormatterFile = packagesDir.fileSystem.file(javaFormatterPath); diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index 352197be305..b36d1102f10 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -15,7 +15,8 @@ class JavaTestCommand extends PackageLoopingCommand { JavaTestCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, processRunner: processRunner); + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform); static const String _gradleWrapper = 'gradlew'; @@ -50,7 +51,7 @@ class JavaTestCommand extends PackageLoopingCommand { final List errors = []; for (final Directory example in examplesWithTests) { - final String exampleName = p.relative(example.path, from: package.path); + final String exampleName = getRelativePosixPath(example, from: package); print('\nRUNNING JAVA TESTS for $exampleName'); final Directory androidDirectory = example.childDirectory('android'); diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 1d3e49c6a7c..093f8143df4 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -112,7 +112,7 @@ class LicenseCheckCommand extends PluginCommand { !_shouldIgnoreFile(file)); final Iterable firstPartyLicenseFiles = (await _getAllFiles()).where( (File file) => - p.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); + path.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); final bool copyrightCheckSucceeded = await _checkCodeLicenses(codeFiles); print('\n=======================================\n'); @@ -246,7 +246,7 @@ class LicenseCheckCommand extends PluginCommand { } bool _isThirdParty(File file) { - return p.split(file.path).contains('third_party'); + return path.split(file.path).contains('third_party'); } Future> _getAllFiles() => packagesDir.parent diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 82cce0bd13e..d0d93fcb79b 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -25,8 +25,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), - }) : _platform = platform, - super(packagesDir, processRunner: processRunner) { + }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addMultiOption('ignore-warnings', help: 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs ' @@ -45,11 +44,9 @@ class LintPodspecsCommand extends PackageLoopingCommand { 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; - final Platform _platform; - @override Future initializeRun() async { - if (!_platform.isMacOS) { + if (!platform.isMacOS) { printError('This command is only supported on macOS'); throw ToolExit(_exitUnsupportedPlatform); } @@ -89,11 +86,10 @@ class LintPodspecsCommand extends PackageLoopingCommand { final List podspecs = await getFilesForPackage(package).where((File entity) { final String filePath = entity.path; - return p.extension(filePath) == '.podspec'; + return path.extension(filePath) == '.podspec'; }).toList(); - podspecs.sort( - (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); + podspecs.sort((File a, File b) => a.basename.compareTo(b.basename)); return podspecs; } diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 39515cf686b..20f01ff98f0 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:platform/platform.dart'; import 'common/plugin_command.dart'; @@ -10,7 +11,10 @@ import 'common/plugin_command.dart'; class ListCommand extends PluginCommand { /// Creates an instance of the list command, whose behavior depends on the /// 'type' argument it provides. - ListCommand(Directory packagesDir) : super(packagesDir) { + ListCommand( + Directory packagesDir, { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, platform: platform) { argParser.addOption( _type, defaultsTo: _plugin, diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index ccafabfddd1..fda68a6a74a 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -8,6 +8,7 @@ import 'dart:io' as io; import 'package:file/file.dart'; import 'package:http/http.dart' as http; +import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; @@ -22,10 +23,11 @@ class PublishCheckCommand extends PackageLoopingCommand { PublishCheckCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), http.Client? httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), - super(packagesDir, processRunner: processRunner) { + super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addFlag( _allowPrereleaseFlag, help: 'Allows the pre-release SDK warning to pass.\n' @@ -128,7 +130,7 @@ class PublishCheckCommand extends PackageLoopingCommand { Future _hasValidPublishCheckRun(Directory package) async { print('Running pub publish --dry-run:'); final io.Process process = await processRunner.start( - 'flutter', + flutterCommand, ['pub', 'publish', '--', '--dry-run'], workingDirectory: package, ); diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 6de53ba2690..8bcb9e37e8e 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -208,9 +208,13 @@ class PublishPluginCommand extends PluginCommand { final List packagesFailed = []; for (final String pubspecPath in changedPubspecs) { + // Convert git's Posix-style paths to a path that matches the current + // filesystem. + final String localStylePubspecPath = + path.joinAll(p.posix.split(pubspecPath)); final File pubspecFile = packagesDir.fileSystem .directory(baseGitDir.path) - .childFile(pubspecPath); + .childFile(localStylePubspecPath); final _CheckNeedsReleaseResult result = await _checkNeedsRelease( pubspecFile: pubspecFile, existingTags: existingTags, @@ -445,7 +449,7 @@ Safe to ignore if the package is deleted in this commit. } final io.Process publish = await processRunner.start( - 'flutter', ['pub', 'publish'] + publishFlags, + flutterCommand, ['pub', 'publish'] + publishFlags, workingDirectory: packageDir); publish.stdout .transform(utf8.decoder) diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 7d39c7322b7..539b170dbea 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -4,6 +4,7 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; +import 'package:platform/platform.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/package_looping_command.dart'; @@ -19,8 +20,14 @@ class PubspecCheckCommand extends PackageLoopingCommand { PubspecCheckCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), GitDir? gitDir, - }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); + }) : super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ); // Section order for plugins. Because the 'flutter' section is critical // information for plugins, and usually small, it goes near the top unlike in diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index d06a2841812..9dfe66b7926 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -15,7 +16,8 @@ class TestCommand extends PackageLoopingCommand { TestCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, processRunner: processRunner) { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addOption( kEnableExperiment, defaultsTo: '', diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index f0902f01683..c08600c3f66 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -7,6 +7,7 @@ import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; @@ -77,11 +78,17 @@ class VersionCheckCommand extends PackageLoopingCommand { VersionCheckCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), GitDir? gitDir, http.Client? httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), - super(packagesDir, processRunner: processRunner, gitDir: gitDir) { + super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ) { argParser.addFlag( _againstPubFlag, help: 'Whether the version check should run against the version on pub.\n' @@ -179,8 +186,13 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} required GitVersionFinder gitVersionFinder, }) async { final File pubspecFile = package.childFile('pubspec.yaml'); - return await gitVersionFinder.getPackageVersion( - p.relative(pubspecFile.absolute.path, from: (await gitDir).path)); + final String relativePath = + path.relative(pubspecFile.absolute.path, from: (await gitDir).path); + // Use Posix-style paths for git. + final String gitPath = path.style == p.Style.windows + ? p.posix.joinAll(path.split(relativePath)) + : relativePath; + return await gitVersionFinder.getPackageVersion(gitPath); } /// Returns true if the version of [package] is either unchanged relative to diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index cd3b674f8d3..176adad39a0 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -6,7 +6,7 @@ import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; -import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -32,7 +32,8 @@ class XCTestCommand extends PackageLoopingCommand { XCTestCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - }) : super(packagesDir, processRunner: processRunner) { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addOption( _kiOSDestination, help: @@ -142,7 +143,7 @@ class XCTestCommand extends PackageLoopingCommand { for (final Directory example in getExamplesForPlugin(plugin)) { // Running tests and static analyzer. final String examplePath = - p.relative(example.path, from: plugin.parent.path); + getRelativePosixPath(example, from: plugin.parent); print('Running $platform tests and analyzer for $examplePath...'); int exitCode = await _runTests(true, example, platform, extraFlags: extraXcrunFlags); diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index adeaabaaca5..69a2c4f9552 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -16,16 +16,21 @@ import 'util.dart'; void main() { late FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late RecordingProcessRunner processRunner; late CommandRunner runner; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final AnalyzeCommand analyzeCommand = - AnalyzeCommand(packagesDir, processRunner: processRunner); + final AnalyzeCommand analyzeCommand = AnalyzeCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); runner = CommandRunner('analyze_command', 'Test for analyze_command'); runner.addCommand(analyzeCommand); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index c0c90a15c71..27489a50228 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -10,8 +10,6 @@ import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/build_examples_command.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -20,18 +18,21 @@ import 'util.dart'; void main() { group('build-example', () { late FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; - final String flutterCommand = - const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final BuildExamplesCommand command = - BuildExamplesCommand(packagesDir, processRunner: processRunner); + final BuildExamplesCommand command = BuildExamplesCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); runner = CommandRunner( 'build_examples_command', 'Test for build_example_command'); @@ -59,7 +60,9 @@ void main() { kPlatformIos: PlatformSupport.inline }); - processRunner.mockProcessesForExecutable['flutter'] = [ + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ MockProcess.failing() // flutter packages get ]; @@ -81,6 +84,7 @@ void main() { test('building for iOS when plugin is not set up for iOS results in no-op', () async { + mockPlatform.isMacOS = true; createFakePlugin('plugin', packagesDir); final List output = @@ -99,7 +103,8 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for ios', () async { + test('building for iOS', () async { + mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline @@ -110,13 +115,11 @@ void main() { final List output = await runCapturingPrint(runner, ['build-examples', '--ios', '--enable-experiment=exp1']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - '\nBUILDING $packageName for iOS', + '\nBUILDING plugin/example for iOS', ]), ); @@ -124,7 +127,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - flutterCommand, + getFlutterCommand(mockPlatform), const [ 'build', 'ios', @@ -138,6 +141,7 @@ void main() { test( 'building for Linux when plugin is not set up for Linux results in no-op', () async { + mockPlatform.isLinux = true; createFakePlugin('plugin', packagesDir); final List output = await runCapturingPrint( @@ -157,6 +161,7 @@ void main() { }); test('building for Linux', () async { + mockPlatform.isLinux = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { kPlatformLinux: PlatformSupport.inline, @@ -167,26 +172,25 @@ void main() { final List output = await runCapturingPrint( runner, ['build-examples', '--linux']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - '\nBUILDING $packageName for Linux', + '\nBUILDING plugin/example for Linux', ]), ); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, const ['build', 'linux'], - pluginExampleDirectory.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'linux'], pluginExampleDirectory.path), ])); }); - test('building for macos with no implementation results in no-op', + test('building for macOS with no implementation results in no-op', () async { + mockPlatform.isMacOS = true; createFakePlugin('plugin', packagesDir); final List output = await runCapturingPrint( @@ -205,7 +209,8 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for macos', () async { + test('building for macOS', () async { + mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { kPlatformMacos: PlatformSupport.inline, @@ -216,21 +221,19 @@ void main() { final List output = await runCapturingPrint( runner, ['build-examples', '--macos']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - '\nBUILDING $packageName for macOS', + '\nBUILDING plugin/example for macOS', ]), ); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, const ['build', 'macos'], - pluginExampleDirectory.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'macos'], pluginExampleDirectory.path), ])); }); @@ -264,27 +267,26 @@ void main() { final List output = await runCapturingPrint(runner, ['build-examples', '--web']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - '\nBUILDING $packageName for web', + '\nBUILDING plugin/example for web', ]), ); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, const ['build', 'web'], - pluginExampleDirectory.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'web'], pluginExampleDirectory.path), ])); }); test( 'building for Windows when plugin is not set up for Windows results in no-op', () async { + mockPlatform.isWindows = true; createFakePlugin('plugin', packagesDir); final List output = await runCapturingPrint( @@ -303,7 +305,8 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for windows', () async { + test('building for Windows', () async { + mockPlatform.isWindows = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { kPlatformWindows: PlatformSupport.inline @@ -314,20 +317,20 @@ void main() { final List output = await runCapturingPrint( runner, ['build-examples', '--windows']); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - '\nBUILDING $packageName for Windows', + '\nBUILDING plugin/example for Windows', ]), ); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, const ['build', 'windows'], + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'windows'], pluginExampleDirectory.path), ])); }); @@ -353,7 +356,7 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for android', () async { + test('building for Android', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { kPlatformAndroid: PlatformSupport.inline @@ -366,21 +369,19 @@ void main() { 'build-examples', '--apk', ]); - final String packageName = - p.relative(pluginExampleDirectory.path, from: packagesDir.path); expect( output, containsAllInOrder([ - '\nBUILDING $packageName for Android (apk)', + '\nBUILDING plugin/example for Android (apk)', ]), ); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(flutterCommand, const ['build', 'apk'], - pluginExampleDirectory.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'apk'], pluginExampleDirectory.path), ])); }); @@ -400,7 +401,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - flutterCommand, + getFlutterCommand(mockPlatform), const ['build', 'apk', '--enable-experiment=exp1'], pluginExampleDirectory.path), ])); @@ -421,7 +422,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - flutterCommand, + getFlutterCommand(mockPlatform), const [ 'build', 'ios', diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 917fbc0fd67..542e91af643 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; +import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; @@ -13,8 +13,10 @@ import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; import '../util.dart'; import 'plugin_command_test.mocks.dart'; @@ -36,11 +38,13 @@ const String _warningFile = 'warnings'; void main() { late FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late Directory thirdPartyPackagesDir; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); thirdPartyPackagesDir = packagesDir.parent .childDirectory('third_party') @@ -69,11 +73,12 @@ void main() { when(mockProcessResult.stdout as String?) .thenReturn(gitDiffResponse); } - return Future.value(mockProcessResult); + return Future.value(mockProcessResult); }); return TestPackageLoopingCommand( packagesDir, + platform: mockPlatform, hasLongOutput: hasLongOutput, includeSubpackages: includeSubpackages, failsDuringInit: failsDuringInit, @@ -502,7 +507,25 @@ void main() { test('getPackageDescription prints packageDir-relative paths by default', () async { final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir); + TestPackageLoopingCommand(packagesDir, platform: mockPlatform); + + expect( + command.getPackageDescription(packagesDir.childDirectory('foo')), + 'foo', + ); + expect( + command.getPackageDescription(packagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')), + 'foo/bar/baz', + ); + }); + + test('getPackageDescription always uses Posix-style paths', () async { + mockPlatform.isWindows = true; + final TestPackageLoopingCommand command = + TestPackageLoopingCommand(packagesDir, platform: mockPlatform); expect( command.getPackageDescription(packagesDir.childDirectory('foo')), @@ -521,7 +544,7 @@ void main() { 'getPackageDescription elides group name in grouped federated plugin structure', () async { final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir); + TestPackageLoopingCommand(packagesDir, platform: mockPlatform); expect( command.getPackageDescription(packagesDir @@ -542,6 +565,7 @@ void main() { class TestPackageLoopingCommand extends PackageLoopingCommand { TestPackageLoopingCommand( Directory packagesDir, { + required Platform platform, this.hasLongOutput = true, this.includeSubpackages = false, this.customFailureListHeader, @@ -552,7 +576,8 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { this.captureOutput = false, ProcessRunner processRunner = const ProcessRunner(), GitDir? gitDir, - }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); + }) : super(packagesDir, + processRunner: processRunner, platform: platform, gitDir: gitDir); final List checkedPackages = []; final List capturedOutput = []; @@ -629,4 +654,4 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { } } -class MockProcessResult extends Mock implements ProcessResult {} +class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 3f1f1adc4c1..fdab9612be3 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -12,8 +12,10 @@ import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:git/git.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; import '../util.dart'; import 'plugin_command_test.mocks.dart'; @@ -22,6 +24,7 @@ void main() { late RecordingProcessRunner processRunner; late CommandRunner runner; late FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late Directory thirdPartyPackagesDir; late List plugins; @@ -30,6 +33,7 @@ void main() { setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); thirdPartyPackagesDir = packagesDir.parent .childDirectory('third_party') @@ -54,6 +58,7 @@ void main() { plugins, packagesDir, processRunner: processRunner, + platform: mockPlatform, gitDir: gitDir, ); runner = @@ -414,8 +419,10 @@ class SamplePluginCommand extends PluginCommand { this._plugins, Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), GitDir? gitDir, - }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); + }) : super(packagesDir, + processRunner: processRunner, platform: platform, gitDir: gitDir); final List _plugins; diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 681a9e0e584..c6893181e28 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -10,7 +10,6 @@ import 'package:file/memory.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/drive_examples_command.dart'; -import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:test/test.dart'; @@ -23,18 +22,18 @@ const String _fakeAndroidDevice = 'emulator-1234'; void main() { group('test drive_example_command', () { late FileSystem fileSystem; + late Platform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; - final String flutterCommand = - const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final DriveExamplesCommand command = - DriveExamplesCommand(packagesDir, processRunner: processRunner); + final DriveExamplesCommand command = DriveExamplesCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); runner = CommandRunner( 'drive_examples_command', 'Test for drive_example_command'); @@ -63,9 +62,9 @@ void main() { final MockProcess mockDevicesProcess = MockProcess.succeeding(); mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures - processRunner.mockProcessesForExecutable['flutter'] = [ - mockDevicesProcess - ]; + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [mockDevicesProcess]; processRunner.resultStdout = output; } @@ -150,9 +149,9 @@ void main() { test('fails for iOS if getting devices fails', () async { // Simulate failure from `flutter devices`. - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess.failing() - ]; + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [MockProcess.failing()]; Error? commandError; final List output = await runCapturingPrint( @@ -216,23 +215,21 @@ void main() { ]), ); - final String deviceTestPath = p.join('test_driver', 'plugin.dart'); - final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), ProcessCall( - flutterCommand, const ['devices', '--machine'], null), - ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', _fakeIosDevice, '--driver', - driverTestPath, + 'test_driver/plugin_test.dart', '--target', - deviceTestPath + 'test_driver/plugin.dart' ], pluginExampleDirectory.path), ])); @@ -337,35 +334,33 @@ void main() { ]), ); - final String driverTestPath = - p.join('test_driver', 'integration_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), ProcessCall( - flutterCommand, const ['devices', '--machine'], null), - ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', _fakeIosDevice, '--driver', - driverTestPath, + 'test_driver/integration_test.dart', '--target', - p.join('integration_test', 'bar_test.dart'), + 'integration_test/bar_test.dart', ], pluginExampleDirectory.path), ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', _fakeIosDevice, '--driver', - driverTestPath, + 'test_driver/integration_test.dart', '--target', - p.join('integration_test', 'foo_test.dart'), + 'integration_test/foo_test.dart', ], pluginExampleDirectory.path), ])); @@ -425,21 +420,19 @@ void main() { ]), ); - final String deviceTestPath = p.join('test_driver', 'plugin.dart'); - final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', 'linux', '--driver', - driverTestPath, + 'test_driver/plugin_test.dart', '--target', - deviceTestPath + 'test_driver/plugin.dart' ], pluginExampleDirectory.path), ])); @@ -500,21 +493,19 @@ void main() { ]), ); - final String deviceTestPath = p.join('test_driver', 'plugin.dart'); - final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', 'macos', '--driver', - driverTestPath, + 'test_driver/plugin_test.dart', '--target', - deviceTestPath + 'test_driver/plugin.dart' ], pluginExampleDirectory.path), ])); @@ -573,23 +564,21 @@ void main() { ]), ); - final String deviceTestPath = p.join('test_driver', 'plugin.dart'); - final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', 'web-server', '--web-port=7357', '--browser-name=chrome', '--driver', - driverTestPath, + 'test_driver/plugin_test.dart', '--target', - deviceTestPath + 'test_driver/plugin.dart' ], pluginExampleDirectory.path), ])); @@ -649,21 +638,19 @@ void main() { ]), ); - final String deviceTestPath = p.join('test_driver', 'plugin.dart'); - final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', 'windows', '--driver', - driverTestPath, + 'test_driver/plugin_test.dart', '--target', - deviceTestPath + 'test_driver/plugin.dart' ], pluginExampleDirectory.path), ])); @@ -699,23 +686,21 @@ void main() { ]), ); - final String deviceTestPath = p.join('test_driver', 'plugin.dart'); - final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), ProcessCall( - flutterCommand, const ['devices', '--machine'], null), - ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', _fakeAndroidDevice, '--driver', - driverTestPath, + 'test_driver/plugin_test.dart', '--target', - deviceTestPath + 'test_driver/plugin.dart' ], pluginExampleDirectory.path), ])); @@ -749,8 +734,8 @@ void main() { // Output should be empty other than the device query. expect(processRunner.recordedCalls, [ - ProcessCall( - flutterCommand, const ['devices', '--machine'], null), + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), ]); }); @@ -782,8 +767,8 @@ void main() { // Output should be empty other than the device query. expect(processRunner.recordedCalls, [ - ProcessCall( - flutterCommand, const ['devices', '--machine'], null), + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), ]); }); @@ -833,24 +818,22 @@ void main() { '--enable-experiment=exp1', ]); - final String deviceTestPath = p.join('test_driver', 'plugin.dart'); - final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), ProcessCall( - flutterCommand, const ['devices', '--machine'], null), - ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', _fakeIosDevice, '--enable-experiment=exp1', '--driver', - driverTestPath, + 'test_driver/plugin_test.dart', '--target', - deviceTestPath + 'test_driver/plugin.dart' ], pluginExampleDirectory.path), ])); @@ -967,7 +950,9 @@ void main() { ); // Simulate failure from `flutter drive`. - processRunner.mockProcessesForExecutable['flutter'] = [ + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ // No mock for 'devices', since it's running for macOS. MockProcess.failing(), // 'drive' #1 MockProcess.failing(), // 'drive' #2 @@ -994,33 +979,31 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final String driverTestPath = - p.join('test_driver', 'integration_test.dart'); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', 'macos', '--driver', - driverTestPath, + 'test_driver/integration_test.dart', '--target', - p.join('integration_test', 'bar_test.dart'), + 'integration_test/bar_test.dart', ], pluginExampleDirectory.path), ProcessCall( - flutterCommand, - [ + getFlutterCommand(mockPlatform), + const [ 'drive', '-d', 'macos', '--driver', - driverTestPath, + 'test_driver/integration_test.dart', '--target', - p.join('integration_test', 'foo_test.dart'), + 'integration_test/foo_test.dart', ], pluginExampleDirectory.path), ])); diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 0199eba9598..c265868bbf3 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -17,16 +17,21 @@ import 'util.dart'; void main() { group('$FirebaseTestLabCommand', () { FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final FirebaseTestLabCommand command = - FirebaseTestLabCommand(packagesDir, processRunner: processRunner); + final FirebaseTestLabCommand command = FirebaseTestLabCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); runner = CommandRunner( 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index e7f4d795eb9..fabef31a1b6 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -17,21 +17,28 @@ import 'util.dart'; void main() { late FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; + late p.Context path; late RecordingProcessRunner processRunner; late CommandRunner runner; late String javaFormatPath; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final FormatCommand analyzeCommand = - FormatCommand(packagesDir, processRunner: processRunner); + final FormatCommand analyzeCommand = FormatCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); // Create the java formatter file that the command checks for, to avoid // a download. - javaFormatPath = p.join(p.dirname(p.fromUri(io.Platform.script)), + path = analyzeCommand.path; + javaFormatPath = path.join(path.dirname(path.fromUri(mockPlatform.script)), 'google-java-format-1.3-all-deps.jar'); fileSystem.file(javaFormatPath).createSync(recursive: true); @@ -42,7 +49,7 @@ void main() { List _getAbsolutePaths( Directory package, List relativePaths) { return relativePaths - .map((String path) => p.join(package.path, path)) + .map((String relativePath) => path.join(package.path, relativePath)) .toList(); } diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart index 9ae95971098..13e0e7fc0f4 100644 --- a/script/tool/test/java_test_command_test.dart +++ b/script/tool/test/java_test_command_test.dart @@ -10,7 +10,6 @@ import 'package:file/memory.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/java_test_command.dart'; -import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'mocks.dart'; @@ -19,16 +18,21 @@ import 'util.dart'; void main() { group('$JavaTestCommand', () { late FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final JavaTestCommand command = - JavaTestCommand(packagesDir, processRunner: processRunner); + final JavaTestCommand command = JavaTestCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); runner = CommandRunner('java_test_test', 'Test for $JavaTestCommand'); @@ -50,13 +54,16 @@ void main() { await runCapturingPrint(runner, ['java-test']); + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + expect( processRunner.recordedCalls, orderedEquals([ ProcessCall( - p.join(plugin.path, 'example/android/gradlew'), + androidFolder.childFile('gradlew').path, const ['testDebugUnitTest', '--info'], - p.join(plugin.path, 'example/android'), + androidFolder.path, ), ]), ); @@ -77,13 +84,16 @@ void main() { await runCapturingPrint(runner, ['java-test']); + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + expect( processRunner.recordedCalls, orderedEquals([ ProcessCall( - p.join(plugin.path, 'example/android/gradlew'), + androidFolder.childFile('gradlew').path, const ['testDebugUnitTest', '--info'], - p.join(plugin.path, 'example/android'), + androidFolder.path, ), ]), ); diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 1236ec0f501..51a4e626777 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -9,7 +9,6 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; -import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'mocks.dart'; @@ -24,7 +23,7 @@ void main() { late RecordingProcessRunner processRunner; setUp(() { - fileSystem = MemoryFileSystem(); + fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); packagesDir = createPackagesDirectory(fileSystem: fileSystem); mockPlatform = MockPlatform(isMacOS: true); @@ -94,7 +93,10 @@ void main() { [ 'lib', 'lint', - p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + plugin1Dir + .childDirectory('ios') + .childFile('plugin1.podspec') + .path, '--configuration=Debug', '--skip-tests', '--use-modular-headers', @@ -106,7 +108,10 @@ void main() { [ 'lib', 'lint', - p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + plugin1Dir + .childDirectory('ios') + .childFile('plugin1.podspec') + .path, '--configuration=Debug', '--skip-tests', '--use-modular-headers', @@ -136,7 +141,7 @@ void main() { [ 'lib', 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), + plugin1Dir.childFile('plugin1.podspec').path, '--configuration=Debug', '--skip-tests', '--use-modular-headers', @@ -149,7 +154,7 @@ void main() { [ 'lib', 'lint', - p.join(plugin1Dir.path, 'plugin1.podspec'), + plugin1Dir.childFile('plugin1.podspec').path, '--configuration=Debug', '--skip-tests', '--use-modular-headers', diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index 836d06671c2..488fc9bcb1e 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -8,18 +8,22 @@ import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/list_command.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { group('$ListCommand', () { late FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - final ListCommand command = ListCommand(packagesDir); + final ListCommand command = + ListCommand(packagesDir, platform: mockPlatform); runner = CommandRunner('list_test', 'Test for $ListCommand'); runner.addCommand(command); diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 02b00658398..0dcdedd3db0 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -10,10 +10,25 @@ import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; class MockPlatform extends Mock implements Platform { - MockPlatform({this.isMacOS = false}); + MockPlatform({ + this.isLinux = false, + this.isMacOS = false, + this.isWindows = false, + }); + + @override + bool isLinux; @override bool isMacOS; + + @override + bool isWindows; + + @override + Uri get script => isWindows + ? Uri.file(r'C:\foo\bar', windows: true) + : Uri.file('/foo/bar', windows: false); } class MockProcess extends Mock implements io.Process { diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 5140316b451..11de9f09548 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -21,16 +21,21 @@ import 'util.dart'; void main() { group('$PublishCheckProcessRunner tests', () { FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late PublishCheckProcessRunner processRunner; late CommandRunner runner; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = PublishCheckProcessRunner(); - final PublishCheckCommand publishCheckCommand = - PublishCheckCommand(packagesDir, processRunner: processRunner); + final PublishCheckCommand publishCheckCommand = PublishCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); runner = CommandRunner( 'publish_check_command', @@ -339,12 +344,16 @@ void main() { }); expect(hasError, isTrue); - expect(output.first, r''' + expect(output.first, contains(r''' { "status": "error", "humanMessage": [ "\n============================================================\n|| Running for no_publish_a\n============================================================\n", - "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException: line 1, column 1: Not a map\n ╷\n1 │ bad-yaml\n │ ^^^^^^^^\n ╵}", + "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException:''')); + // This is split into two checks since the details of the YamlException + // aren't controlled by this package, so asserting its exact format would + // make the test fragile to irrelevant changes in those details. + expect(output.first, contains(r''' "no pubspec", "\n============================================================\n|| Running for no_publish_b\n============================================================\n", "url https://pub.dev/packages/no_publish_b.json", @@ -356,7 +365,7 @@ void main() { " no_publish_a", "See above for full details." ] -}'''); +}''')); }); }); } diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 497579b02f8..c7df8195264 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -16,6 +16,7 @@ import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -1091,7 +1092,7 @@ class TestProcessRunner extends ProcessRunner { {Directory? workingDirectory}) async { /// Never actually publish anything. Start is always and only used for this /// since it returns something we can route stdin through. - assert(executable == 'flutter' && + assert(executable == getFlutterCommand(const LocalPlatform()) && args.isNotEmpty && args[0] == 'pub' && args[1] == 'publish'); diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 9e633e21b4a..177ed7f25b4 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -9,6 +9,7 @@ import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/pubspec_check_command.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { @@ -16,15 +17,20 @@ void main() { late CommandRunner runner; late RecordingProcessRunner processRunner; late FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = fileSystem.currentDirectory.childDirectory('packages'); createPackagesDirectory(parentDir: packagesDir.parent); processRunner = RecordingProcessRunner(); - final PubspecCheckCommand command = - PubspecCheckCommand(packagesDir, processRunner: processRunner); + final PubspecCheckCommand command = PubspecCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); runner = CommandRunner( 'pubspec_check_command', 'Test for pubspec_check_command'); diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index ac0ac4b3dd4..503e24d0305 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -10,6 +10,7 @@ import 'package:file/memory.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/test_command.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -18,16 +19,21 @@ import 'util.dart'; void main() { group('$TestCommand', () { late FileSystem fileSystem; + late Platform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final TestCommand command = - TestCommand(packagesDir, processRunner: processRunner); + final TestCommand command = TestCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); runner = CommandRunner('test_test', 'Test for $TestCommand'); runner.addCommand(command); @@ -44,10 +50,10 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['test', '--color'], plugin1Dir.path), - ProcessCall( - 'flutter', const ['test', '--color'], plugin2Dir.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin1Dir.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin2Dir.path), ]), ); }); @@ -58,7 +64,9 @@ void main() { createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); - processRunner.mockProcessesForExecutable['flutter'] = [ + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ MockProcess.failing(), // plugin 1 test MockProcess.succeeding(), // plugin 2 test ]; @@ -88,8 +96,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['test', '--color'], plugin2Dir.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin2Dir.path), ]), ); }); @@ -107,7 +115,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'flutter', + getFlutterCommand(mockPlatform), const ['test', '--color', '--enable-experiment=exp1'], pluginDir.path), ProcessCall('dart', const ['pub', 'get'], packageDir.path), @@ -183,7 +191,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'flutter', + getFlutterCommand(mockPlatform), const ['test', '--color', '--platform=chrome'], pluginDir.path), ]), @@ -203,7 +211,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'flutter', + getFlutterCommand(mockPlatform), const ['test', '--color', '--enable-experiment=exp1'], pluginDir.path), ProcessCall('dart', const ['pub', 'get'], packageDir.path), diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index b65b1fcaa84..1984a25cc43 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -14,8 +14,14 @@ 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:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:quiver/collection.dart'; +/// Returns the exe name that command will use when running Flutter on +/// [platform]. +String getFlutterCommand(Platform platform) => + platform.isWindows ? 'flutter.bat' : 'flutter'; + /// Creates a packages directory in the given location. /// /// If [parentDir] is set the packages directory will be created there, diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 6fbed9c691b..587de1a58cd 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -18,6 +18,7 @@ import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; import 'util.dart'; void testAllowedVersion( @@ -46,6 +47,7 @@ void main() { const String indentation = ' '; group('$VersionCheckCommand', () { FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; @@ -55,6 +57,7 @@ void main() { setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); gitDirCommands = >[]; gitShowResponses = {}; @@ -80,7 +83,7 @@ void main() { }); processRunner = RecordingProcessRunner(); final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir); + processRunner: processRunner, platform: mockPlatform, gitDir: gitDir); runner = CommandRunner( 'version_check_command', 'Test for $VersionCheckCommand'); diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 10329b18980..aa6d23fb56f 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -90,16 +90,18 @@ void main() { group('test xctest_command', () { late FileSystem fileSystem; + late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final XCTestCommand command = - XCTestCommand(packagesDir, processRunner: processRunner); + final XCTestCommand command = XCTestCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); runner = CommandRunner('xctest_command', 'Test for xctest_command'); runner.addCommand(command); From 5f7735d16c4022cd51ae0447c1b6d12ff221f54d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 12 Jul 2021 17:57:39 -0700 Subject: [PATCH 096/249] [flutter_plugin_tools] Support format on Windows (#4150) Allows `format` to run successfully on Windows: - Ensures that no calls exceed the command length limit. - Allows specifying a `java` path to make it easier to run without a system Java (e.g., by pointing to the `java` binary in an Android Studio installation). - Adds clear error messages when `java` or `clang-format` is missing since it's very non-obvious what's wrong otherwise. Bumps the version, which I intended to do in the previous PR but apparently didn't push to the PR. --- script/tool/CHANGELOG.md | 3 +- script/tool/lib/src/format_command.dart | 147 ++++++++++-- script/tool/pubspec.yaml | 2 +- script/tool/test/format_command_test.dart | 275 ++++++++++++++++++++-- 4 files changed, 383 insertions(+), 44 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 3f31a4953f6..9db94dda37d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 0.4.0 - Modified the output format of many commands - **Breaking change**: `firebase-test-lab` no longer supports `*_e2e.dart` @@ -10,6 +10,7 @@ - Deprecated `--plugins` in favor of new `--packages`. `--plugins` continues to work for now, but will be removed in the future. - Make `drive-examples` device detection robust against Flutter tool banners. +- `format` is now supported on Windows. ## 0.3.0 diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 7954fd044ce..c67fb96d283 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -7,17 +7,31 @@ import 'dart:io' as io; import 'package:file/file.dart'; import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; import 'package:platform/platform.dart'; -import 'package:quiver/iterables.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; import 'common/process_runner.dart'; +/// In theory this should be 8191, but in practice that was still resulting in +/// "The input line is too long" errors. This was chosen as a value that worked +/// in practice in testing with flutter/plugins, but may need to be adjusted +/// based on further experience. +@visibleForTesting +const int windowsCommandLineMax = 8000; + +/// This value is picked somewhat arbitrarily based on checking `ARG_MAX` on a +/// macOS and Linux machine. If anyone encounters a lower limit in pratice, it +/// can be lowered accordingly. +@visibleForTesting +const int nonWindowsCommandLineMax = 1000000; + const int _exitClangFormatFailed = 3; const int _exitFlutterFormatFailed = 4; const int _exitJavaFormatFailed = 5; const int _exitGitFailed = 6; +const int _exitDependencyMissing = 7; 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'); @@ -32,8 +46,9 @@ class FormatCommand extends PluginCommand { }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addFlag('fail-on-change', hide: true); argParser.addOption('clang-format', - defaultsTo: 'clang-format', - help: 'Path to executable of clang-format.'); + defaultsTo: 'clang-format', help: 'Path to "clang-format" executable.'); + argParser.addOption('java', + defaultsTo: 'java', help: 'Path to "java" executable.'); } @override @@ -52,7 +67,8 @@ class FormatCommand extends PluginCommand { // This class is not based on PackageLoopingCommand because running the // formatters separately for each package is an order of magnitude slower, // due to the startup overhead of the formatters. - final Iterable files = await _getFilteredFilePaths(getFiles()); + final Iterable files = + await _getFilteredFilePaths(getFiles(), relativeTo: packagesDir); await _formatDart(files); await _formatJava(files, googleFormatterPath); await _formatCppAndObjectiveC(files); @@ -112,19 +128,18 @@ class FormatCommand extends PluginCommand { final Iterable clangFiles = _getPathsWithExtensions( files, {'.h', '.m', '.mm', '.cc', '.cpp'}); if (clangFiles.isNotEmpty) { - print('Formatting .cc, .cpp, .h, .m, and .mm files...'); - final Iterable> batches = partition(clangFiles, 100); - int exitCode = 0; - for (final List batch in batches) { - batch.sort(); // For ease of testing; partition changes the order. - exitCode = await processRunner.runAndStream( - getStringArg('clang-format'), - ['-i', '--style=Google', ...batch], - workingDir: packagesDir); - if (exitCode != 0) { - break; - } + final String clangFormat = getStringArg('clang-format'); + if (!await _hasDependency(clangFormat)) { + printError( + 'Unable to run \'clang-format\'. Make sure that it is in your ' + 'path, or provide a full path with --clang-format.'); + throw ToolExit(_exitDependencyMissing); } + + print('Formatting .cc, .cpp, .h, .m, and .mm files...'); + final int exitCode = await _runBatched( + getStringArg('clang-format'), ['-i', '--style=Google'], + files: clangFiles); if (exitCode != 0) { printError( 'Failed to format C, C++, and Objective-C files: exit code $exitCode.'); @@ -138,10 +153,18 @@ class FormatCommand extends PluginCommand { final Iterable javaFiles = _getPathsWithExtensions(files, {'.java'}); if (javaFiles.isNotEmpty) { + final String java = getStringArg('java'); + if (!await _hasDependency(java)) { + printError( + 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'provide a full path with --java.'); + throw ToolExit(_exitDependencyMissing); + } + print('Formatting .java files...'); - final int exitCode = await processRunner.runAndStream('java', - ['-jar', googleFormatterPath, '--replace', ...javaFiles], - workingDir: packagesDir); + final int exitCode = await _runBatched( + java, ['-jar', googleFormatterPath, '--replace'], + files: javaFiles); if (exitCode != 0) { printError('Failed to format Java files: exit code $exitCode.'); throw ToolExit(_exitJavaFormatFailed); @@ -156,9 +179,8 @@ class FormatCommand extends PluginCommand { print('Formatting .dart files...'); // `flutter format` doesn't require the project to actually be a Flutter // project. - final int exitCode = await processRunner.runAndStream( - flutterCommand, ['format', ...dartFiles], - workingDir: packagesDir); + final int exitCode = await _runBatched(flutterCommand, ['format'], + files: dartFiles); if (exitCode != 0) { printError('Failed to format Dart files: exit code $exitCode.'); throw ToolExit(_exitFlutterFormatFailed); @@ -166,7 +188,12 @@ class FormatCommand extends PluginCommand { } } - Future> _getFilteredFilePaths(Stream files) async { + /// Given a stream of [files], returns the paths of any that are not in known + /// locations to ignore, relative to [relativeTo]. + Future> _getFilteredFilePaths( + Stream files, { + required Directory relativeTo, + }) async { // Returns a pattern to check for [directories] as a subset of a file path. RegExp pathFragmentForDirectories(List directories) { String s = path.separator; @@ -177,8 +204,10 @@ class FormatCommand extends PluginCommand { return RegExp('(?:^|$s)${path.joinAll(directories)}$s'); } + final String fromPath = relativeTo.path; + return files - .map((File file) => file.path) + .map((File file) => path.relative(file.path, from: fromPath)) .where((String path) => // Ignore files in build/ directories (e.g., headers of frameworks) // to avoid useless extra work in local repositories. @@ -212,4 +241,74 @@ class FormatCommand extends PluginCommand { return javaFormatterPath; } + + /// Returns true if [command] can be run successfully. + Future _hasDependency(String command) async { + try { + final io.ProcessResult result = + await processRunner.run(command, ['--version']); + if (result.exitCode != 0) { + return false; + } + } on io.ProcessException { + // Thrown when the binary is missing entirely. + return false; + } + return true; + } + + /// Runs [command] on [arguments] on all of the files in [files], batched as + /// necessary to avoid OS command-line length limits. + /// + /// Returns the exit code of the first failure, which stops the run, or 0 + /// on success. + Future _runBatched( + String command, + List arguments, { + required Iterable files, + }) async { + final int commandLineMax = + platform.isWindows ? windowsCommandLineMax : nonWindowsCommandLineMax; + + // Compute the max length of the file argument portion of a batch. + // Add one to each argument's length for the space before it. + final int argumentTotalLength = + arguments.fold(0, (int sum, String arg) => sum + arg.length + 1); + final int batchMaxTotalLength = + commandLineMax - command.length - argumentTotalLength; + + // Run the command in batches. + final List> batches = + _partitionFileList(files, maxStringLength: batchMaxTotalLength); + for (final List batch in batches) { + batch.sort(); // For ease of testing. + final int exitCode = await processRunner.runAndStream( + command, [...arguments, ...batch], + workingDir: packagesDir); + if (exitCode != 0) { + return exitCode; + } + } + return 0; + } + + /// Partitions [files] into batches whose max string length as parameters to + /// a command (including the spaces between them, and between the list and + /// the command itself) is no longer than [maxStringLength]. + List> _partitionFileList(Iterable files, + {required int maxStringLength}) { + final List> batches = >[[]]; + int currentBatchTotalLength = 0; + for (final String file in files) { + final int length = file.length + 1 /* for the space */; + if (currentBatchTotalLength + length > maxStringLength) { + // Start a new batch. + batches.add([]); + currentBatchTotalLength = 0; + } + batches.last.add(file); + currentBatchTotalLength += length; + } + return batches; + } } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 6273fe9bf27..7dadc598d4b 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.3.0 +version: 0.4.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index fabef31a1b6..4728c313655 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -19,8 +19,8 @@ void main() { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; - late p.Context path; late RecordingProcessRunner processRunner; + late FormatCommand analyzeCommand; late CommandRunner runner; late String javaFormatPath; @@ -29,7 +29,7 @@ void main() { mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final FormatCommand analyzeCommand = FormatCommand( + analyzeCommand = FormatCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -37,7 +37,7 @@ void main() { // Create the java formatter file that the command checks for, to avoid // a download. - path = analyzeCommand.path; + final p.Context path = analyzeCommand.path; javaFormatPath = path.join(path.dirname(path.fromUri(mockPlatform.script)), 'google-java-format-1.3-all-deps.jar'); fileSystem.file(javaFormatPath).createSync(recursive: true); @@ -46,13 +46,39 @@ void main() { runner.addCommand(analyzeCommand); }); - List _getAbsolutePaths( + /// Returns a modified version of a list of [relativePaths] that are relative + /// to [package] to instead be relative to [packagesDir]. + List _getPackagesDirRelativePaths( Directory package, List relativePaths) { + final p.Context path = analyzeCommand.path; + final String relativeBase = + path.relative(package.path, from: packagesDir.path); return relativePaths - .map((String relativePath) => path.join(package.path, relativePath)) + .map((String relativePath) => path.join(relativeBase, relativePath)) .toList(); } + /// Returns a list of [count] relative paths to pass to [createFakePlugin] + /// with name [pluginName] such that each path will be 99 characters long + /// relative to [packagesDir]. + /// + /// This is for each of testing batching, since it means each file will + /// consume 100 characters of the batch length. + List _get99CharacterPathExtraFiles(String pluginName, int count) { + final int padding = 99 - + pluginName.length - + 1 - // the path separator after the plugin name + 1 - // the path separator after the padding + 10; // the file name + const int filenameBase = 10000; + + final p.Context path = analyzeCommand.path; + return [ + for (int i = filenameBase; i < filenameBase + count; ++i) + path.join('a' * padding, '$i.dart'), + ]; + } + test('formats .dart files', () async { const List files = [ 'lib/a.dart', @@ -71,8 +97,11 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'flutter', - ['format', ..._getAbsolutePaths(pluginDir, files)], + getFlutterCommand(mockPlatform), + [ + 'format', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], packagesDir.path), ])); }); @@ -85,9 +114,8 @@ void main() { ]; createFakePlugin('a_plugin', packagesDir, extraFiles: files); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess.failing() - ]; + processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [MockProcess.failing()]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { @@ -118,19 +146,20 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + const ProcessCall('java', ['--version'], null), ProcessCall( 'java', [ '-jar', javaFormatPath, '--replace', - ..._getAbsolutePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(pluginDir, files) ], packagesDir.path), ])); }); - test('fails if Java formatter fails', () async { + test('fails with a clear message if Java is not in the path', () async { const List files = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', @@ -146,6 +175,33 @@ void main() { commandError = e; }); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'provide a full path with --java.'), + ])); + }); + + test('fails if Java formatter fails', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['java'] = [ + MockProcess.succeeding(), // check for working java + MockProcess.failing(), // format + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); expect( output, @@ -154,6 +210,35 @@ void main() { ])); }); + test('honors --java flag', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint(runner, ['format', '--java=/path/to/java']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall('/path/to/java', ['--version'], null), + ProcessCall( + '/path/to/java', + [ + '-jar', + javaFormatPath, + '--replace', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + test('formats c-ish files', () async { const List files = [ 'ios/Classes/Foo.h', @@ -174,18 +259,20 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + const ProcessCall('clang-format', ['--version'], null), ProcessCall( 'clang-format', [ '-i', '--style=Google', - ..._getAbsolutePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(pluginDir, files) ], packagesDir.path), ])); }); - test('fails if clang-format fails', () async { + test('fails with a clear message if clang-format is not in the path', + () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', @@ -201,6 +288,62 @@ void main() { commandError = e; }); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to run \'clang-format\'. Make sure that it is in your ' + 'path, or provide a full path with --clang-format.'), + ])); + }); + + test('honors --clang-format flag', () async { + const List files = [ + 'windows/foo_plugin.cpp', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint( + runner, ['format', '--clang-format=/path/to/clang-format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + '/path/to/clang-format', ['--version'], null), + ProcessCall( + '/path/to/clang-format', + [ + '-i', + '--style=Google', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + + test('fails if clang-format fails', () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['clang-format'] = [ + MockProcess.succeeding(), // check for working clang-format + MockProcess.failing(), // format + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); expect( output, @@ -246,12 +389,15 @@ void main() { [ '-i', '--style=Google', - ..._getAbsolutePaths(pluginDir, clangFiles) + ..._getPackagesDirRelativePaths(pluginDir, clangFiles) ], packagesDir.path), ProcessCall( - 'flutter', - ['format', ..._getAbsolutePaths(pluginDir, dartFiles)], + getFlutterCommand(mockPlatform), + [ + 'format', + ..._getPackagesDirRelativePaths(pluginDir, dartFiles) + ], packagesDir.path), ProcessCall( 'java', @@ -259,7 +405,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getAbsolutePaths(pluginDir, javaFiles) + ..._getPackagesDirRelativePaths(pluginDir, javaFiles) ], packagesDir.path), ])); @@ -348,4 +494,97 @@ void main() { contains('Unable to determine diff.'), ])); }); + + test('Batches moderately long file lists on Windows', () async { + mockPlatform.isWindows = true; + + const String pluginName = 'a_plugin'; + // -1 since the command itself takes some length. + const int batchSize = (windowsCommandLineMax ~/ 100) - 1; + + // Make the file list one file longer than would fit in the batch. + final List batch1 = + _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + final String extraFile = batch1.removeLast(); + + createFakePlugin( + pluginName, + packagesDir, + extraFiles: [...batch1, extraFile], + ); + + await runCapturingPrint(runner, ['format']); + + // Ensure that it was batched... + expect(processRunner.recordedCalls.length, 2); + // ... and that the spillover into the second batch was only one file. + expect( + processRunner.recordedCalls, + contains( + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + '$pluginName\\$extraFile', + ], + packagesDir.path), + )); + }); + + // Validates that the Windows limit--which is much lower than the limit on + // other platforms--isn't being used on all platforms, as that would make + // formatting slower on Linux and macOS. + test('Does not batch moderately long file lists on non-Windows', () async { + const String pluginName = 'a_plugin'; + // -1 since the command itself takes some length. + const int batchSize = (windowsCommandLineMax ~/ 100) - 1; + + // Make the file list one file longer than would fit in a Windows batch. + final List batch = + _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + + createFakePlugin( + pluginName, + packagesDir, + extraFiles: batch, + ); + + await runCapturingPrint(runner, ['format']); + + expect(processRunner.recordedCalls.length, 1); + }); + + test('Batches extremely long file lists on non-Windows', () async { + const String pluginName = 'a_plugin'; + // -1 since the command itself takes some length. + const int batchSize = (nonWindowsCommandLineMax ~/ 100) - 1; + + // Make the file list one file longer than would fit in the batch. + final List batch1 = + _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + final String extraFile = batch1.removeLast(); + + createFakePlugin( + pluginName, + packagesDir, + extraFiles: [...batch1, extraFile], + ); + + await runCapturingPrint(runner, ['format']); + + // Ensure that it was batched... + expect(processRunner.recordedCalls.length, 2); + // ... and that the spillover into the second batch was only one file. + expect( + processRunner.recordedCalls, + contains( + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + '$pluginName/$extraFile', + ], + packagesDir.path), + )); + }); } From ca5e75348664b450158bab3bd4549ba3b0329b85 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 13 Jul 2021 13:25:41 -0700 Subject: [PATCH 097/249] [flutter_plugin_tools] Improve license-check output (#4154) Currently each type of check handles its output in isolation, which creates confusing output when the last check succeeds but an earlier check fails, since the end of the output will just be a success message. This makes the output follow the same basic approach as the package looper commands, where all failures are collected, and then a final summary is presented at the end, so the last message will always reflect the important details. It also adopts the colorized output now used by most other commands. --- script/tool/CHANGELOG.md | 4 + .../tool/lib/src/license_check_command.dart | 122 ++++++++------- .../tool/test/license_check_command_test.dart | 147 +++++++++++++----- 3 files changed, 175 insertions(+), 98 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 9db94dda37d..17b28927538 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +- Improved `license-check` output. + ## 0.4.0 - Modified the output format of many commands diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 093f8143df4..e68585c44bd 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -107,21 +107,65 @@ class LicenseCheckCommand extends PluginCommand { @override Future run() async { - final Iterable codeFiles = (await _getAllFiles()).where((File file) => + final Iterable allFiles = await _getAllFiles(); + + final Iterable codeFiles = allFiles.where((File file) => _codeFileExtensions.contains(p.extension(file.path)) && !_shouldIgnoreFile(file)); - final Iterable firstPartyLicenseFiles = (await _getAllFiles()).where( - (File file) => - path.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); + final Iterable firstPartyLicenseFiles = allFiles.where((File file) => + path.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); - final bool copyrightCheckSucceeded = await _checkCodeLicenses(codeFiles); - print('\n=======================================\n'); - final bool licenseCheckSucceeded = + final List licenseFileFailures = await _checkLicenseFiles(firstPartyLicenseFiles); + final Map<_LicenseFailureType, List> codeFileFailures = + await _checkCodeLicenses(codeFiles); + + bool passed = true; + + print('\n=======================================\n'); + + if (licenseFileFailures.isNotEmpty) { + passed = false; + printError( + 'The following LICENSE files do not follow the expected format:'); + for (final File file in licenseFileFailures) { + printError(' ${file.path}'); + } + printError('Please ensure that they use the exact format used in this ' + 'repository".\n'); + } + + if (codeFileFailures[_LicenseFailureType.incorrectFirstParty]!.isNotEmpty) { + passed = false; + printError('The license block for these files is missing or incorrect:'); + for (final File file + in codeFileFailures[_LicenseFailureType.incorrectFirstParty]!) { + printError(' ${file.path}'); + } + printError( + 'If this third-party code, move it to a "third_party/" directory, ' + 'otherwise ensure that you are using the exact copyright and license ' + 'text used by all first-party files in this repository.\n'); + } + + if (codeFileFailures[_LicenseFailureType.unknownThirdParty]!.isNotEmpty) { + passed = false; + printError( + 'No recognized license was found for the following third-party files:'); + for (final File file + in codeFileFailures[_LicenseFailureType.unknownThirdParty]!) { + printError(' ${file.path}'); + } + print('Please check that they have a license at the top of the file. ' + 'If they do, the license check needs to be updated to recognize ' + 'the new third-party license block.\n'); + } - if (!copyrightCheckSucceeded || !licenseCheckSucceeded) { + if (!passed) { throw ToolExit(1); } + + printSuccess('All files passed validation!'); } // Creates the expected copyright+license block for first-party code. @@ -135,9 +179,10 @@ class LicenseCheckCommand extends PluginCommand { '${comment}found in the LICENSE file.$suffix\n'; } - // Checks all license blocks for [codeFiles], returning false if any of them - // fail validation. - Future _checkCodeLicenses(Iterable codeFiles) async { + /// Checks all license blocks for [codeFiles], returning any that fail + /// validation. + Future>> _checkCodeLicenses( + Iterable codeFiles) async { final List incorrectFirstPartyFiles = []; final List unrecognizedThirdPartyFiles = []; @@ -171,7 +216,6 @@ class LicenseCheckCommand extends PluginCommand { } } } - print('\n'); // Sort by path for more usable output. final int Function(File, File) pathCompare = @@ -179,38 +223,14 @@ class LicenseCheckCommand extends PluginCommand { incorrectFirstPartyFiles.sort(pathCompare); unrecognizedThirdPartyFiles.sort(pathCompare); - if (incorrectFirstPartyFiles.isNotEmpty) { - print('The license block for these files is missing or incorrect:'); - for (final File file in incorrectFirstPartyFiles) { - print(' ${file.path}'); - } - print('If this third-party code, move it to a "third_party/" directory, ' - 'otherwise ensure that you are using the exact copyright and license ' - 'text used by all first-party files in this repository.\n'); - } - - if (unrecognizedThirdPartyFiles.isNotEmpty) { - print( - 'No recognized license was found for the following third-party files:'); - for (final File file in unrecognizedThirdPartyFiles) { - print(' ${file.path}'); - } - print('Please check that they have a license at the top of the file. ' - 'If they do, the license check needs to be updated to recognize ' - 'the new third-party license block.\n'); - } - - final bool succeeded = - incorrectFirstPartyFiles.isEmpty && unrecognizedThirdPartyFiles.isEmpty; - if (succeeded) { - print('All source files passed validation!'); - } - return succeeded; + return <_LicenseFailureType, List>{ + _LicenseFailureType.incorrectFirstParty: incorrectFirstPartyFiles, + _LicenseFailureType.unknownThirdParty: unrecognizedThirdPartyFiles, + }; } - // Checks all provide LICENSE files, returning false if any of them - // fail validation. - Future _checkLicenseFiles(Iterable files) async { + /// Checks all provided LICENSE [files], returning any that fail validation. + Future> _checkLicenseFiles(Iterable files) async { final List incorrectLicenseFiles = []; for (final File file in files) { @@ -219,22 +239,8 @@ class LicenseCheckCommand extends PluginCommand { incorrectLicenseFiles.add(file); } } - print('\n'); - if (incorrectLicenseFiles.isNotEmpty) { - print('The following LICENSE files do not follow the expected format:'); - for (final File file in incorrectLicenseFiles) { - print(' ${file.path}'); - } - print( - 'Please ensure that they use the exact format used in this repository".\n'); - } - - final bool succeeded = incorrectLicenseFiles.isEmpty; - if (succeeded) { - print('All LICENSE files passed validation!'); - } - return succeeded; + return incorrectLicenseFiles; } bool _shouldIgnoreFile(File file) { @@ -255,3 +261,5 @@ class LicenseCheckCommand extends PluginCommand { .map((FileSystemEntity file) => file as File) .toList(); } + +enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty } diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 64adc9214d8..288cf4696a5 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -131,8 +131,12 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check a file. - expect(output, contains('Checking checked.cc')); - expect(output, contains('All source files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking checked.cc'), + contains('All files passed validation!'), + ])); }); test('handles the comment styles for all supported languages', () async { @@ -150,10 +154,14 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the files. - expect(output, contains('Checking file_a.cc')); - expect(output, contains('Checking file_b.sh')); - expect(output, contains('Checking file_c.html')); - expect(output, contains('All source files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking file_a.cc'), + contains('Checking file_b.sh'), + contains('Checking file_c.html'), + contains('All files passed validation!'), + ])); }); test('fails if any checked files are missing license blocks', () async { @@ -176,12 +184,14 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'The license block for these files is missing or incorrect:')); - expect(output, contains(' bad.cc')); - expect(output, contains(' bad.h')); + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + contains(' bad.h'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('fails if any checked files are missing just the copyright', () async { @@ -202,11 +212,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'The license block for these files is missing or incorrect:')); - expect(output, contains(' bad.cc')); + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('fails if any checked files are missing just the license', () async { @@ -227,11 +239,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'The license block for these files is missing or incorrect:')); - expect(output, contains(' bad.cc')); + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('fails if any third-party code is not in a third_party directory', @@ -250,11 +264,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'The license block for these files is missing or incorrect:')); - expect(output, contains(' third_party.cc')); + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' third_party.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('succeeds for third-party code in a third_party directory', () async { @@ -276,8 +292,12 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the file. - expect(output, contains('Checking a_plugin/lib/src/third_party/file.cc')); - expect(output, contains('All source files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking a_plugin/lib/src/third_party/file.cc'), + contains('All files passed validation!'), + ])); }); test('allows first-party code in a third_party directory', () async { @@ -294,9 +314,12 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the file. - expect(output, - contains('Checking a_plugin/lib/src/third_party/first_party.cc')); - expect(output, contains('All source files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking a_plugin/lib/src/third_party/first_party.cc'), + contains('All files passed validation!'), + ])); }); test('fails for licenses that the tool does not expect', () async { @@ -320,11 +343,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'No recognized license was found for the following third-party files:')); - expect(output, contains(' third_party/bad.cc')); + containsAllInOrder([ + contains( + 'No recognized license was found for the following third-party files:'), + contains(' third_party/bad.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('Apache is not recognized for new authors without validation changes', @@ -353,11 +378,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'No recognized license was found for the following third-party files:')); - expect(output, contains(' third_party/bad.cc')); + containsAllInOrder([ + contains( + 'No recognized license was found for the following third-party files:'), + contains(' third_party/bad.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('passes if all first-party LICENSE files are correctly formatted', @@ -370,8 +397,12 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the file. - expect(output, contains('Checking LICENSE')); - expect(output, contains('All LICENSE files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking LICENSE'), + contains('All files passed validation!'), + ])); }); test('fails if any first-party LICENSE files are incorrectly formatted', @@ -387,7 +418,7 @@ void main() { }); expect(commandError, isA()); - expect(output, isNot(contains('All LICENSE files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('ignores third-party LICENSE format', () async { @@ -400,8 +431,42 @@ void main() { await runCapturingPrint(runner, ['license-check']); // The file shouldn't be checked. - expect(output, isNot(contains('Checking third_party/LICENSE'))); - expect(output, contains('All LICENSE files passed validation!')); + expect(output, isNot(contains(contains('Checking third_party/LICENSE')))); + }); + + test('outputs all errors at the end', () async { + root.childFile('bad.cc').createSync(); + root + .childDirectory('third_party') + .childFile('bad.cc') + .createSync(recursive: true); + final File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_incorrectLicenseFileText); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Checking LICENSE'), + contains('Checking bad.cc'), + contains('Checking third_party/bad.cc'), + contains( + 'The following LICENSE files do not follow the expected format:'), + contains(' LICENSE'), + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + contains( + 'No recognized license was found for the following third-party files:'), + contains(' third_party/bad.cc'), + ])); }); }); } From a2067827598b3da2727b9a7f77a5d56ae82187f6 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 20 Jul 2021 10:01:07 -0700 Subject: [PATCH 098/249] [flutter_plugin_tools] Use -version with java (#4171) --- script/tool/CHANGELOG.md | 4 +++- script/tool/lib/src/format_command.dart | 5 ++++- script/tool/pubspec.yaml | 2 +- script/tool/test/format_command_test.dart | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 17b28927538..1e447721d13 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 0.4.1 - Improved `license-check` output. +- Use `java -version` rather than `java --version`, for compatibility with more + versions of Java. ## 0.4.0 diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index c67fb96d283..d09a94b1aef 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -244,9 +244,12 @@ class FormatCommand extends PluginCommand { /// Returns true if [command] can be run successfully. Future _hasDependency(String command) async { + // Some versions of Java accept both -version and --version, but some only + // accept -version. + final String versionFlag = command == 'java' ? '-version' : '--version'; try { final io.ProcessResult result = - await processRunner.run(command, ['--version']); + await processRunner.run(command, [versionFlag]); if (result.exitCode != 0) { return false; } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 7dadc598d4b..7b2cdd4f410 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.4.0 +version: 0.4.1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index 4728c313655..b072e5d30aa 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -146,7 +146,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - const ProcessCall('java', ['--version'], null), + const ProcessCall('java', ['-version'], null), ProcessCall( 'java', [ From ff8cb52f8e3d9ad776e0f500ca8096b9cdfd08b2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 20 Jul 2021 18:17:20 -0700 Subject: [PATCH 099/249] [flutter_plugin_tests] Split analyze out of xctest (#4161) To prep for making a combined command to run native tests across different platforms, rework `xctest`: - Split analyze out into a new `xcode-analyze` command: - Since the analyze step runs a new build over everything with different flags, this is only a small amount slower than the combined version - This makes the logic easier to follow - This allows us to meaningfully report skips, to better notice missing tests. - Add the ability to target specific test bundles (RunnerTests or RunnerUITests) To share code between the commands, this extracts a new `Xcode` helper class. Part of https://github.com/flutter/flutter/issues/84392 and https://github.com/flutter/flutter/issues/86489 --- script/tool/CHANGELOG.md | 6 + script/tool/lib/src/common/xcode.dart | 159 +++++++ script/tool/lib/src/main.dart | 2 + .../tool/lib/src/xcode_analyze_command.dart | 111 +++++ script/tool/lib/src/xctest_command.dart | 219 ++++----- script/tool/test/common/xcode_test.dart | 396 ++++++++++++++++ .../tool/test/xcode_analyze_command_test.dart | 416 +++++++++++++++++ script/tool/test/xctest_command_test.dart | 427 +++++++++++------- 8 files changed, 1440 insertions(+), 296 deletions(-) create mode 100644 script/tool/lib/src/common/xcode.dart create mode 100644 script/tool/lib/src/xcode_analyze_command.dart create mode 100644 script/tool/test/common/xcode_test.dart create mode 100644 script/tool/test/xcode_analyze_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1e447721d13..377e7860bd2 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## NEXT + +- Added an `xctest` flag to select specific test targets, to allow running only + unit tests or integration tests. +- Split Xcode analysis out of `xctest` and into a new `xcode-analyze` command. + ## 0.4.1 - Improved `license-check` output. diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart new file mode 100644 index 00000000000..d6bbae419ed --- /dev/null +++ b/script/tool/lib/src/common/xcode.dart @@ -0,0 +1,159 @@ +// 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'; +import 'process_runner.dart'; + +const String _xcodeBuildCommand = 'xcodebuild'; +const String _xcRunCommand = 'xcrun'; + +/// A utility class for interacting with the installed version of Xcode. +class Xcode { + /// Creates an instance that runs commends with the given [processRunner]. + /// + /// If [log] is true, commands run by this instance will long various status + /// messages. + Xcode({ + this.processRunner = const ProcessRunner(), + this.log = false, + }); + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// Whether or not to log when running commands. + final bool log; + + /// Runs an `xcodebuild` in [directory] with the given parameters. + Future runXcodeBuild( + Directory directory, { + List actions = const ['build'], + required String workspace, + required String scheme, + String? configuration, + List extraFlags = const [], + }) { + final List args = [ + _xcodeBuildCommand, + ...actions, + if (workspace != null) ...['-workspace', workspace], + if (scheme != null) ...['-scheme', scheme], + if (configuration != null) ...['-configuration', configuration], + ...extraFlags, + ]; + final String completeTestCommand = '$_xcRunCommand ${args.join(' ')}'; + if (log) { + print(completeTestCommand); + } + return processRunner.runAndStream(_xcRunCommand, args, + workingDir: directory); + } + + /// Returns true if [project], which should be an .xcodeproj directory, + /// contains a target called [target], false if it does not, and null if the + /// check fails (e.g., if [project] is not an Xcode project). + Future projectHasTarget(Directory project, String target) async { + final io.ProcessResult result = + await processRunner.run(_xcRunCommand, [ + _xcodeBuildCommand, + '-list', + '-json', + '-project', + project.path, + ]); + if (result.exitCode != 0) { + return null; + } + Map? projectInfo; + try { + projectInfo = (jsonDecode(result.stdout as String) + as Map)['project'] as Map?; + } on FormatException { + return null; + } + if (projectInfo == null) { + return null; + } + final List? targets = + (projectInfo['targets'] as List?)?.cast(); + return targets?.contains(target) ?? false; + } + + /// Returns the newest available simulator (highest OS version, with ties + /// broken in favor of newest device), if any. + Future findBestAvailableIphoneSimulator() async { + final List findSimulatorsArguments = [ + 'simctl', + 'list', + 'devices', + 'runtimes', + 'available', + '--json', + ]; + final String findSimulatorCompleteCommand = + '$_xcRunCommand ${findSimulatorsArguments.join(' ')}'; + if (log) { + print('Looking for available simulators...'); + print(findSimulatorCompleteCommand); + } + final io.ProcessResult findSimulatorsResult = + await processRunner.run(_xcRunCommand, findSimulatorsArguments); + if (findSimulatorsResult.exitCode != 0) { + if (log) { + printError( + 'Error occurred while running "$findSimulatorCompleteCommand":\n' + '${findSimulatorsResult.stderr}'); + } + return null; + } + final Map simulatorListJson = + jsonDecode(findSimulatorsResult.stdout as String) + as Map; + final List> runtimes = + (simulatorListJson['runtimes'] as List) + .cast>(); + final Map devices = + (simulatorListJson['devices'] as Map) + .cast(); + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String? id; + // Looking for runtimes, trying to find one with highest OS version. + for (final Map rawRuntimeMap in runtimes.reversed) { + final Map runtimeMap = + rawRuntimeMap.cast(); + if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { + continue; + } + final String? runtimeID = runtimeMap['identifier'] as String?; + if (runtimeID == null) { + continue; + } + final List>? devicesForRuntime = + (devices[runtimeID] as List?)?.cast>(); + if (devicesForRuntime == null || devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (final Map rawDevice in devicesForRuntime.reversed) { + final Map device = rawDevice.cast(); + id = device['udid'] as String?; + if (id == null) { + continue; + } + if (log) { + print('device selected: $device'); + } + return id; + } + } + return null; + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index f397a04aa66..ef1a18ab15b 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -24,6 +24,7 @@ import 'publish_plugin_command.dart'; import 'pubspec_check_command.dart'; import 'test_command.dart'; import 'version_check_command.dart'; +import 'xcode_analyze_command.dart'; import 'xctest_command.dart'; void main(List args) { @@ -59,6 +60,7 @@ void main(List args) { ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) + ..addCommand(XcodeAnalyzeCommand(packagesDir)) ..addCommand(XCTestCommand(packagesDir)); commandRunner.run(args).catchError((Object e) { diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart new file mode 100644 index 00000000000..27cd8c43514 --- /dev/null +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -0,0 +1,111 @@ +// 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:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; +import 'common/xcode.dart'; + +/// The command to run Xcode's static analyzer on plugins. +class XcodeAnalyzeCommand extends PackageLoopingCommand { + /// Creates an instance of the test command. + XcodeAnalyzeCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : _xcode = Xcode(processRunner: processRunner, log: true), + super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addFlag(kPlatformIos, help: 'Analyze iOS'); + argParser.addFlag(kPlatformMacos, help: 'Analyze macOS'); + } + + final Xcode _xcode; + + @override + final String name = 'xcode-analyze'; + + @override + final String description = + 'Runs Xcode analysis on the iOS and/or macOS example apps.'; + + @override + Future initializeRun() async { + if (!(getBoolArg(kPlatformIos) || getBoolArg(kPlatformMacos))) { + printError('At least one platform flag must be provided.'); + throw ToolExit(exitInvalidArguments); + } + } + + @override + Future runForPackage(Directory package) async { + final bool testIos = getBoolArg(kPlatformIos) && + pluginSupportsPlatform(kPlatformIos, package, + requiredMode: PlatformSupport.inline); + final bool testMacos = getBoolArg(kPlatformMacos) && + pluginSupportsPlatform(kPlatformMacos, package, + requiredMode: PlatformSupport.inline); + + final bool multiplePlatformsRequested = + getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos); + if (!(testIos || testMacos)) { + return PackageResult.skip('Not implemented for target platform(s).'); + } + + final List failures = []; + if (testIos && + !await _analyzePlugin(package, 'iOS', extraFlags: [ + '-destination', + 'generic/platform=iOS Simulator' + ])) { + failures.add('iOS'); + } + if (testMacos && !await _analyzePlugin(package, 'macOS')) { + failures.add('macOS'); + } + + // Only provide the failing platform in the failure details if testing + // multiple platforms, otherwise it's just noise. + return failures.isEmpty + ? PackageResult.success() + : PackageResult.fail( + multiplePlatformsRequested ? failures : []); + } + + /// Analyzes [plugin] for [platform], returning true if it passed analysis. + Future _analyzePlugin( + Directory plugin, + String platform, { + List extraFlags = const [], + }) async { + bool passing = true; + for (final Directory example in getExamplesForPlugin(plugin)) { + // Running tests and static analyzer. + final String examplePath = + getRelativePosixPath(example, from: plugin.parent); + print('Running $platform tests and analyzer for $examplePath...'); + final int exitCode = await _xcode.runXcodeBuild( + example, + actions: ['analyze'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + ); + if (exitCode == 0) { + printSuccess('$examplePath ($platform) passed analysis.'); + } else { + printError('$examplePath ($platform) failed analysis.'); + passing = false; + } + } + return passing; + } +} diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 176adad39a0..44fc3a87d54 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -2,9 +2,6 @@ // 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 'package:platform/platform.dart'; @@ -12,35 +9,39 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/xcode.dart'; + +const String _iosDestinationFlag = 'ios-destination'; +const String _testTargetFlag = 'test-target'; -const String _kiOSDestination = 'ios-destination'; -const String _kXcodeBuildCommand = 'xcodebuild'; -const String _kXCRunCommand = 'xcrun'; -const String _kFoundNoSimulatorsMessage = - 'Cannot find any available simulators, tests failed'; +// The exit code from 'xcodebuild test' when there are no tests. +const int _xcodebuildNoTestExitCode = 66; -const int _exitFindingSimulatorsFailed = 3; -const int _exitNoSimulators = 4; +const int _exitNoSimulators = 3; /// The command to run XCTests (XCUnitTest and XCUITest) in plugins. /// The tests target have to be added to the Xcode project of the example app, /// usually at "example/{ios,macos}/Runner.xcworkspace". -/// -/// The static analyzer is also run. class XCTestCommand extends PackageLoopingCommand { /// Creates an instance of the test command. XCTestCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { + }) : _xcode = Xcode(processRunner: processRunner, log: true), + super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addOption( - _kiOSDestination, + _iosDestinationFlag, help: 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' 'this is passed to the `-destination` argument in xcodebuild command.\n' 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', ); + argParser.addOption( + _testTargetFlag, + help: + 'Limits the tests to a specific target (e.g., RunnerTests or RunnerUITests)', + ); argParser.addFlag(kPlatformIos, help: 'Runs the iOS tests'); argParser.addFlag(kPlatformMacos, help: 'Runs the macOS tests'); } @@ -48,6 +49,8 @@ class XCTestCommand extends PackageLoopingCommand { // The device destination flags for iOS tests. List _iosDestinationFlags = []; + final Xcode _xcode; + @override final String name = 'xctest'; @@ -56,9 +59,6 @@ class XCTestCommand extends PackageLoopingCommand { 'Runs the xctests in the iOS and/or macOS example apps.\n\n' 'This command requires "flutter" and "xcrun" to be in your path.'; - @override - String get failureListHeader => 'The following packages are failing XCTests:'; - @override Future initializeRun() async { final bool shouldTestIos = getBoolArg(kPlatformIos); @@ -70,11 +70,12 @@ class XCTestCommand extends PackageLoopingCommand { } if (shouldTestIos) { - String destination = getStringArg(_kiOSDestination); + String destination = getStringArg(_iosDestinationFlag); if (destination.isEmpty) { - final String? simulatorId = await _findAvailableIphoneSimulator(); + final String? simulatorId = + await _xcode.findBestAvailableIphoneSimulator(); if (simulatorId == null) { - printError(_kFoundNoSimulatorsMessage); + printError('Cannot find any available simulators, tests failed'); throw ToolExit(_exitNoSimulators); } destination = 'id=$simulatorId'; @@ -115,15 +116,26 @@ class XCTestCommand extends PackageLoopingCommand { } final List failures = []; - if (testIos && - !await _testPlugin(package, 'iOS', - extraXcrunFlags: _iosDestinationFlags)) { - failures.add('iOS'); + bool ranTests = false; + if (testIos) { + final RunState result = await _testPlugin(package, 'iOS', + extraXcrunFlags: _iosDestinationFlags); + ranTests |= result != RunState.skipped; + if (result == RunState.failed) { + failures.add('iOS'); + } } - if (testMacos && !await _testPlugin(package, 'macOS')) { - failures.add('macOS'); + if (testMacos) { + final RunState result = await _testPlugin(package, 'macOS'); + ranTests |= result != RunState.skipped; + if (result == RunState.failed) { + failures.add('macOS'); + } } + if (!ranTests) { + return PackageResult.skip('No tests found.'); + } // Only provide the failing platform in the failure details if testing // multiple platforms, otherwise it's just noise. return failures.isEmpty @@ -133,124 +145,67 @@ class XCTestCommand extends PackageLoopingCommand { } /// Runs all applicable tests for [plugin], printing status and returning - /// success if the tests passed. - Future _testPlugin( + /// the test result. + Future _testPlugin( Directory plugin, String platform, { List extraXcrunFlags = const [], }) async { - bool passing = true; + final String testTarget = getStringArg(_testTargetFlag); + + // Assume skipped until at least one test has run. + RunState overallResult = RunState.skipped; for (final Directory example in getExamplesForPlugin(plugin)) { - // Running tests and static analyzer. final String examplePath = getRelativePosixPath(example, from: plugin.parent); - print('Running $platform tests and analyzer for $examplePath...'); - int exitCode = - await _runTests(true, example, platform, extraFlags: extraXcrunFlags); - // 66 = there is no test target (this fails fast). Try again with just the analyzer. - if (exitCode == 66) { - print('Tests not found for $examplePath, running analyzer only...'); - exitCode = await _runTests(false, example, platform, - extraFlags: extraXcrunFlags); - } - if (exitCode == 0) { - printSuccess('Successfully ran $platform xctest for $examplePath'); - } else { - passing = false; - } - } - return passing; - } - Future _runTests( - bool runTests, - Directory example, - String platform, { - List extraFlags = const [], - }) { - final List xctestArgs = [ - _kXcodeBuildCommand, - if (runTests) 'test', - 'analyze', - '-workspace', - '${platform.toLowerCase()}/Runner.xcworkspace', - '-configuration', - 'Debug', - '-scheme', - 'Runner', - ...extraFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ]; - final String completeTestCommand = - '$_kXCRunCommand ${xctestArgs.join(' ')}'; - print(completeTestCommand); - return processRunner.runAndStream(_kXCRunCommand, xctestArgs, - workingDir: example); - } - - Future _findAvailableIphoneSimulator() async { - // Find the first available destination if not specified. - final List findSimulatorsArguments = [ - 'simctl', - 'list', - '--json' - ]; - final String findSimulatorCompleteCommand = - '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; - print('Looking for available simulators...'); - print(findSimulatorCompleteCommand); - final io.ProcessResult findSimulatorsResult = - await processRunner.run(_kXCRunCommand, findSimulatorsArguments); - if (findSimulatorsResult.exitCode != 0) { - printError( - 'Error occurred while running "$findSimulatorCompleteCommand":\n' - '${findSimulatorsResult.stderr}'); - throw ToolExit(_exitFindingSimulatorsFailed); - } - final Map simulatorListJson = - jsonDecode(findSimulatorsResult.stdout as String) - as Map; - final List> runtimes = - (simulatorListJson['runtimes'] as List) - .cast>(); - final Map devices = - (simulatorListJson['devices'] as Map) - .cast(); - if (runtimes.isEmpty || devices.isEmpty) { - return null; - } - String? id; - // Looking for runtimes, trying to find one with highest OS version. - for (final Map rawRuntimeMap in runtimes.reversed) { - final Map runtimeMap = - rawRuntimeMap.cast(); - if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { - continue; - } - final String? runtimeID = runtimeMap['identifier'] as String?; - if (runtimeID == null) { - continue; - } - final List>? devicesForRuntime = - (devices[runtimeID] as List?)?.cast>(); - if (devicesForRuntime == null || devicesForRuntime.isEmpty) { - continue; - } - // Looking for runtimes, trying to find latest version of device. - for (final Map rawDevice in devicesForRuntime.reversed) { - final Map device = rawDevice.cast(); - if (device['availabilityError'] != null || - (device['isAvailable'] as bool?) == false) { + if (testTarget.isNotEmpty) { + final Directory project = example + .childDirectory(platform.toLowerCase()) + .childDirectory('Runner.xcodeproj'); + final bool? hasTarget = + await _xcode.projectHasTarget(project, testTarget); + if (hasTarget == null) { + printError('Unable to check targets for $examplePath.'); + overallResult = RunState.failed; continue; - } - id = device['udid'] as String?; - if (id == null) { + } else if (!hasTarget) { + print('No "$testTarget" target in $examplePath; skipping.'); continue; } - print('device selected: $device'); - return id; + } + + print('Running $platform tests for $examplePath...'); + final int exitCode = await _xcode.runXcodeBuild( + example, + actions: ['test'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + if (testTarget.isNotEmpty) '-only-testing:$testTarget', + ...extraXcrunFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + ); + + switch (exitCode) { + case _xcodebuildNoTestExitCode: + print('No tests found for $examplePath'); + continue; + case 0: + printSuccess('Successfully ran $platform xctest for $examplePath'); + // If this is the first test, assume success until something fails. + if (overallResult == RunState.skipped) { + overallResult = RunState.succeeded; + } + break; + default: + // Any failure means a failure overall. + overallResult = RunState.failed; + break; } } - return null; + return overallResult; } } diff --git a/script/tool/test/common/xcode_test.dart b/script/tool/test/common/xcode_test.dart new file mode 100644 index 00000000000..7e046a2446c --- /dev/null +++ b/script/tool/test/common/xcode_test.dart @@ -0,0 +1,396 @@ +// 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 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/common/xcode.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; + +void main() { + late RecordingProcessRunner processRunner; + late Xcode xcode; + + setUp(() { + processRunner = RecordingProcessRunner(); + xcode = Xcode(processRunner: processRunner); + }); + + group('findBestAvailableIphoneSimulator', () { + test('finds the newest device', () async { + const String expectedDeviceId = '1E76A0FD-38AC-4537-A989-EA639D7D012A'; + // Note: This uses `dynamic` deliberately, and should not be updated to + // Object, in order to ensure that the code correctly handles this return + // type from JSON decoding. + final Map devices = { + 'runtimes': >[ + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime', + 'buildversion': '17A577', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', + 'version': '13.0', + 'isAvailable': true, + 'name': 'iOS 13.0' + }, + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', + 'buildversion': '17L255', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', + 'version': '13.4', + 'isAvailable': true, + 'name': 'iOS 13.4' + }, + { + 'bundlePath': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', + 'buildversion': '17T531', + 'runtimeRoot': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', + 'version': '6.2.1', + 'isAvailable': true, + 'name': 'watchOS 6.2' + } + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8', + 'state': 'Shutdown', + 'name': 'iPhone 8' + }, + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': expectedDeviceId, + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', + 'state': 'Shutdown', + 'name': 'iPhone 8 Plus' + } + ] + } + }; + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = jsonEncode(devices); + + expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId); + }); + + test('ignores non-iOS runtimes', () async { + // Note: This uses `dynamic` deliberately, and should not be updated to + // Object, in order to ensure that the code correctly handles this return + // type from JSON decoding. + final Map devices = { + 'runtimes': >[ + { + 'bundlePath': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', + 'buildversion': '17T531', + 'runtimeRoot': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', + 'version': '6.2.1', + 'isAvailable': true, + 'name': 'watchOS 6.2' + } + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2': + >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm', + 'state': 'Shutdown', + 'name': 'Apple Watch' + } + ] + } + }; + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = jsonEncode(devices); + + expect(await xcode.findBestAvailableIphoneSimulator(), null); + }); + + test('returns null if simctl fails', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing(), + ]; + + expect(await xcode.findBestAvailableIphoneSimulator(), null); + }); + }); + + group('runXcodeBuild', () { + test('handles minimal arguments', () async { + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild( + directory, + workspace: 'A.xcworkspace', + scheme: 'AScheme', + ); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'build', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + ], + directory.path), + ])); + }); + + test('handles all arguments', () async { + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild(directory, + actions: ['action1', 'action2'], + workspace: 'A.xcworkspace', + scheme: 'AScheme', + configuration: 'Debug', + extraFlags: ['-a', '-b', 'c=d']); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'action1', + 'action2', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + '-configuration', + 'Debug', + '-a', + '-b', + 'c=d', + ], + directory.path), + ])); + }); + + test('returns error codes', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing(), + ]; + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild( + directory, + workspace: 'A.xcworkspace', + scheme: 'AScheme', + ); + + expect(exitCode, 1); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'build', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + ], + directory.path), + ])); + }); + }); + + group('projectHasTarget', () { + test('returns true when present', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = ''' +{ + "project" : { + "configurations" : [ + "Debug", + "Release" + ], + "name" : "Runner", + "schemes" : [ + "Runner" + ], + "targets" : [ + "Runner", + "RunnerTests", + "RunnerUITests" + ] + } +}'''; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), true); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns false when not present', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = ''' +{ + "project" : { + "configurations" : [ + "Debug", + "Release" + ], + "name" : "Runner", + "schemes" : [ + "Runner" + ], + "targets" : [ + "Runner", + "RunnerUITests" + ] + } +}'''; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), false); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for unexpected output', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = '{}'; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for invalid output', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = ':)'; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for failure', () async { + processRunner.processToReturn = MockProcess.failing(); + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + }); +} diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart new file mode 100644 index 00000000000..b715ac531f5 --- /dev/null +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -0,0 +1,416 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.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/xcode_analyze_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of +// doing all the process mocking and validation. +void main() { + group('test xcode_analyze_command', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final XcodeAnalyzeCommand command = XcodeAnalyzeCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'xcode_analyze_command', 'Test for xcode_analyze_command'); + runner.addCommand(command); + }); + + test('Fails if no platforms are provided', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xcode-analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one platform flag must be provided'), + ]), + ); + }); + + group('iOS', () { + test('skip if iOS is not supported', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final List output = + await runCapturingPrint(runner, ['xcode-analyze', '--ios']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if iOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.federated + }); + + final List output = + await runCapturingPrint(runner, ['xcode-analyze', '--ios']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('plugin/example (iOS) passed analysis.') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'xcode-analyze', + '--ios', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ])); + }); + }); + + group('macOS', () { + test('skip if macOS is not supported', () async { + createFakePlugin( + 'plugin', + packagesDir, + ); + + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if macOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.federated, + }); + + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--macos', + ]); + + expect(output, + contains(contains('plugin/example (macOS) passed analysis.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ]), + ); + }); + }); + + group('combined', () { + test('runs both iOS and macOS when supported', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAll([ + contains('plugin/example (iOS) passed analysis.'), + contains('plugin/example (macOS) passed analysis.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only macOS for a macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('plugin/example (macOS) passed analysis.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only iOS for a iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder( + [contains('plugin/example (iOS) passed analysis.')])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when neither are supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + }); + }); +} diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index aa6d23fb56f..324dea0e71e 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -16,22 +16,8 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; -// Note: This uses `dynamic` deliberately, and should not be updated to Object, -// in order to ensure that the code correctly handles this return type from -// JSON decoding. final Map _kDeviceListMap = { 'runtimes': >[ - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime', - 'buildversion': '17A577', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', - 'version': '13.0', - 'isAvailable': true, - 'name': 'iOS 13.0' - }, { 'bundlePath': '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', @@ -43,32 +29,9 @@ final Map _kDeviceListMap = { 'isAvailable': true, 'name': 'iOS 13.4' }, - { - 'bundlePath': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', - 'buildversion': '17T531', - 'runtimeRoot': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', - 'version': '6.2.1', - 'isAvailable': true, - 'name': 'watchOS 6.2' - } ], 'devices': { 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774', - 'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8', - 'state': 'Shutdown', - 'name': 'iPhone 8' - }, { 'dataPath': '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', @@ -85,6 +48,8 @@ final Map _kDeviceListMap = { } }; +// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of +// doing all the process mocking and validation. void main() { const String _kDestination = '--ios-destination'; @@ -123,13 +88,198 @@ void main() { ); }); + test('allows target filtering', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; + + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + '--test-target=RunnerTests', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-only-testing:RunnerTests', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when the requested target is not present', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = '{"project":{"targets":["Runner"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + '--test-target=RunnerTests', + ]); + + expect( + output, + containsAllInOrder([ + contains('No "RunnerTests" target in plugin/example; skipping.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + + test('fails if unable to check for requested target', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.failing(); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + '--test-target=RunnerTests', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to check targets for plugin/example.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + + test('reports skips with no tests', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + // Exit code 66 from testing indicates no tests. + final MockProcess noTestsProcessResult = MockProcess(); + noTestsProcessResult.exitCodeCompleter.complete(66); + processRunner.mockProcessesForExecutable['xcrun'] = [ + noTestsProcessResult, + ]; + final List output = + await runCapturingPrint(runner, ['xctest', '--macos']); + + expect(output, contains(contains('No tests found.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + group('iOS', () { test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); @@ -141,11 +291,10 @@ void main() { }); test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.federated - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.federated + }); final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); @@ -157,19 +306,14 @@ void main() { }); test('running with correct destination', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -192,13 +336,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -209,45 +352,43 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final Map schemeCommandResult = { - 'project': { - 'targets': ['bar_scheme', 'foo_scheme'] - } - }; processRunner.processToReturn = MockProcess.succeeding(); - // For simplicity of the test, we combine all the mock results into a single mock result, each internal command - // will get this result and they should still be able to parse them correctly. - processRunner.resultStdout = - jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + processRunner.resultStdout = jsonEncode(_kDeviceListMap); await runCapturingPrint(runner, ['xctest', '--ios']); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall( - 'xcrun', ['simctl', 'list', '--json'], null), + 'xcrun', + [ + 'simctl', + 'list', + 'devices', + 'runtimes', + 'available', + '--json', + ], + null), ProcessCall( 'xcrun', const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -257,15 +398,11 @@ void main() { }); test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.inline - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline + }); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess.failing() ]; @@ -288,7 +425,7 @@ void main() { expect( output, containsAllInOrder([ - contains('The following packages are failing XCTests:'), + contains('The following packages had errors:'), contains(' plugin'), ])); }); @@ -296,16 +433,10 @@ void main() { group('macOS', () { test('skip if macOS is not supported', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - ], - ); + createFakePlugin('plugin', packagesDir); - final List output = await runCapturingPrint(runner, - ['xctest', '--macos', _kDestination, 'foo_destination']); + final List output = + await runCapturingPrint(runner, ['xctest', '--macos']); expect( output, contains( @@ -314,14 +445,13 @@ void main() { }); test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.federated, - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.federated, + }); - final List output = await runCapturingPrint(runner, - ['xctest', '--macos', _kDestination, 'foo_destination']); + final List output = + await runCapturingPrint(runner, ['xctest', '--macos']); expect( output, contains( @@ -330,19 +460,15 @@ void main() { }); test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--macos', @@ -361,13 +487,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'macos/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), @@ -375,15 +500,11 @@ void main() { }); test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess.failing() ]; @@ -398,7 +519,7 @@ void main() { expect( output, containsAllInOrder([ - contains('The following packages are failing XCTests:'), + contains('The following packages had errors:'), contains(' plugin'), ]), ); @@ -407,20 +528,16 @@ void main() { group('combined', () { test('runs both iOS and macOS when supported', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -444,13 +561,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -461,13 +577,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'macos/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), @@ -475,19 +590,15 @@ void main() { }); test('runs only macOS for a macOS plugin', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -511,13 +622,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'macos/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), @@ -525,19 +635,14 @@ void main() { }); test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -561,13 +666,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -577,13 +681,8 @@ void main() { }); test('skips when neither are supported', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ]); + createFakePlugin('plugin', packagesDir); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', From 3c6df98154f42529b1f6c4a8199373dfc86a1434 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 21 Jul 2021 12:03:47 -0700 Subject: [PATCH 100/249] [flutter_plugin_tools] Make firebase-test-lab fail when no tests run (#4172) If a package supports Android, it will now report failure instead of skip if no tests run. This matches the new behavior of drive-examples, and is intended to prevent recurrance of situations where we are silently failing to run tests because of, e.g., tests being in the wrong directory. Also fixes a long-standing but unnoticed problem where if a run tried to run more than one package's tests, it would hang forever (although on the bots it doesn't seem to time out, just end logs abruptly) due to a logic error in the call to configure gcloud. Fixes flutter/flutter#86732 --- script/tool/CHANGELOG.md | 6 + .../lib/src/firebase_test_lab_command.dart | 36 ++-- .../test/firebase_test_lab_command_test.dart | 160 +++++++++++++++++- 3 files changed, 185 insertions(+), 17 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 377e7860bd2..d701278ee76 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -3,6 +3,12 @@ - Added an `xctest` flag to select specific test targets, to allow running only unit tests or integration tests. - Split Xcode analysis out of `xctest` and into a new `xcode-analyze` command. +- Fixed a bug that caused `firebase-test-lab` to hang if it tried to run more + than one plugin's tests in a single run. +- **Breaking change**: If `firebase-test-lab` is run on a package that supports + Android, but for which no tests are run, it now fails instead of skipping. + This matches `drive-examples`, as this command is what is used for driving + Android Flutter integration tests on CI. ## 0.4.1 diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 5e4d9f08008..30491282496 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -76,13 +76,12 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { static const String _gradleWrapper = 'gradlew'; - Completer? _firebaseProjectConfigured; + bool _firebaseProjectConfigured = false; Future _configureFirebaseProject() async { - if (_firebaseProjectConfigured != null) { - return _firebaseProjectConfigured!.future; + if (_firebaseProjectConfigured) { + return; } - _firebaseProjectConfigured = Completer(); final String serviceKey = getStringArg('service-key'); if (serviceKey.isEmpty) { @@ -110,31 +109,34 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { print(''); if (exitCode == 0) { print('Firebase project configured.'); - return; } else { logWarning( 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'); } } - _firebaseProjectConfigured!.complete(null); + _firebaseProjectConfigured = true; } @override Future runForPackage(Directory package) async { - if (!package - .childDirectory('example') - .childDirectory('android') + final Directory exampleDirectory = package.childDirectory('example'); + final Directory androidDirectory = + exampleDirectory.childDirectory('android'); + if (!androidDirectory.existsSync()) { + return PackageResult.skip( + '${getPackageDescription(exampleDirectory)} does not support Android.'); + } + + if (!androidDirectory .childDirectory('app') .childDirectory('src') .childDirectory('androidTest') .existsSync()) { - return PackageResult.skip('No example with androidTest directory'); + printError('No androidTest directory found.'); + return PackageResult.fail( + ['No tests ran (use --exclude if this is intentional).']); } - final Directory exampleDirectory = package.childDirectory('example'); - final Directory androidDirectory = - exampleDirectory.childDirectory('android'); - // Ensures that gradle wrapper exists if (!await _ensureGradleWrapperExists(androidDirectory)) { return PackageResult.fail(['Unable to build example apk']); @@ -191,6 +193,12 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { errors.add('$testName failed tests'); } } + + if (errors.isEmpty && resultsCounter == 0) { + printError('No integration tests were run.'); + errors.add('No tests ran (use --exclude if this is intentional).'); + } + return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index c265868bbf3..185b9d83f0f 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -84,6 +84,85 @@ void main() { ])); }); + test('only runs gcloud configuration once', () async { + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + createFakePlugin('plugin2', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'example/integration_test/bar_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('Firebase project configured.'), + contains('Testing example/integration_test/foo_test.dart...'), + contains('Running for plugin2'), + contains('Testing example/integration_test/bar_test.dart...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin1/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin1/example/android'), + ProcessCall( + '/packages/plugin1/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin1/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin1/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin1/example'), + ProcessCall( + '/packages/plugin2/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin2/example/android'), + ProcessCall( + '/packages/plugin2/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin2/example/integration_test/bar_test.dart' + .split(' '), + '/packages/plugin2/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin2/example'), + ]), + ); + }); + test('runs integration tests', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'test/plugin_test.dart', @@ -203,12 +282,87 @@ void main() { ); }); - test('skips packages with no androidTest directory', () async { + test('fails for packages with no androidTest directory', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', ]); + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No androidTest directory found.'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No tests ran (use --exclude if this is intentional).'), + ]), + ); + }); + + test('fails for packages with no integration test files', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No integration tests were run'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No tests ran (use --exclude if this is intentional).'), + ]), + ); + }); + + test('skips packages with no android directory', () async { + createFakePackage('package', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + ]); + final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', @@ -224,8 +378,8 @@ void main() { expect( output, containsAllInOrder([ - contains('Running for plugin'), - contains('No example with androidTest directory'), + contains('Running for package'), + contains('package/example does not support Android'), ]), ); expect(output, From 97178aff8578a6ed547ec70526ce7bd3d90fee68 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 22 Jul 2021 11:14:17 -0700 Subject: [PATCH 101/249] [flutter_plugin_tools] Replace xctest and java-test with native-test (#4176) Creates a new `native-test` command that will be used to run native unit and UI/integration tests for all platforms over time. This replaces both `xctest` and `java-test`. For CI we can continue to run each platform separately for clarity, but the combined command makes it easier to use (and remember how to use) for local development, as well as avoiding the need to introduce several new commands for desktop testing as support for that is added to the tool. Fixes https://github.com/flutter/flutter/issues/84392 Fixes https://github.com/flutter/flutter/issues/86489 --- script/tool/CHANGELOG.md | 10 +- script/tool/README.md | 24 +- .../src/common/package_looping_command.dart | 4 +- script/tool/lib/src/java_test_command.dart | 78 -- script/tool/lib/src/main.dart | 8 +- script/tool/lib/src/native_test_command.dart | 377 ++++++ script/tool/lib/src/xctest_command.dart | 211 ---- script/tool/test/java_test_command_test.dart | 187 --- .../tool/test/native_test_command_test.dart | 1071 +++++++++++++++++ script/tool/test/xctest_command_test.dart | 705 ----------- 10 files changed, 1481 insertions(+), 1194 deletions(-) delete mode 100644 script/tool/lib/src/java_test_command.dart create mode 100644 script/tool/lib/src/native_test_command.dart delete mode 100644 script/tool/lib/src/xctest_command.dart delete mode 100644 script/tool/test/java_test_command_test.dart create mode 100644 script/tool/test/native_test_command_test.dart delete mode 100644 script/tool/test/xctest_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index d701278ee76..dc30c05f79c 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,13 +2,21 @@ - Added an `xctest` flag to select specific test targets, to allow running only unit tests or integration tests. -- Split Xcode analysis out of `xctest` and into a new `xcode-analyze` command. +- **Breaking change**: Split Xcode analysis out of `xctest` and into a new + `xcode-analyze` command. - Fixed a bug that caused `firebase-test-lab` to hang if it tried to run more than one plugin's tests in a single run. - **Breaking change**: If `firebase-test-lab` is run on a package that supports Android, but for which no tests are run, it now fails instead of skipping. This matches `drive-examples`, as this command is what is used for driving Android Flutter integration tests on CI. +- **Breaking change**: Replaced `xctest` with a new `native-test` command that + will eventually be able to run native unit and integration tests for all + platforms. + - Adds the ability to disable test types via `--no-unit` or + `--no-integration`. +- **Breaking change**: Replaced `java-test` with Android unit test support for + the new `native-test` command. ## 0.4.1 diff --git a/script/tool/README.md b/script/tool/README.md index 5629dc50646..1a87f098757 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -75,14 +75,28 @@ cd dart run ./script/tool/bin/flutter_plugin_tools.dart test --packages plugin_name ``` -### Run XCTests +### Run Dart Integration Tests ```sh cd -# For iOS: -dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --ios --packages plugin_name -# For macOS: -dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --macos --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart build-examples --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --packages plugin_name +``` + +### Run Native Tests + +`native-test` takes one or more platform flags to run tests for. By default it +runs both unit tests and (on platforms that support it) integration tests, but +`--no-unit` or `--no-integration` can be used to run just one type. + +Examples: + +```sh +cd +# Run just unit tests for iOS and Android: +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android --no-integration --packages plugin_name +# Run all tests for macOS: +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages plugin_name ``` ### Publish a Release diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 9f4039ec707..0bcde6d296d 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -165,9 +165,9 @@ abstract class PackageLoopingCommand extends PluginCommand { final List components = p.posix.split(packageName); // For the common federated plugin pattern of `foo/foo_subpackage`, drop // the first part since it's not useful. - if (components.length == 2 && + if (components.length >= 2 && components[1].startsWith('${components[0]}_')) { - packageName = components[1]; + packageName = p.posix.joinAll(components.sublist(1)); } return packageName; } diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart deleted file mode 100644 index b36d1102f10..00000000000 --- a/script/tool/lib/src/java_test_command.dart +++ /dev/null @@ -1,78 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; - -/// A command to run the Java tests of Android plugins. -class JavaTestCommand extends PackageLoopingCommand { - /// Creates an instance of the test runner. - JavaTestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - static const String _gradleWrapper = 'gradlew'; - - @override - final String name = 'java-test'; - - @override - final String description = 'Runs the Java tests of the example apps.\n\n' - 'Building the apks of the example apps is required before executing this' - 'command.'; - - @override - Future runForPackage(Directory package) async { - final Iterable examplesWithTests = getExamplesForPlugin(package) - .where((Directory d) => - isFlutterPackage(d) && - (d - .childDirectory('android') - .childDirectory('app') - .childDirectory('src') - .childDirectory('test') - .existsSync() || - d.parent - .childDirectory('android') - .childDirectory('src') - .childDirectory('test') - .existsSync())); - - if (examplesWithTests.isEmpty) { - return PackageResult.skip('No Java unit tests.'); - } - - final List errors = []; - for (final Directory example in examplesWithTests) { - final String exampleName = getRelativePosixPath(example, from: package); - print('\nRUNNING JAVA TESTS for $exampleName'); - - final Directory androidDirectory = example.childDirectory('android'); - final File gradleFile = androidDirectory.childFile(_gradleWrapper); - if (!gradleFile.existsSync()) { - printError('ERROR: Run "flutter build apk" on $exampleName, or run ' - 'this tool\'s "build-examples --apk" command, ' - 'before executing tests.'); - errors.add('$exampleName has not been built.'); - continue; - } - - final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest', '--info'], - workingDir: androidDirectory); - if (exitCode != 0) { - errors.add('$exampleName tests failed.'); - } - } - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } -} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index ef1a18ab15b..6001c5df7f0 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -15,17 +15,16 @@ import 'create_all_plugins_app_command.dart'; import 'drive_examples_command.dart'; import 'firebase_test_lab_command.dart'; import 'format_command.dart'; -import 'java_test_command.dart'; import 'license_check_command.dart'; import 'lint_podspecs_command.dart'; import 'list_command.dart'; +import 'native_test_command.dart'; import 'publish_check_command.dart'; import 'publish_plugin_command.dart'; import 'pubspec_check_command.dart'; import 'test_command.dart'; import 'version_check_command.dart'; import 'xcode_analyze_command.dart'; -import 'xctest_command.dart'; void main(List args) { const FileSystem fileSystem = LocalFileSystem(); @@ -51,17 +50,16 @@ void main(List args) { ..addCommand(DriveExamplesCommand(packagesDir)) ..addCommand(FirebaseTestLabCommand(packagesDir)) ..addCommand(FormatCommand(packagesDir)) - ..addCommand(JavaTestCommand(packagesDir)) ..addCommand(LicenseCheckCommand(packagesDir)) ..addCommand(LintPodspecsCommand(packagesDir)) ..addCommand(ListCommand(packagesDir)) + ..addCommand(NativeTestCommand(packagesDir)) ..addCommand(PublishCheckCommand(packagesDir)) ..addCommand(PublishPluginCommand(packagesDir)) ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) - ..addCommand(XcodeAnalyzeCommand(packagesDir)) - ..addCommand(XCTestCommand(packagesDir)); + ..addCommand(XcodeAnalyzeCommand(packagesDir)); commandRunner.run(args).catchError((Object e) { final ToolExit toolExit = e as ToolExit; diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart new file mode 100644 index 00000000000..73a435d83e1 --- /dev/null +++ b/script/tool/lib/src/native_test_command.dart @@ -0,0 +1,377 @@ +// 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:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; +import 'common/xcode.dart'; + +const String _unitTestFlag = 'unit'; +const String _integrationTestFlag = 'integration'; + +const String _iosDestinationFlag = 'ios-destination'; + +const int _exitNoIosSimulators = 3; + +/// The command to run native tests for plugins: +/// - iOS and macOS: XCTests (XCUnitTest and XCUITest) in plugins. +class NativeTestCommand extends PackageLoopingCommand { + /// Creates an instance of the test command. + NativeTestCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : _xcode = Xcode(processRunner: processRunner, log: true), + super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addOption( + _iosDestinationFlag, + help: 'Specify the destination when running iOS tests.\n' + 'This is passed to the `-destination` argument in the xcodebuild command.\n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT ' + 'for details on how to specify the destination.', + ); + argParser.addFlag(kPlatformAndroid, help: 'Runs Android tests'); + argParser.addFlag(kPlatformIos, help: 'Runs iOS tests'); + argParser.addFlag(kPlatformMacos, help: 'Runs macOS tests'); + + // By default, both unit tests and integration tests are run, but provide + // flags to disable one or the other. + argParser.addFlag(_unitTestFlag, + help: 'Runs native unit tests', defaultsTo: true); + argParser.addFlag(_integrationTestFlag, + help: 'Runs native integration (UI) tests', defaultsTo: true); + } + + static const String _gradleWrapper = 'gradlew'; + + // The device destination flags for iOS tests. + List _iosDestinationFlags = []; + + final Xcode _xcode; + + @override + final String name = 'native-test'; + + @override + final String description = ''' +Runs native unit tests and native integration tests. + +Currently supported platforms: +- Android (unit tests only) +- iOS: requires 'xcrun' to be in your path. +- macOS: requires 'xcrun' to be in your path. + +The example app(s) must be built for all targeted platforms before running +this command. +'''; + + Map _platforms = {}; + + List _requestedPlatforms = []; + + @override + Future initializeRun() async { + _platforms = { + kPlatformAndroid: _PlatformDetails('Android', _testAndroid), + kPlatformIos: _PlatformDetails('iOS', _testIos), + kPlatformMacos: _PlatformDetails('macOS', _testMacOS), + }; + _requestedPlatforms = _platforms.keys + .where((String platform) => getBoolArg(platform)) + .toList(); + _requestedPlatforms.sort(); + + if (_requestedPlatforms.isEmpty) { + printError('At least one platform flag must be provided.'); + throw ToolExit(exitInvalidArguments); + } + + if (!(getBoolArg(_unitTestFlag) || getBoolArg(_integrationTestFlag))) { + printError('At least one test type must be enabled.'); + throw ToolExit(exitInvalidArguments); + } + + if (getBoolArg(kPlatformAndroid) && getBoolArg(_integrationTestFlag)) { + logWarning('This command currently only supports unit tests for Android. ' + 'See https://github.com/flutter/flutter/issues/86490.'); + } + + // iOS-specific run-level state. + if (_requestedPlatforms.contains('ios')) { + String destination = getStringArg(_iosDestinationFlag); + if (destination.isEmpty) { + final String? simulatorId = + await _xcode.findBestAvailableIphoneSimulator(); + if (simulatorId == null) { + printError('Cannot find any available iOS simulators.'); + throw ToolExit(_exitNoIosSimulators); + } + destination = 'id=$simulatorId'; + } + _iosDestinationFlags = [ + '-destination', + destination, + ]; + } + } + + @override + Future runForPackage(Directory package) async { + final List testPlatforms = []; + for (final String platform in _requestedPlatforms) { + if (pluginSupportsPlatform(platform, package, + requiredMode: PlatformSupport.inline)) { + testPlatforms.add(platform); + } else { + print('No implementation for ${_platforms[platform]!.label}.'); + } + } + + if (testPlatforms.isEmpty) { + return PackageResult.skip('Not implemented for target platform(s).'); + } + + final _TestMode mode = _TestMode( + unit: getBoolArg(_unitTestFlag), + integration: getBoolArg(_integrationTestFlag), + ); + + bool ranTests = false; + bool failed = false; + final List failureMessages = []; + for (final String platform in testPlatforms) { + final _PlatformDetails platformInfo = _platforms[platform]!; + print('Running tests for ${platformInfo.label}...'); + print('----------------------------------------'); + final _PlatformResult result = + await platformInfo.testFunction(package, mode); + ranTests |= result.state != RunState.skipped; + if (result.state == RunState.failed) { + failed = true; + + final String? error = result.error; + // Only provide the failing platforms in the failure details if testing + // multiple platforms, otherwise it's just noise. + if (_requestedPlatforms.length > 1) { + failureMessages.add(error != null + ? '${platformInfo.label}: $error' + : platformInfo.label); + } else if (error != null) { + // If there's only one platform, only provide error details in the + // summary if the platform returned a message. + failureMessages.add(error); + } + } + } + + if (!ranTests) { + return PackageResult.skip('No tests found.'); + } + return failed + ? PackageResult.fail(failureMessages) + : PackageResult.success(); + } + + Future<_PlatformResult> _testAndroid(Directory plugin, _TestMode mode) async { + final List examplesWithTests = []; + for (final Directory example in getExamplesForPlugin(plugin)) { + if (!isFlutterPackage(example)) { + continue; + } + if (example + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('test') + .existsSync() || + example.parent + .childDirectory('android') + .childDirectory('src') + .childDirectory('test') + .existsSync()) { + examplesWithTests.add(example); + } else { + _printNoExampleTestsMessage(example, 'Android'); + } + } + + if (examplesWithTests.isEmpty) { + return _PlatformResult(RunState.skipped); + } + + bool failed = false; + bool hasMissingBuild = false; + for (final Directory example in examplesWithTests) { + final String exampleName = getPackageDescription(example); + _printRunningExampleTestsMessage(example, 'Android'); + + final Directory androidDirectory = example.childDirectory('android'); + final File gradleFile = androidDirectory.childFile(_gradleWrapper); + if (!gradleFile.existsSync()) { + printError('ERROR: Run "flutter build apk" on $exampleName, or run ' + 'this tool\'s "build-examples --apk" command, ' + 'before executing tests.'); + failed = true; + hasMissingBuild = true; + continue; + } + + final int exitCode = await processRunner.runAndStream( + gradleFile.path, ['testDebugUnitTest', '--info'], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName tests failed.'); + failed = true; + } + } + return _PlatformResult(failed ? RunState.failed : RunState.succeeded, + error: + hasMissingBuild ? 'Examples must be built before testing.' : null); + } + + Future<_PlatformResult> _testIos(Directory plugin, _TestMode mode) { + return _runXcodeTests(plugin, 'iOS', mode, + extraFlags: _iosDestinationFlags); + } + + Future<_PlatformResult> _testMacOS(Directory plugin, _TestMode mode) { + return _runXcodeTests(plugin, 'macOS', mode); + } + + /// Runs all applicable tests for [plugin], printing status and returning + /// the test result. + /// + /// The tests targets must be added to the Xcode project of the example app, + /// usually at "example/{ios,macos}/Runner.xcworkspace". + Future<_PlatformResult> _runXcodeTests( + Directory plugin, + String platform, + _TestMode mode, { + List extraFlags = const [], + }) async { + String? testTarget; + if (mode.unitOnly) { + testTarget = 'RunnerTests'; + } else if (mode.integrationOnly) { + testTarget = 'RunnerUITests'; + } + + // Assume skipped until at least one test has run. + RunState overallResult = RunState.skipped; + for (final Directory example in getExamplesForPlugin(plugin)) { + final String exampleName = getPackageDescription(example); + + if (testTarget != null) { + final Directory project = example + .childDirectory(platform.toLowerCase()) + .childDirectory('Runner.xcodeproj'); + final bool? hasTarget = + await _xcode.projectHasTarget(project, testTarget); + if (hasTarget == null) { + printError('Unable to check targets for $exampleName.'); + overallResult = RunState.failed; + continue; + } else if (!hasTarget) { + print('No "$testTarget" target in $exampleName; skipping.'); + continue; + } + } + + _printRunningExampleTestsMessage(example, platform); + final int exitCode = await _xcode.runXcodeBuild( + example, + actions: ['test'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + if (testTarget != null) '-only-testing:$testTarget', + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + ); + + // The exit code from 'xcodebuild test' when there are no tests. + const int _xcodebuildNoTestExitCode = 66; + switch (exitCode) { + case _xcodebuildNoTestExitCode: + _printNoExampleTestsMessage(example, platform); + continue; + case 0: + printSuccess('Successfully ran $platform xctest for $exampleName'); + // If this is the first test, assume success until something fails. + if (overallResult == RunState.skipped) { + overallResult = RunState.succeeded; + } + break; + default: + // Any failure means a failure overall. + overallResult = RunState.failed; + break; + } + } + return _PlatformResult(overallResult); + } + + /// Prints a standard format message indicating that [platform] tests for + /// [plugin]'s [example] are about to be run. + void _printRunningExampleTestsMessage(Directory example, String platform) { + print('Running $platform tests for ${getPackageDescription(example)}...'); + } + + /// Prints a standard format message indicating that no tests were found for + /// [plugin]'s [example] for [platform]. + void _printNoExampleTestsMessage(Directory example, String platform) { + print('No $platform tests found for ${getPackageDescription(example)}'); + } +} + +// The type for a function that takes a plugin directory and runs its native +// tests for a specific platform. +typedef _TestFunction = Future<_PlatformResult> Function(Directory, _TestMode); + +/// A collection of information related to a specific platform. +class _PlatformDetails { + const _PlatformDetails( + this.label, + this.testFunction, + ); + + /// The name to use in output. + final String label; + + /// The function to call to run tests. + final _TestFunction testFunction; +} + +/// Enabled state for different test types. +class _TestMode { + const _TestMode({required this.unit, required this.integration}); + + final bool unit; + final bool integration; + + bool get integrationOnly => integration && !unit; + bool get unitOnly => unit && !integration; +} + +/// The result of running a single platform's tests. +class _PlatformResult { + _PlatformResult(this.state, {this.error}); + + /// The overall state of the platform's tests. This should be: + /// - failed if any tests failed. + /// - succeeded if at least one test ran, and all tests passed. + /// - skipped if no tests ran. + final RunState state; + + /// An optional error string to include in the summary for this platform. + /// + /// Ignored unless [state] is `failed`. + final String? error; +} diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart deleted file mode 100644 index 44fc3a87d54..00000000000 --- a/script/tool/lib/src/xctest_command.dart +++ /dev/null @@ -1,211 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/xcode.dart'; - -const String _iosDestinationFlag = 'ios-destination'; -const String _testTargetFlag = 'test-target'; - -// The exit code from 'xcodebuild test' when there are no tests. -const int _xcodebuildNoTestExitCode = 66; - -const int _exitNoSimulators = 3; - -/// The command to run XCTests (XCUnitTest and XCUITest) in plugins. -/// The tests target have to be added to the Xcode project of the example app, -/// usually at "example/{ios,macos}/Runner.xcworkspace". -class XCTestCommand extends PackageLoopingCommand { - /// Creates an instance of the test command. - XCTestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : _xcode = Xcode(processRunner: processRunner, log: true), - super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addOption( - _iosDestinationFlag, - help: - 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' - 'this is passed to the `-destination` argument in xcodebuild command.\n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', - ); - argParser.addOption( - _testTargetFlag, - help: - 'Limits the tests to a specific target (e.g., RunnerTests or RunnerUITests)', - ); - argParser.addFlag(kPlatformIos, help: 'Runs the iOS tests'); - argParser.addFlag(kPlatformMacos, help: 'Runs the macOS tests'); - } - - // The device destination flags for iOS tests. - List _iosDestinationFlags = []; - - final Xcode _xcode; - - @override - final String name = 'xctest'; - - @override - final String description = - 'Runs the xctests in the iOS and/or macOS example apps.\n\n' - 'This command requires "flutter" and "xcrun" to be in your path.'; - - @override - Future initializeRun() async { - final bool shouldTestIos = getBoolArg(kPlatformIos); - final bool shouldTestMacos = getBoolArg(kPlatformMacos); - - if (!(shouldTestIos || shouldTestMacos)) { - printError('At least one platform flag must be provided.'); - throw ToolExit(exitInvalidArguments); - } - - if (shouldTestIos) { - String destination = getStringArg(_iosDestinationFlag); - if (destination.isEmpty) { - final String? simulatorId = - await _xcode.findBestAvailableIphoneSimulator(); - if (simulatorId == null) { - printError('Cannot find any available simulators, tests failed'); - throw ToolExit(_exitNoSimulators); - } - destination = 'id=$simulatorId'; - } - _iosDestinationFlags = [ - '-destination', - destination, - ]; - } - } - - @override - Future runForPackage(Directory package) async { - final bool testIos = getBoolArg(kPlatformIos) && - pluginSupportsPlatform(kPlatformIos, package, - requiredMode: PlatformSupport.inline); - final bool testMacos = getBoolArg(kPlatformMacos) && - pluginSupportsPlatform(kPlatformMacos, package, - requiredMode: PlatformSupport.inline); - - final bool multiplePlatformsRequested = - getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos); - if (!(testIos || testMacos)) { - String description; - if (multiplePlatformsRequested) { - description = 'Neither iOS nor macOS is'; - } else if (getBoolArg(kPlatformIos)) { - description = 'iOS is not'; - } else { - description = 'macOS is not'; - } - return PackageResult.skip( - '$description implemented by this plugin package.'); - } - - if (multiplePlatformsRequested && (!testIos || !testMacos)) { - print('Only running for ${testIos ? 'iOS' : 'macOS'}\n'); - } - - final List failures = []; - bool ranTests = false; - if (testIos) { - final RunState result = await _testPlugin(package, 'iOS', - extraXcrunFlags: _iosDestinationFlags); - ranTests |= result != RunState.skipped; - if (result == RunState.failed) { - failures.add('iOS'); - } - } - if (testMacos) { - final RunState result = await _testPlugin(package, 'macOS'); - ranTests |= result != RunState.skipped; - if (result == RunState.failed) { - failures.add('macOS'); - } - } - - if (!ranTests) { - return PackageResult.skip('No tests found.'); - } - // Only provide the failing platform in the failure details if testing - // multiple platforms, otherwise it's just noise. - return failures.isEmpty - ? PackageResult.success() - : PackageResult.fail( - multiplePlatformsRequested ? failures : []); - } - - /// Runs all applicable tests for [plugin], printing status and returning - /// the test result. - Future _testPlugin( - Directory plugin, - String platform, { - List extraXcrunFlags = const [], - }) async { - final String testTarget = getStringArg(_testTargetFlag); - - // Assume skipped until at least one test has run. - RunState overallResult = RunState.skipped; - for (final Directory example in getExamplesForPlugin(plugin)) { - final String examplePath = - getRelativePosixPath(example, from: plugin.parent); - - if (testTarget.isNotEmpty) { - final Directory project = example - .childDirectory(platform.toLowerCase()) - .childDirectory('Runner.xcodeproj'); - final bool? hasTarget = - await _xcode.projectHasTarget(project, testTarget); - if (hasTarget == null) { - printError('Unable to check targets for $examplePath.'); - overallResult = RunState.failed; - continue; - } else if (!hasTarget) { - print('No "$testTarget" target in $examplePath; skipping.'); - continue; - } - } - - print('Running $platform tests for $examplePath...'); - final int exitCode = await _xcode.runXcodeBuild( - example, - actions: ['test'], - workspace: '${platform.toLowerCase()}/Runner.xcworkspace', - scheme: 'Runner', - configuration: 'Debug', - extraFlags: [ - if (testTarget.isNotEmpty) '-only-testing:$testTarget', - ...extraXcrunFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - ); - - switch (exitCode) { - case _xcodebuildNoTestExitCode: - print('No tests found for $examplePath'); - continue; - case 0: - printSuccess('Successfully ran $platform xctest for $examplePath'); - // If this is the first test, assume success until something fails. - if (overallResult == RunState.skipped) { - overallResult = RunState.succeeded; - } - break; - default: - // Any failure means a failure overall. - overallResult = RunState.failed; - break; - } - } - return overallResult; - } -} diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart deleted file mode 100644 index 13e0e7fc0f4..00000000000 --- a/script/tool/test/java_test_command_test.dart +++ /dev/null @@ -1,187 +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: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/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/java_test_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$JavaTestCommand', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final JavaTestCommand command = JavaTestCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = - CommandRunner('java_test_test', 'Test for $JavaTestCommand'); - runner.addCommand(command); - }); - - test('Should run Java tests in Android implementation folder', () async { - final Directory plugin = createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - extraFiles: [ - 'example/android/gradlew', - 'android/src/test/example_test.java', - ], - ); - - await runCapturingPrint(runner, ['java-test']); - - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], - androidFolder.path, - ), - ]), - ); - }); - - test('Should run Java tests in example folder', () async { - final Directory plugin = createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - await runCapturingPrint(runner, ['java-test']); - - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], - androidFolder.path, - ), - ]), - ); - }); - - test('fails when the app needs to be built', () async { - createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - extraFiles: [ - 'example/android/app/src/test/example_test.java', - ], - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['java-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('ERROR: Run "flutter build apk" on example'), - contains('plugin1:\n' - ' example has not been built.') - ]), - ); - }); - - test('fails when a test fails', () async { - final Directory pluginDir = createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['java-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('plugin1:\n' - ' example tests failed.') - ]), - ); - }); - - test('Skips when running no tests', () async { - createFakePlugin( - 'plugin1', - packagesDir, - ); - - final List output = - await runCapturingPrint(runner, ['java-test']); - - expect( - output, - containsAllInOrder( - [contains('SKIPPING: No Java unit tests.')]), - ); - }); - }); -} diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart new file mode 100644 index 00000000000..ca28a6cff0e --- /dev/null +++ b/script/tool/test/native_test_command_test.dart @@ -0,0 +1,1071 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.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/native_test_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +final Map _kDeviceListMap = { + 'runtimes': >[ + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', + 'buildversion': '17L255', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', + 'version': '13.4', + 'isAvailable': true, + 'name': 'iOS 13.4' + }, + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', + 'state': 'Shutdown', + 'name': 'iPhone 8 Plus' + } + ] + } +}; + +// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of +// doing all the process mocking and validation. +void main() { + const String _kDestination = '--ios-destination'; + + group('test native_test_command', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final NativeTestCommand command = NativeTestCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'native_test_command', 'Test for native_test_command'); + runner.addCommand(command); + }); + + test('fails if no platforms are provided', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one platform flag must be provided.'), + ]), + ); + }); + + test('fails if all test types are disabled', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-unit', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one test type must be enabled.'), + ]), + ); + }); + + test('reports skips with no tests', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + // Exit code 66 from testing indicates no tests. + final MockProcess noTestsProcessResult = MockProcess(); + noTestsProcessResult.exitCodeCompleter.complete(66); + processRunner.mockProcessesForExecutable['xcrun'] = [ + noTestsProcessResult, + ]; + final List output = + await runCapturingPrint(runner, ['native-test', '--macos']); + + expect(output, contains(contains('No tests found.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + group('iOS', () { + test('skip if iOS is not supported', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final List output = await runCapturingPrint(runner, + ['native-test', '--ios', _kDestination, 'foo_destination']); + expect( + output, + containsAllInOrder([ + contains('No implementation for iOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if iOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.federated + }); + + final List output = await runCapturingPrint(runner, + ['native-test', '--ios', _kDestination, 'foo_destination']); + expect( + output, + containsAllInOrder([ + contains('No implementation for iOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('running with correct destination', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--ios', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Successfully ran iOS xctest for plugin/example') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = jsonEncode(_kDeviceListMap); + await runCapturingPrint(runner, ['native-test', '--ios']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'xcrun', + [ + 'simctl', + 'list', + 'devices', + 'runtimes', + 'available', + '--json', + ], + null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + }); + + group('macOS', () { + test('skip if macOS is not supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = + await runCapturingPrint(runner, ['native-test', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('No implementation for macOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if macOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.federated, + }); + + final List output = + await runCapturingPrint(runner, ['native-test', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('No implementation for macOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + }); + + group('Android', () { + test('runs Java tests in Android implementation folder', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'android/src/test/example_test.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest', '--info'], + androidFolder.path, + ), + ]), + ); + }); + + test('runs Java tests in example folder', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest', '--info'], + androidFolder.path, + ), + ]), + ); + }); + + test('fails when the app needs to be built', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/app/src/test/example_test.java', + ], + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('ERROR: Run "flutter build apk" on plugin/example'), + contains('plugin:\n' + ' Examples must be built before testing.') + ]), + ); + }); + + test('fails when a test fails', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('plugin/example tests failed.'), + contains('The following packages had errors:'), + contains('plugin') + ]), + ); + }); + + test('skips if Android is not supported', () async { + createFakePlugin( + 'plugin', + packagesDir, + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android']); + + expect( + output, + containsAllInOrder([ + contains('No implementation for Android.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ]), + ); + }); + + test('skips when running no tests', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android']); + + expect( + output, + containsAllInOrder([ + contains('No Android tests found for plugin/example'), + contains('SKIPPING: No tests found.'), + ]), + ); + }); + }); + + // Tests behaviors of implementation that is shared between iOS and macOS. + group('iOS/macOS', () { + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['native-test', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ]), + ); + }); + + test('honors unit-only', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = + '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-integration', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + // --no-integration should translate to '-only-testing:RunnerTests'. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-only-testing:RunnerTests', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('honors integration-only', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = + '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-unit', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + // --no-unit should translate to '-only-testing:RunnerUITests'. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-only-testing:RunnerUITests', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when the requested target is not present', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + // Simulate a project with unit tests but no integration tests... + processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; + // ... then try to run only integration tests. + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-unit', + ]); + + expect( + output, + containsAllInOrder([ + contains( + 'No "RunnerUITests" target in plugin/example; skipping.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + + test('fails if unable to check for requested target', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.failing(); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to check targets for plugin/example.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + }); + + group('multiplatform', () { + test('runs all platfroms when supported', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/android/gradlew', + 'android/src/test/example_test.java', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + final Directory androidFolder = + pluginExampleDirectory.childDirectory('android'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAll([ + contains('Running Android tests for plugin/example'), + contains('Successfully ran iOS xctest for plugin/example'), + contains('Successfully ran macOS xctest for plugin/example'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest', '--info'], + androidFolder.path), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only macOS for a macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No implementation for iOS.'), + contains('Successfully ran macOS xctest for plugin/example'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only iOS for a iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No implementation for macOS.'), + contains('Successfully ran iOS xctest for plugin/example') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when nothing is supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No implementation for Android.'), + contains('No implementation for iOS.'), + contains('No implementation for macOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('failing one platform does not stop the tests', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + // Simulate failing Android, but not iOS. + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--ios-destination', + 'foo_destination', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('Running tests for Android...'), + contains('plugin/example tests failed.'), + contains('Running tests for iOS...'), + contains('Successfully ran iOS xctest for plugin/example'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' Android') + ]), + ); + }); + + test('failing multiple platforms reports multiple failures', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + // Simulate failing Android. + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess.failing() + ]; + // Simulate failing Android. + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--ios-destination', + 'foo_destination', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('Running tests for Android...'), + contains('Running tests for iOS...'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' Android\n' + ' iOS') + ]), + ); + }); + }); + }); +} diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart deleted file mode 100644 index 324dea0e71e..00000000000 --- a/script/tool/test/xctest_command_test.dart +++ /dev/null @@ -1,705 +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:convert'; -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/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'; - -import 'mocks.dart'; -import 'util.dart'; - -final Map _kDeviceListMap = { - 'runtimes': >[ - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', - 'buildversion': '17L255', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', - 'version': '13.4', - 'isAvailable': true, - 'name': 'iOS 13.4' - }, - ], - 'devices': { - 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', - 'state': 'Shutdown', - 'name': 'iPhone 8 Plus' - } - ] - } -}; - -// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of -// doing all the process mocking and validation. -void main() { - const String _kDestination = '--ios-destination'; - - group('test xctest_command', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(isMacOS: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final XCTestCommand command = XCTestCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner('xctest_command', 'Test for xctest_command'); - runner.addCommand(command); - }); - - test('Fails if no platforms are provided', () async { - Error? commandError; - final List output = await runCapturingPrint( - runner, ['xctest'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one platform flag must be provided'), - ]), - ); - }); - - test('allows target filtering', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--macos', - '--test-target=RunnerTests', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-only-testing:RunnerTests', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('skips when the requested target is not present', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = '{"project":{"targets":["Runner"]}}'; - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--macos', - '--test-target=RunnerTests', - ]); - - expect( - output, - containsAllInOrder([ - contains('No "RunnerTests" target in plugin/example; skipping.'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ])); - }); - - test('fails if unable to check for requested target', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - processRunner.processToReturn = MockProcess.failing(); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--macos', - '--test-target=RunnerTests', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to check targets for plugin/example.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ])); - }); - - test('reports skips with no tests', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - // Exit code 66 from testing indicates no tests. - final MockProcess noTestsProcessResult = MockProcess(); - noTestsProcessResult.exitCodeCompleter.complete(66); - processRunner.mockProcessesForExecutable['xcrun'] = [ - noTestsProcessResult, - ]; - final List output = - await runCapturingPrint(runner, ['xctest', '--macos']); - - expect(output, contains(contains('No tests found.'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - group('iOS', () { - test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final List output = await runCapturingPrint(runner, - ['xctest', '--ios', _kDestination, 'foo_destination']); - expect( - output, - contains( - contains('iOS is not implemented by this plugin package.'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.federated - }); - - final List output = await runCapturingPrint(runner, - ['xctest', '--ios', _kDestination, 'foo_destination']); - expect( - output, - contains( - contains('iOS is not implemented by this plugin package.'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('running with correct destination', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Successfully ran iOS xctest for plugin/example') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('Not specifying --ios-destination assigns an available simulator', - () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(_kDeviceListMap); - await runCapturingPrint(runner, ['xctest', '--ios']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall( - 'xcrun', - [ - 'simctl', - 'list', - 'devices', - 'runtimes', - 'available', - '--json', - ], - null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'xctest', - '--ios', - _kDestination, - 'foo_destination', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ])); - }); - }); - - group('macOS', () { - test('skip if macOS is not supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['xctest', '--macos']); - expect( - output, - contains( - contains('macOS is not implemented by this plugin package.'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.federated, - }); - - final List output = - await runCapturingPrint(runner, ['xctest', '--macos']); - expect( - output, - contains( - contains('macOS is not implemented by this plugin package.'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--macos', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['xctest', '--macos'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ]), - ); - }); - }); - - group('combined', () { - test('runs both iOS and macOS when supported', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - '--macos', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAll([ - contains('Successfully ran iOS xctest for plugin/example'), - contains('Successfully ran macOS xctest for plugin/example'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('runs only macOS for a macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - '--macos', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('Only running for macOS'), - contains('Successfully ran macOS xctest for plugin/example'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - '--macos', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('Only running for iOS'), - contains('Successfully ran iOS xctest for plugin/example') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('skips when neither are supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - '--macos', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains( - 'SKIPPING: Neither iOS nor macOS is implemented by this plugin package.'), - ])); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - }); - }); -} From d17489c62fed52e07381cb1762cd23e62dec93d5 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 22 Jul 2021 14:26:44 -0700 Subject: [PATCH 102/249] [flutter_plugin_tools] Support YAML exception lists (#4183) Currently the tool accepts `--custom-analysis` to allow a list of packages for which custom `analysis_options.yaml` are allowed, and `--exclude` to exclude a set of packages when running a command against all, or all changed, packages. This results in these exception lists being embedded into CI configuration files (e.g., .cirrus.yaml) or scripts, which makes them harder to maintain, and harder to re-use in other contexts (local runs, new CI systems). This adds support for both flags to accept paths to YAML files that contain the lists, so that they can be maintained separately, and with inline comments about the reasons things are on the lists. Also updates the CI to use this new support, eliminating those lists from `.cirrus.yaml` and `tool_runner.sh` Fixes https://github.com/flutter/flutter/issues/86799 --- script/tool/CHANGELOG.md | 4 ++++ script/tool/lib/src/analyze_command.dart | 21 +++++++++++++++++-- .../tool/lib/src/common/plugin_command.dart | 17 +++++++++++++-- script/tool/test/analyze_command_test.dart | 19 +++++++++++++++++ .../tool/test/common/plugin_command_test.dart | 13 ++++++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index dc30c05f79c..7d1eac01b76 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,9 @@ ## NEXT +- `--exclude` and `--custom-analysis` now accept paths to YAML files that + contain lists of packages to exclude, in addition to just package names, + so that exclude lists can be maintained separately from scripts and CI + configuration. - Added an `xctest` flag to select specific test targets, to allow running only unit tests or integration tests. - **Breaking change**: Split Xcode analysis out of `xctest` and into a new diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index e56b95d88eb..4fd15f027f5 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -23,7 +24,10 @@ class AnalyzeCommand extends PackageLoopingCommand { }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addMultiOption(_customAnalysisFlag, help: - 'Directories (comma separated) that are allowed to have their own analysis options.', + 'Directories (comma separated) that are allowed to have their own ' + 'analysis options.\n\n' + 'Alternately, a list of one or more YAML files that contain a list ' + 'of allowed directories.', defaultsTo: []); argParser.addOption(_analysisSdk, valueHelp: 'dart-sdk', @@ -37,6 +41,8 @@ class AnalyzeCommand extends PackageLoopingCommand { late String _dartBinaryPath; + Set _allowedCustomAnalysisDirectories = const {}; + @override final String name = 'analyze'; @@ -56,7 +62,7 @@ class AnalyzeCommand extends PackageLoopingCommand { continue; } - final bool allowed = (getStringListArg(_customAnalysisFlag)).any( + final bool allowed = _allowedCustomAnalysisDirectories.any( (String directory) => directory.isNotEmpty && path.isWithin( @@ -107,6 +113,17 @@ class AnalyzeCommand extends PackageLoopingCommand { throw ToolExit(_exitPackagesGetFailed); } + _allowedCustomAnalysisDirectories = + getStringListArg(_customAnalysisFlag).expand((String item) { + if (item.endsWith('.yaml')) { + final File file = packagesDir.fileSystem.file(item); + return (loadYaml(file.readAsStringSync()) as YamlList) + .toList() + .cast(); + } + return [item]; + }).toSet(); + // Use the Dart SDK override if one was passed in. final String? dartSdk = argResults![_analysisSdk] as String?; _dartBinaryPath = diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index ecdcb0565d3..7781eee0d96 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -9,6 +9,7 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; import 'core.dart'; import 'git_version_finder.dart'; @@ -48,7 +49,9 @@ abstract class PluginCommand extends Command { argParser.addMultiOption( _excludeArg, abbr: 'e', - help: 'Exclude packages from this command.', + help: 'A list of packages to exclude from from this command.\n\n' + 'Alternately, a list of one or more YAML files that contain a list ' + 'of packages to exclude.', defaultsTo: [], ); argParser.addFlag(_runOnChangedPackagesArg, @@ -214,8 +217,18 @@ abstract class PluginCommand extends Command { /// of packages in the flutter/packages repository. Stream _getAllPlugins() async* { Set plugins = Set.from(getStringListArg(_packagesArg)); + final Set excludedPlugins = - Set.from(getStringListArg(_excludeArg)); + getStringListArg(_excludeArg).expand((String item) { + if (item.endsWith('.yaml')) { + final File file = packagesDir.fileSystem.file(item); + return (loadYaml(file.readAsStringSync()) as YamlList) + .toList() + .cast(); + } + return [item]; + }).toSet(); + final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); if (plugins.isEmpty && runOnChangedPackages && diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 69a2c4f9552..9dc8b6a3fca 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -176,6 +176,25 @@ void main() { ])); }); + test('takes an allow config file', () async { + final Directory pluginDir = createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); + final File allowFile = packagesDir.childFile('custom.yaml'); + allowFile.writeAsStringSync('- foo'); + + await runCapturingPrint( + runner, ['analyze', '--custom-analysis', allowFile.path]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], pluginDir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + pluginDir.path), + ])); + }); + // See: https://github.com/flutter/flutter/issues/78994 test('takes an empty allow list', () async { createFakePlugin('foo', packagesDir, diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index fdab9612be3..7f67acfb2df 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -172,6 +172,19 @@ void main() { expect(plugins, unorderedEquals([plugin2.path])); }); + test('exclude accepts config files', () async { + createFakePlugin('plugin1', packagesDir); + final File configFile = packagesDir.childFile('exclude.yaml'); + configFile.writeAsStringSync('- plugin1'); + + await runCapturingPrint(runner, [ + 'sample', + '--packages=plugin1', + '--exclude=${configFile.path}' + ]); + expect(plugins, unorderedEquals([])); + }); + group('test run-on-changed-packages', () { test('all plugins should be tested if there are no changes.', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); From e5bed35b8c7cf45cd5660a6d4165123bdb3dd280 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Sat, 24 Jul 2021 16:06:34 -0700 Subject: [PATCH 103/249] Make java-test output more useful (#4184) --- script/tool/lib/src/native_test_command.dart | 2 +- script/tool/test/native_test_command_test.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 73a435d83e1..36b12741f2c 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -222,7 +222,7 @@ this command. } final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest', '--info'], + gradleFile.path, ['testDebugUnitTest'], workingDir: androidDirectory); if (exitCode != 0) { printError('$exampleName tests failed.'); diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index ca28a6cff0e..e656e2f2372 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -376,7 +376,7 @@ void main() { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], + const ['testDebugUnitTest'], androidFolder.path, ), ]), @@ -406,7 +406,7 @@ void main() { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], + const ['testDebugUnitTest'], androidFolder.path, ), ]), @@ -812,7 +812,7 @@ void main() { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], + const ['testDebugUnitTest'], androidFolder.path), ProcessCall( 'xcrun', From 2e42b205b17c49f89a1d1e6907e45a45a6b30204 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 26 Jul 2021 15:04:18 -0700 Subject: [PATCH 104/249] [flutter_plugin_tools] Test and comment Dart analysis (#4194) Adds a unit test and comments intended to avoid accidental breakage of the Dart repo's run of analysis against this repository. Addresses https://github.com/flutter/plugins/pull/4183#issuecomment-885767597 --- script/tool/test/analyze_command_test.dart | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 9dc8b6a3fca..da2f0aba86c 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -251,4 +251,43 @@ void main() { ]), ); }); + + // Ensure that the command used to analyze flutter/plugins in the Dart repo: + // https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh + // continues to work. + // + // DO NOT remove or modify this test without a coordination plan in place to + // modify the script above, as it is run from source, but out-of-repo. + // Contact stuartmorgan or devoncarew for assistance. + test('Dart repo analyze command works', () async { + final Directory pluginDir = createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); + final File allowFile = packagesDir.childFile('custom.yaml'); + allowFile.writeAsStringSync('- foo'); + + await runCapturingPrint(runner, [ + // DO NOT change this call; see comment above. + 'analyze', + '--analysis-sdk', + 'foo/bar/baz', + '--custom-analysis', + allowFile.path + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['packages', 'get'], + pluginDir.path, + ), + ProcessCall( + 'foo/bar/baz/bin/dart', + const ['analyze', '--fatal-infos'], + pluginDir.path, + ), + ]), + ); + }); } From a063a21346068cce6094a79513a55ac130b7b2c0 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 26 Jul 2021 22:41:14 -0700 Subject: [PATCH 105/249] Skip an integration test and extend firebase testlab timeout (#4195) --- script/tool/lib/src/firebase_test_lab_command.dart | 2 +- script/tool/test/firebase_test_lab_command_test.dart | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 30491282496..8459f6c7015 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -178,7 +178,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { '--test', 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', '--timeout', - '5m', + '7m', '--results-bucket=${getStringArg('results-bucket')}', '--results-dir=$resultsDir', ]; diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 185b9d83f0f..35697af3f5f 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -142,7 +142,7 @@ void main() { '/packages/plugin1/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin1/example'), ProcessCall( @@ -156,7 +156,7 @@ void main() { '/packages/plugin2/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin2/example'), ]), @@ -219,7 +219,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -229,7 +229,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -445,7 +445,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -601,7 +601,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' .split(' '), '/packages/plugin/example'), ]), From 1ee7bef5133ba0b09a5286e94fe32aa3e698ef5e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 3 Aug 2021 16:24:46 -0700 Subject: [PATCH 106/249] [flutter_plugin_tools] Track and log exclusions (#4205) Makes commands that use the package-looping base command track and report exclusions. This will make it much easier to debug/audit situations where tests aren't running when expected (e.g., when enabling a new type of test for a package that previously had to be explicitly excluded from that test to avoid failing for having no tests, but forgetting to remove the package from the exclusion list). Also fixes a latent issue with using different exclusion lists on different commands in a single CI task when using sharding could cause unexpected failures due to different sets of plugins being included for each step (e.g., build+drive with an exclude list on drive could potentially try to drive a plugin that hadn't been built in that shard) by sharding before filtering out excluded packages. Adds testing for sharding in general, as there was previously none. --- script/tool/CHANGELOG.md | 2 + script/tool/lib/src/analyze_command.dart | 6 +- .../src/common/package_looping_command.dart | 90 +++++-- .../tool/lib/src/common/plugin_command.dart | 147 +++++++---- .../src/create_all_plugins_app_command.dart | 9 +- script/tool/lib/src/list_command.dart | 15 +- .../common/package_looping_command_test.dart | 76 ++++++ .../tool/test/common/plugin_command_test.dart | 239 +++++++++++++++--- 8 files changed, 460 insertions(+), 124 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7d1eac01b76..7f326ff3c8f 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -21,6 +21,8 @@ `--no-integration`. - **Breaking change**: Replaced `java-test` with Android unit test support for the new `native-test` command. +- Commands that print a run summary at the end now track and log exclusions + similarly to skips for easier auditing. ## 0.4.1 diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 4fd15f027f5..2b728e2b907 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; import 'package:platform/platform.dart'; import 'package:yaml/yaml.dart'; @@ -84,7 +85,10 @@ class AnalyzeCommand extends PackageLoopingCommand { /// Ensures that the dependent packages have been fetched for all packages /// (including their sub-packages) that will be analyzed. Future _runPackagesGetOnTargetPackages() async { - final List packageDirectories = await getPackages().toList(); + final List packageDirectories = + await getTargetPackagesAndSubpackages() + .map((PackageEnumerationEntry package) => package.directory) + .toList(); final Set packagePaths = packageDirectories.map((Directory dir) => dir.path).toSet(); packageDirectories.removeWhere((Directory directory) { diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 0bcde6d296d..0e0976ecc6a 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -22,6 +22,10 @@ enum RunState { /// The command was skipped for the package. skipped, + /// The command was skipped for the package because it was explicitly excluded + /// in the command arguments. + excluded, + /// The command failed for the package. failed, } @@ -35,6 +39,9 @@ class PackageResult { PackageResult.skip(String reason) : this._(RunState.skipped, [reason]); + /// A run that was excluded by the command invocation. + PackageResult.exclude() : this._(RunState.excluded); + /// A run that failed. /// /// If [errors] are provided, they will be listed in the summary, otherwise @@ -70,13 +77,14 @@ abstract class PackageLoopingCommand extends PluginCommand { processRunner: processRunner, platform: platform, gitDir: gitDir); /// Packages that had at least one [logWarning] call. - final Set _packagesWithWarnings = {}; + final Set _packagesWithWarnings = + {}; /// Number of warnings that happened outside of a [runForPackage] call. int _otherWarningCount = 0; /// The package currently being run by [runForPackage]. - Directory? _currentPackage; + PackageEnumerationEntry? _currentPackage; /// Called during [run] before any calls to [runForPackage]. This provides an /// opportunity to fail early if the command can't be run (e.g., because the @@ -215,15 +223,24 @@ abstract class PackageLoopingCommand extends PluginCommand { await initializeRun(); - final List packages = includeSubpackages - ? await getPackages().toList() - : await getPlugins().toList(); + final List packages = includeSubpackages + ? await getTargetPackagesAndSubpackages(filterExcluded: false).toList() + : await getTargetPackages(filterExcluded: false).toList(); - final Map results = {}; - for (final Directory package in packages) { + final Map results = + {}; + for (final PackageEnumerationEntry package in packages) { _currentPackage = package; _printPackageHeading(package); - final PackageResult result = await runForPackage(package); + + // Command implementations should never see excluded packages; they are + // included at this level only for logging. + if (package.excluded) { + results[package] = PackageResult.exclude(); + continue; + } + + final PackageResult result = await runForPackage(package.directory); if (result.state == RunState.skipped) { final String message = '${indentation}SKIPPING: ${result.details.first}'; @@ -266,8 +283,11 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Something is always printed to make it easier to distinguish between /// a command running for a package and producing no output, and a command /// not having been run for a package. - void _printPackageHeading(Directory package) { - String heading = 'Running for ${getPackageDescription(package)}'; + void _printPackageHeading(PackageEnumerationEntry package) { + final String packageDisplayName = getPackageDescription(package.directory); + String heading = package.excluded + ? 'Not running for $packageDisplayName; excluded' + : 'Running for $packageDisplayName'; if (hasLongOutput) { heading = ''' @@ -275,24 +295,35 @@ abstract class PackageLoopingCommand extends PluginCommand { || $heading ============================================================ '''; - } else { + } else if (!package.excluded) { heading = '$heading...'; } - captureOutput ? print(heading) : print(Colorize(heading)..cyan()); + if (captureOutput) { + print(heading); + } else { + final Colorize colorizeHeading = Colorize(heading); + print(package.excluded + ? colorizeHeading.darkGray() + : colorizeHeading.cyan()); + } } /// Prints a summary of packges run, packages skipped, and warnings. - void _printRunSummary( - List packages, Map results) { - final Set skippedPackages = results.entries - .where((MapEntry entry) => + void _printRunSummary(List packages, + Map results) { + final Set skippedPackages = results.entries + .where((MapEntry entry) => entry.value.state == RunState.skipped) - .map((MapEntry entry) => entry.key) + .map((MapEntry entry) => + entry.key) .toSet(); - final int skipCount = skippedPackages.length; + final int skipCount = skippedPackages.length + + packages + .where((PackageEnumerationEntry package) => package.excluded) + .length; // Split the warnings into those from packages that ran, and those that // were skipped. - final Set _skippedPackagesWithWarnings = + final Set _skippedPackagesWithWarnings = _packagesWithWarnings.intersection(skippedPackages); final int skippedWarningCount = _skippedPackagesWithWarnings.length; final int runWarningCount = @@ -318,14 +349,17 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Prints a one-line-per-package overview of the run results for each /// package. - void _printPerPackageRunOverview(List packages, - {required Set skipped}) { + void _printPerPackageRunOverview(List packages, + {required Set skipped}) { print('Run overview:'); - for (final Directory package in packages) { + for (final PackageEnumerationEntry package in packages) { final bool hadWarning = _packagesWithWarnings.contains(package); Styles style; String summary; - if (skipped.contains(package)) { + if (package.excluded) { + summary = 'excluded'; + style = Styles.DARK_GRAY; + } else if (skipped.contains(package)) { summary = 'skipped'; style = hadWarning ? Styles.LIGHT_YELLOW : Styles.DARK_GRAY; } else { @@ -339,17 +373,17 @@ abstract class PackageLoopingCommand extends PluginCommand { if (!captureOutput) { summary = (Colorize(summary)..apply(style)).toString(); } - print(' ${getPackageDescription(package)} - $summary'); + print(' ${getPackageDescription(package.directory)} - $summary'); } print(''); } /// Prints a summary of all of the failures from [results]. - void _printFailureSummary( - List packages, Map results) { + void _printFailureSummary(List packages, + Map results) { const String indentation = ' '; _printError(failureListHeader); - for (final Directory package in packages) { + for (final PackageEnumerationEntry package in packages) { final PackageResult result = results[package]!; if (result.state == RunState.failed) { final String errorIndentation = indentation * 2; @@ -359,7 +393,7 @@ abstract class PackageLoopingCommand extends PluginCommand { ':\n$errorIndentation${result.details.join('\n$errorIndentation')}'; } _printError( - '$indentation${getPackageDescription(package)}$errorDetails'); + '$indentation${getPackageDescription(package.directory)}$errorDetails'); } } _printError(failureListFooter); diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 7781eee0d96..db0a821fd2d 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -15,6 +15,19 @@ import 'core.dart'; import 'git_version_finder.dart'; import 'process_runner.dart'; +/// An entry in package enumeration for APIs that need to include extra +/// data about the entry. +class PackageEnumerationEntry { + /// Creates a new entry for the given package directory. + PackageEnumerationEntry(this.directory, {required this.excluded}); + + /// The package's location. + final Directory directory; + + /// Whether or not this package was excluded by the command invocation. + final bool excluded; +} + /// Interface definition for all commands in this tool. // TODO(stuartmorgan): Move most of this logic to PackageLoopingCommand. abstract class PluginCommand extends Command { @@ -97,6 +110,9 @@ abstract class PluginCommand extends Command { int? _shardIndex; int? _shardCount; + // Cached set of explicitly excluded packages. + Set? _excludedPackages; + /// A context that matches the default for [platform]. p.Context get path => platform.isWindows ? p.windows : p.posix; @@ -174,60 +190,82 @@ abstract class PluginCommand extends Command { _shardCount = shardCount; } - /// Returns the root Dart package folders of the plugins involved in this - /// command execution. - // TODO(stuartmorgan): Rename/restructure this, _getAllPlugins, and - // getPackages, as the current naming is very confusing. - Stream getPlugins() async* { + /// Returns the set of plugins to exclude based on the `--exclude` argument. + Set _getExcludedPackageName() { + final Set excludedPackages = _excludedPackages ?? + getStringListArg(_excludeArg).expand((String item) { + if (item.endsWith('.yaml')) { + final File file = packagesDir.fileSystem.file(item); + return (loadYaml(file.readAsStringSync()) as YamlList) + .toList() + .cast(); + } + return [item]; + }).toSet(); + // Cache for future calls. + _excludedPackages = excludedPackages; + return excludedPackages; + } + + /// Returns the root diretories of the packages involved in this command + /// execution. + /// + /// Depending on the command arguments, this may be a user-specified set of + /// packages, the set of packages that should be run for a given diff, or all + /// packages. + /// + /// By default, packages excluded via --exclude will not be in the stream, but + /// they can be included by passing false for [filterExcluded]. + Stream getTargetPackages( + {bool filterExcluded = true}) 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 List allPlugins = + await _getAllPackages().toList(); + allPlugins.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => + p1.directory.path.compareTo(p2.directory.path)); 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; + for (final PackageEnumerationEntry plugin + in allPlugins.sublist(start, end)) { + if (!(filterExcluded && plugin.excluded)) { + yield plugin; + } } } - /// Returns the root Dart package folders of the plugins involved in this - /// command execution, assuming there is only one shard. + /// Returns the root Dart package folders of the packages involved in this + /// command execution, assuming there is only one shard. Depending on the + /// command arguments, this may be a user-specified set of packages, the + /// set of packages that should be run for a given diff, or all packages. + /// + /// This will return packages that have been excluded by the --exclude + /// parameter, annotated in the entry as excluded. /// - /// Plugin packages can exist in the following places relative to the packages + /// 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. + /// packages directory. This is a non-plugin package, or a non-federated + /// plugin. /// 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. + /// packages which implement a single plugin. This directory contains an + /// "app-facing" package which declares the API for the plugin, a + /// platform interface package which declares the API for implementations, + /// and one or more platform-specific implementation packages. /// 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* { + Stream _getAllPackages() async* { Set plugins = Set.from(getStringListArg(_packagesArg)); - final Set excludedPlugins = - getStringListArg(_excludeArg).expand((String item) { - if (item.endsWith('.yaml')) { - final File file = packagesDir.fileSystem.file(item); - return (loadYaml(file.readAsStringSync()) as YamlList) - .toList() - .cast(); - } - return [item]; - }).toSet(); + final Set excludedPluginNames = _getExcludedPackageName(); final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); if (plugins.isEmpty && @@ -248,9 +286,9 @@ abstract class PluginCommand extends Command { 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; + if (plugins.isEmpty || plugins.contains(p.basename(entity.path))) { + yield PackageEnumerationEntry(entity as Directory, + excluded: excludedPluginNames.contains(entity.basename)); } } else if (entity is Directory) { // Look for Dart packages under this top-level directory. @@ -264,13 +302,13 @@ abstract class PluginCommand extends Command { path.relative(subdir.path, from: dir.path); final String packageName = path.basename(subdir.path); final String basenamePath = path.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; + if (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath)) { + yield PackageEnumerationEntry(subdir as Directory, + excluded: excludedPluginNames.contains(basenamePath) || + excludedPluginNames.contains(packageName) || + excludedPluginNames.contains(relativePath)); } } } @@ -279,27 +317,30 @@ abstract class PluginCommand extends Command { } } - /// 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()) { + /// Returns all Dart package folders (typically, base package + example) of + /// the packages involved in this command execution. + /// + /// By default, packages excluded via --exclude will not be in the stream, but + /// they can be included by passing false for [filterExcluded]. + Stream getTargetPackagesAndSubpackages( + {bool filterExcluded = true}) async* { + await for (final PackageEnumerationEntry plugin + in getTargetPackages(filterExcluded: filterExcluded)) { yield plugin; - yield* plugin + yield* plugin.directory .list(recursive: true, followLinks: false) .where(_isDartPackage) - .cast(); + .map((FileSystemEntity directory) => PackageEnumerationEntry( + directory as Directory, // _isDartPackage guarantees this works. + excluded: plugin.excluded)); } } /// Returns the files contained, recursively, within the plugins /// involved in this command execution. Stream getFiles() { - return getPlugins() + return getTargetPackages() + .map((PackageEnumerationEntry entry) => entry.directory) .asyncExpand((Directory folder) => getFilesForPackage(folder)); } 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 ed701445608..d4eccb8a313 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -156,13 +156,14 @@ class CreateAllPluginsAppCommand extends PluginCommand { final Map pathDependencies = {}; - await for (final Directory package in getPlugins()) { - final String pluginName = package.basename; - final File pubspecFile = package.childFile('pubspec.yaml'); + await for (final PackageEnumerationEntry package in getTargetPackages()) { + final Directory pluginDirectory = package.directory; + final String pluginName = pluginDirectory.basename; + final File pubspecFile = pluginDirectory.childFile('pubspec.yaml'); final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec.publishTo != 'none') { - pathDependencies[pluginName] = PathDependency(package.path); + pathDependencies[pluginName] = PathDependency(pluginDirectory.path); } } return pathDependencies; diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 20f01ff98f0..29a8ceb1278 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -39,18 +39,23 @@ class ListCommand extends PluginCommand { Future run() async { switch (getStringArg(_type)) { case _plugin: - await for (final Directory package in getPlugins()) { - print(package.path); + await for (final PackageEnumerationEntry package + in getTargetPackages()) { + print(package.directory.path); } break; case _example: - await for (final Directory package in getExamples()) { + final Stream examples = getTargetPackages() + .map((PackageEnumerationEntry entry) => entry.directory) + .expand(getExamplesForPlugin); + await for (final Directory package in examples) { print(package.path); } break; case _package: - await for (final Directory package in getPackages()) { - print(package.path); + await for (final PackageEnumerationEntry package + in getTargetPackagesAndSubpackages()) { + print(package.directory.path); } break; case _file: diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 542e91af643..00e64ddc21f 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -185,6 +185,28 @@ void main() { package.childDirectory('example').path, ])); }); + + test('excludes subpackages when main package is excluded', () async { + final Directory excluded = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + final Directory included = createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(includeSubpackages: true); + await runCommand(command, arguments: ['--exclude=a_plugin']); + + expect( + command.checkedPackages, + unorderedEquals([ + included.path, + included.childDirectory('example').path, + ])); + expect(command.checkedPackages, isNot(contains(excluded.path))); + expect(command.checkedPackages, + isNot(contains(excluded.childDirectory('example1').path))); + expect(command.checkedPackages, + isNot(contains(excluded.childDirectory('example2').path))); + }); }); group('output', () { @@ -376,6 +398,23 @@ void main() { ])); }); + test('logs exclusions', () async { + createFakePackage('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = + await runCommand(command, arguments: ['--exclude=package_b']); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startSkipColor}Not running for package_b; excluded$_endColor', + ])); + }); + test('logs warnings', () async { final Directory warnPackage = createFakePackage('package_a', packagesDir); warnPackage @@ -435,6 +474,24 @@ void main() { expect(output, isNot(contains(contains('package a - ran')))); }); + test('counts exclusions as skips in run summary', () async { + createFakePackage('package_a', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = + await runCommand(command, arguments: ['--exclude=package_a']); + + expect( + output, + containsAllInOrder([ + '------------------------------------------------------------', + 'Skipped 1 package(s)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + test('prints long-form run summary for long-output commands', () async { final Directory warnPackage1 = createFakePackage('package_a', packagesDir); @@ -478,6 +535,25 @@ void main() { ])); }); + test('prints exclusions as skips in long-form run summary', () async { + createFakePackage('package_a', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = + await runCommand(command, arguments: ['--exclude=package_a']); + + expect( + output, + containsAllInOrder([ + ' package_a - ${_startSkipColor}excluded$_endColor', + '', + 'Skipped 1 package(s)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + test('handles warnings outside of runForPackage', () async { createFakePackage('package_a', packagesDir); diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 7f67acfb2df..2f332aa8eb5 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -22,12 +22,12 @@ import 'plugin_command_test.mocks.dart'; @GenerateMocks([GitDir]) void main() { late RecordingProcessRunner processRunner; + late SamplePluginCommand command; late CommandRunner runner; late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; late Directory thirdPartyPackagesDir; - late List plugins; late List?> gitDirCommands; late String gitDiffResponse; @@ -53,9 +53,7 @@ void main() { return Future.value(mockProcessResult); }); processRunner = RecordingProcessRunner(); - plugins = []; - final SamplePluginCommand samplePluginCommand = SamplePluginCommand( - plugins, + command = SamplePluginCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -63,7 +61,7 @@ void main() { ); runner = CommandRunner('common_command', 'Test for common functionality'); - runner.addCommand(samplePluginCommand); + runner.addCommand(command); }); group('plugin iteration', () { @@ -71,7 +69,8 @@ void main() { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample']); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('includes both plugins and packages', () async { @@ -81,7 +80,7 @@ void main() { final Directory package4 = createFakePackage('package4', packagesDir); await runCapturingPrint(runner, ['sample']); expect( - plugins, + command.plugins, unorderedEquals([ plugin1.path, plugin2.path, @@ -96,7 +95,7 @@ void main() { final Directory plugin3 = createFakePlugin('plugin3', thirdPartyPackagesDir); await runCapturingPrint(runner, ['sample']); - expect(plugins, + expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); }); @@ -108,7 +107,7 @@ void main() { await runCapturingPrint( runner, ['sample', '--packages=plugin1,package4']); expect( - plugins, + command.plugins, unorderedEquals([ plugin1.path, package4.path, @@ -123,7 +122,7 @@ void main() { await runCapturingPrint( runner, ['sample', '--plugins=plugin1,package4']); expect( - plugins, + command.plugins, unorderedEquals([ plugin1.path, package4.path, @@ -138,7 +137,7 @@ void main() { '--packages=plugin1,plugin2', '--exclude=plugin1' ]); - expect(plugins, unorderedEquals([plugin2.path])); + expect(command.plugins, unorderedEquals([plugin2.path])); }); test('exclude packages when packages flag isn\'t specified', () async { @@ -146,7 +145,7 @@ void main() { createFakePlugin('plugin2', packagesDir); await runCapturingPrint( runner, ['sample', '--exclude=plugin1,plugin2']); - expect(plugins, unorderedEquals([])); + expect(command.plugins, unorderedEquals([])); }); test('exclude federated plugins when packages flag is specified', () async { @@ -157,7 +156,7 @@ void main() { '--packages=federated/plugin1,plugin2', '--exclude=federated/plugin1' ]); - expect(plugins, unorderedEquals([plugin2.path])); + expect(command.plugins, unorderedEquals([plugin2.path])); }); test('exclude entire federated plugins when packages flag is specified', @@ -169,7 +168,7 @@ void main() { '--packages=federated/plugin1,plugin2', '--exclude=federated' ]); - expect(plugins, unorderedEquals([plugin2.path])); + expect(command.plugins, unorderedEquals([plugin2.path])); }); test('exclude accepts config files', () async { @@ -182,7 +181,7 @@ void main() { '--packages=plugin1', '--exclude=${configFile.path}' ]); - expect(plugins, unorderedEquals([])); + expect(command.plugins, unorderedEquals([])); }); group('test run-on-changed-packages', () { @@ -195,7 +194,8 @@ void main() { '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test( @@ -210,7 +210,8 @@ void main() { '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if .cirrus.yml changes.', () async { @@ -226,7 +227,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if .ci.yaml changes', () async { @@ -242,7 +244,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if anything in .ci/ changes', @@ -259,7 +262,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if anything in script changes.', @@ -276,7 +280,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if the root analysis options change.', @@ -293,7 +298,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if formatting options change.', @@ -310,7 +316,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('Only changed plugin should be tested.', () async { @@ -323,7 +330,7 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); test('multiple files in one plugin should also test the plugin', @@ -340,7 +347,7 @@ packages/plugin1/ios/plugin1.m '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); test('multiple plugins changed should test all the changed plugins', @@ -358,7 +365,8 @@ packages/plugin2/ios/plugin2.m '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test( @@ -379,7 +387,31 @@ packages/plugin1/plugin1_web/plugin1_web.dart '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); + }); + + test( + 'changing one plugin in a federated group should include all plugins in the group', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1/plugin1.dart +'''; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); + final Directory plugin2 = createFakePlugin('plugin1_platform_interface', + packagesDir.childDirectory('plugin1')); + final Directory plugin3 = createFakePlugin( + 'plugin1_web', packagesDir.childDirectory('plugin1')); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect( + command.plugins, + unorderedEquals( + [plugin1.path, plugin2.path, plugin3.path])); }); test( @@ -401,7 +433,8 @@ packages/plugin3/plugin3.dart '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('--exclude flag works with --run-on-changed-packages', () async { @@ -421,15 +454,155 @@ packages/plugin3/plugin3.dart '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); }); }); + + group('sharding', () { + test('distributes evenly when evenly divisible', () async { + final List> expectedShards = >[ + [ + createFakePackage('package1', packagesDir), + createFakePackage('package2', packagesDir), + createFakePackage('package3', packagesDir), + ], + [ + createFakePackage('package4', packagesDir), + createFakePackage('package5', packagesDir), + createFakePackage('package6', packagesDir), + ], + [ + createFakePackage('package7', packagesDir), + createFakePackage('package8', packagesDir), + createFakePackage('package9', packagesDir), + ], + ]; + + for (int i = 0; i < expectedShards.length; ++i) { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'Shard testing'); + localRunner.addCommand(localCommand); + + await runCapturingPrint(localRunner, [ + 'sample', + '--shardIndex=$i', + '--shardCount=3', + ]); + expect( + localCommand.plugins, + unorderedEquals(expectedShards[i] + .map((Directory package) => package.path) + .toList())); + } + }); + + test('distributes as evenly as possible when not evenly divisible', + () async { + final List> expectedShards = >[ + [ + createFakePackage('package1', packagesDir), + createFakePackage('package2', packagesDir), + createFakePackage('package3', packagesDir), + ], + [ + createFakePackage('package4', packagesDir), + createFakePackage('package5', packagesDir), + createFakePackage('package6', packagesDir), + ], + [ + createFakePackage('package7', packagesDir), + createFakePackage('package8', packagesDir), + ], + ]; + + for (int i = 0; i < expectedShards.length; ++i) { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'Shard testing'); + localRunner.addCommand(localCommand); + + await runCapturingPrint(localRunner, [ + 'sample', + '--shardIndex=$i', + '--shardCount=3', + ]); + expect( + localCommand.plugins, + unorderedEquals(expectedShards[i] + .map((Directory package) => package.path) + .toList())); + } + }); + + // In CI (which is the use case for sharding) we often want to run muliple + // commands on the same set of packages, but the exclusion lists for those + // commands may be different. In those cases we still want all the commands + // to operate on a consistent set of plugins. + // + // E.g., some commands require running build-examples in a previous step; + // excluding some plugins from the later step shouldn't change what's tested + // in each shard, as it may no longer align with what was built. + test('counts excluded plugins when sharding', () async { + final List> expectedShards = >[ + [ + createFakePackage('package1', packagesDir), + createFakePackage('package2', packagesDir), + createFakePackage('package3', packagesDir), + ], + [ + createFakePackage('package4', packagesDir), + createFakePackage('package5', packagesDir), + createFakePackage('package6', packagesDir), + ], + [ + createFakePackage('package7', packagesDir), + ], + ]; + // These would be in the last shard, but are excluded. + createFakePackage('package8', packagesDir); + createFakePackage('package9', packagesDir); + + for (int i = 0; i < expectedShards.length; ++i) { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'Shard testing'); + localRunner.addCommand(localCommand); + + await runCapturingPrint(localRunner, [ + 'sample', + '--shardIndex=$i', + '--shardCount=3', + '--exclude=package8,package9', + ]); + expect( + localCommand.plugins, + unorderedEquals(expectedShards[i] + .map((Directory package) => package.path) + .toList())); + } + }); + }); } class SamplePluginCommand extends PluginCommand { SamplePluginCommand( - this._plugins, Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), @@ -437,7 +610,7 @@ class SamplePluginCommand extends PluginCommand { }) : super(packagesDir, processRunner: processRunner, platform: platform, gitDir: gitDir); - final List _plugins; + final List plugins = []; @override final String name = 'sample'; @@ -447,8 +620,8 @@ class SamplePluginCommand extends PluginCommand { @override Future run() async { - await for (final Directory package in getPlugins()) { - _plugins.add(package.path); + await for (final PackageEnumerationEntry package in getTargetPackages()) { + plugins.add(package.directory.path); } } } From 9b590484f6113e9db6f328c02c0d13379997cd90 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 13 Aug 2021 16:22:24 -0700 Subject: [PATCH 107/249] Eliminate build_all_plugins_app.sh (#4232) Removes the `build_all_plugins_app.sh` bash script, in support of the goal of eliminating all use of bash from the repository (for maintainability, and for better Windows compatibility). - The exclusion list moves to a config file, match other recent repo changes - The exclusion logging moves into the tool itself, consistent with the tool doing more logging of skipped and excluded plugins - The bulk of the logic moves to a Cirrus task template. This was done instead of rewriting the script in Dart, even though it will mean more work for alternate CI support (e.g., bringing this up on a Windows LUCI bot), because breaking it into components makes it easier to pinpoint failures from the CI UI rather than having all the steps smashed together. --- .../tool/lib/src/common/plugin_command.dart | 4 +- .../src/create_all_plugins_app_command.dart | 28 +++++++++---- .../create_all_plugins_app_command_test.dart | 42 +++++++++++++++---- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index db0a821fd2d..10f42336087 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -191,7 +191,7 @@ abstract class PluginCommand extends Command { } /// Returns the set of plugins to exclude based on the `--exclude` argument. - Set _getExcludedPackageName() { + Set getExcludedPackageNames() { final Set excludedPackages = _excludedPackages ?? getStringListArg(_excludeArg).expand((String item) { if (item.endsWith('.yaml')) { @@ -265,7 +265,7 @@ abstract class PluginCommand extends Command { Stream _getAllPackages() async* { Set plugins = Set.from(getStringListArg(_packagesArg)); - final Set excludedPluginNames = _getExcludedPackageName(); + final Set excludedPluginNames = getExcludedPackageNames(); final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); if (plugins.isEmpty && 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 d4eccb8a313..e1cee6f3fe7 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -12,22 +12,27 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; +const String _outputDirectoryFlag = 'output-dir'; + /// A command to create an application that builds all in a single application. class CreateAllPluginsAppCommand extends PluginCommand { /// Creates an instance of the builder command. CreateAllPluginsAppCommand( Directory packagesDir, { Directory? pluginsRoot, - }) : pluginsRoot = pluginsRoot ?? packagesDir.fileSystem.currentDirectory, - super(packagesDir) { - appDirectory = this.pluginsRoot.childDirectory('all_plugins'); + }) : super(packagesDir) { + final Directory defaultDir = + pluginsRoot ?? packagesDir.fileSystem.currentDirectory; + argParser.addOption(_outputDirectoryFlag, + defaultsTo: defaultDir.path, + help: 'The path the directory to create the "all_plugins" project in.\n' + 'Defaults to the repository root.'); } - /// The root directory of the plugin repository. - Directory pluginsRoot; - /// The location of the synthesized app project. - late Directory appDirectory; + Directory get appDirectory => packagesDir.fileSystem + .directory(getStringArg(_outputDirectoryFlag)) + .childDirectory('all_plugins'); @override String get description => @@ -43,6 +48,15 @@ class CreateAllPluginsAppCommand extends PluginCommand { throw ToolExit(exitCode); } + final Set excluded = getExcludedPackageNames(); + if (excluded.isNotEmpty) { + print('Exluding the following plugins from the combined build:'); + for (final String plugin in excluded) { + print(' $plugin'); + } + print(''); + } + await Future.wait(>[ _genPubspecWithAllPlugins(), _updateAppGradle(), diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 073024a17bb..4439d13c362 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -13,10 +13,10 @@ import 'util.dart'; void main() { group('$CreateAllPluginsAppCommand', () { late CommandRunner runner; - FileSystem fileSystem; + late CreateAllPluginsAppCommand command; + late FileSystem fileSystem; late Directory testRoot; late Directory packagesDir; - late Directory appDir; setUp(() { // Since the core of this command is a call to 'flutter create', the test @@ -26,11 +26,10 @@ void main() { testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); - final CreateAllPluginsAppCommand command = CreateAllPluginsAppCommand( + command = CreateAllPluginsAppCommand( packagesDir, pluginsRoot: testRoot, ); - appDir = command.appDirectory; runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPluginsAppCommand'); runner.addCommand(command); @@ -47,7 +46,7 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app']); final List pubspec = - appDir.childFile('pubspec.yaml').readAsLinesSync(); + command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); expect( pubspec, @@ -65,7 +64,7 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app']); final List pubspec = - appDir.childFile('pubspec.yaml').readAsLinesSync(); + command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); expect( pubspec, @@ -82,9 +81,38 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app']); final String pubspec = - appDir.childFile('pubspec.yaml').readAsStringSync(); + command.appDirectory.childFile('pubspec.yaml').readAsStringSync(); expect(pubspec, contains(RegExp('sdk:\\s*(?:["\']>=|[^])2\\.12\\.'))); }); + + test('handles --output-dir', () async { + createFakePlugin('plugina', packagesDir); + + final Directory customOutputDir = + fileSystem.systemTempDirectory.createTempSync(); + await runCapturingPrint(runner, + ['all-plugins-app', '--output-dir=${customOutputDir.path}']); + + expect(command.appDirectory.path, + customOutputDir.childDirectory('all_plugins').path); + }); + + test('logs exclusions', () async { + createFakePlugin('plugina', packagesDir); + createFakePlugin('pluginb', packagesDir); + createFakePlugin('pluginc', packagesDir); + + final List output = await runCapturingPrint( + runner, ['all-plugins-app', '--exclude=pluginb,pluginc']); + + expect( + output, + containsAllInOrder([ + 'Exluding the following plugins from the combined build:', + ' pluginb', + ' pluginc', + ])); + }); }); } From 69a271351d733d362f7f414728586ebb91a2c0f5 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 17 Aug 2021 08:36:40 -0700 Subject: [PATCH 108/249] [flutter_plugin_tool] Don't allow NEXT on version bumps (#4246) The special "NEXT" entry in a CHANGELOG should never be present in a commit that bumped the version. This validates that this is true even if the CHANGELOG would be correct for a non-version-change state, to catch someone incorrectly resolving a merge conflict by leaving both parts of the conflict, rather than folding the 'NEXT' entry's list into the new version's notes. Fixes https://github.com/flutter/flutter/issues/85584 --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/version_check_command.dart | 84 ++++++++++++++----- .../tool/test/version_check_command_test.dart | 54 +++++++++++- 3 files changed, 115 insertions(+), 25 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7f326ff3c8f..584ea571f0e 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -23,6 +23,8 @@ the new `native-test` command. - Commands that print a run summary at the end now track and log exclusions similarly to skips for easier auditing. +- `version-check` now validates that `NEXT` is not present when changing + the version. ## 0.4.1 diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index c08600c3f66..67c56378288 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -32,6 +32,21 @@ enum NextVersionType { RELEASE, } +/// The state of a package's version relative to the comparison base. +enum _CurrentVersionState { + /// The version is unchanged. + unchanged, + + /// The version has changed, and the transition is valid. + validChange, + + /// The version has changed, and the transition is invalid. + invalidChange, + + /// There was an error determining the version state. + unknown, +} + /// Returns the set of allowed next versions, with their change type, for /// [version]. /// @@ -140,11 +155,28 @@ class VersionCheckCommand extends PackageLoopingCommand { final List errors = []; - if (!await _hasValidVersionChange(package, pubspec: pubspec)) { - errors.add('Disallowed version change.'); + bool versionChanged; + final _CurrentVersionState versionState = + await _getVersionState(package, pubspec: pubspec); + switch (versionState) { + case _CurrentVersionState.unchanged: + versionChanged = false; + break; + case _CurrentVersionState.validChange: + versionChanged = true; + break; + case _CurrentVersionState.invalidChange: + versionChanged = true; + errors.add('Disallowed version change.'); + break; + case _CurrentVersionState.unknown: + versionChanged = false; + errors.add('Unable to determine previous version.'); + break; } - if (!(await _hasConsistentVersion(package, pubspec: pubspec))) { + if (!(await _validateChangelogVersion(package, + pubspec: pubspec, pubspecVersionChanged: versionChanged))) { errors.add('pubspec.yaml and CHANGELOG.md have different versions'); } @@ -195,10 +227,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} return await gitVersionFinder.getPackageVersion(gitPath); } - /// Returns true if the version of [package] is either unchanged relative to - /// the comparison base (git or pub, depending on flags), or is a valid - /// version transition. - Future _hasValidVersionChange( + /// Returns the state of the verison of [package] relative to the comparison + /// base (git or pub, depending on flags). + Future<_CurrentVersionState> _getVersionState( Directory package, { required Pubspec pubspec, }) async { @@ -208,7 +239,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} if (getBoolArg(_againstPubFlag)) { previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); if (previousVersion == null) { - return false; + return _CurrentVersionState.unknown; } if (previousVersion != Version.none) { print( @@ -225,12 +256,12 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); logWarning( '${indentation}If this plugin is not new, something has gone wrong.'); - return true; + return _CurrentVersionState.validChange; // Assume new, thus valid. } if (previousVersion == currentVersion) { print('${indentation}No version change.'); - return true; + return _CurrentVersionState.unchanged; } // Check for reverts when doing local validation. @@ -241,9 +272,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // to be a revert rather than a typo by checking that the transition // from the lower version to the new version would have been valid. if (possibleVersionsFromNewVersion.containsKey(previousVersion)) { - print('${indentation}New version is lower than previous version. ' + logWarning('${indentation}New version is lower than previous version. ' 'This is assumed to be a revert.'); - return true; + return _CurrentVersionState.validChange; } } @@ -257,7 +288,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} printError('${indentation}Incorrectly updated version.\n' '${indentation}HEAD: $currentVersion, $source: $previousVersion.\n' '${indentation}Allowed versions: $allowedNextVersions'); - return false; + return _CurrentVersionState.invalidChange; } final bool isPlatformInterface = @@ -268,16 +299,20 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR) { printError('${indentation}Breaking change detected.\n' '${indentation}Breaking changes to platform interfaces are strongly discouraged.\n'); - return false; + return _CurrentVersionState.invalidChange; } - return true; + return _CurrentVersionState.validChange; } - /// Returns whether or not the pubspec version and CHANGELOG version for - /// [plugin] match. - Future _hasConsistentVersion( + /// Checks whether or not [package]'s CHANGELOG's versioning is correct, + /// both that it matches [pubspec] and that NEXT is used correctly, printing + /// the results of its checks. + /// + /// Returns false if the CHANGELOG fails validation. + Future _validateChangelogVersion( Directory package, { required Pubspec pubspec, + required bool pubspecVersionChanged, }) async { // This method isn't called unless `version` is non-null. final Version fromPubspec = pubspec.version!; @@ -296,10 +331,19 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // Remove all leading mark down syntax from the version line. String? versionString = firstLineWithText?.split(' ').last; + final String badNextErrorMessage = '${indentation}When bumping the version ' + 'for release, the NEXT section should be incorporated into the new ' + 'version\'s release notes.'; + // Skip validation for the special NEXT version that's used to accumulate // changes that don't warrant publishing on their own. final bool hasNextSection = versionString == 'NEXT'; if (hasNextSection) { + // NEXT should not be present in a commit that changes the version. + if (pubspecVersionChanged) { + printError(badNextErrorMessage); + return false; + } print( '${indentation}Found NEXT; validating next version in the CHANGELOG.'); // Ensure that the version in pubspec hasn't changed without updating @@ -334,9 +378,7 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. if (!hasNextSection) { final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); if (lines.any((String line) => nextRegex.hasMatch(line))) { - printError('${indentation}When bumping the version for release, the ' - 'NEXT section should be incorporated into the new version\'s ' - 'release notes.'); + printError(badNextErrorMessage); return false; } } diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 587de1a58cd..7765073feb0 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -373,6 +373,10 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); await expectLater( @@ -384,8 +388,7 @@ void main() { ); }); - test('Fail if NEXT is left in the CHANGELOG when adding a version bump', - () async { + test('Fail if NEXT appears after a version', () async { const String version = '1.0.1'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, version: version); @@ -419,6 +422,45 @@ void main() { ); }); + test('Fail if NEXT is left in the CHANGELOG when adding a version bump', + () async { + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); + + const String changelog = ''' +## NEXT +* Some changes that should have been folded in 1.0.1. +## $version +* Some changes. +## 1.0.0 +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + bool hasError = false; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + output, + containsAllInOrder([ + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.') + ]), + ); + }); + test('Fail if the version changes without replacing NEXT', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, version: '1.0.1'); @@ -430,6 +472,10 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + bool hasError = false; final List output = await runCapturingPrint(runner, [ 'version-check', @@ -444,8 +490,8 @@ void main() { expect( output, containsAllInOrder([ - contains('Found NEXT; validating next version in the CHANGELOG.'), - contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.') ]), ); }); From 954804f68d1670a9b2aa25a59a845fd614320b81 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 17 Aug 2021 09:43:31 -0700 Subject: [PATCH 109/249] [flutter_plugin_tools] Add Android native UI test support (#4188) Adds integration test support for Android to `native-test`. Also fixes an issue where the existing unit test support was not honoring `--no-unit`. Fixes https://github.com/flutter/flutter/issues/86490 --- script/tool/CHANGELOG.md | 4 + script/tool/lib/src/native_test_command.dart | 124 +++++++--- script/tool/pubspec.yaml | 2 +- .../tool/test/native_test_command_test.dart | 227 +++++++++++++++++- 4 files changed, 319 insertions(+), 38 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 584ea571f0e..267019fe735 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,9 @@ ## NEXT +- Added Android native integration test support to `native-test`. + +## 0.5.0 + - `--exclude` and `--custom-analysis` now accept paths to YAML files that contain lists of packages to exclude, in addition to just package names, so that exclude lists can be maintained separately from scripts and CI diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 36b12741f2c..9fc6a2912cc 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -96,11 +96,6 @@ this command. throw ToolExit(exitInvalidArguments); } - if (getBoolArg(kPlatformAndroid) && getBoolArg(_integrationTestFlag)) { - logWarning('This command currently only supports unit tests for Android. ' - 'See https://github.com/flutter/flutter/issues/86490.'); - } - // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { String destination = getStringArg(_iosDestinationFlag); @@ -178,12 +173,8 @@ this command. } Future<_PlatformResult> _testAndroid(Directory plugin, _TestMode mode) async { - final List examplesWithTests = []; - for (final Directory example in getExamplesForPlugin(plugin)) { - if (!isFlutterPackage(example)) { - continue; - } - if (example + bool exampleHasUnitTests(Directory example) { + return example .childDirectory('android') .childDirectory('app') .childDirectory('src') @@ -193,20 +184,62 @@ this command. .childDirectory('android') .childDirectory('src') .childDirectory('test') - .existsSync()) { - examplesWithTests.add(example); - } else { - _printNoExampleTestsMessage(example, 'Android'); - } + .existsSync(); } - if (examplesWithTests.isEmpty) { - return _PlatformResult(RunState.skipped); + bool exampleHasNativeIntegrationTests(Directory example) { + final Directory integrationTestDirectory = example + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('androidTest'); + // There are two types of integration tests that can be in the androidTest + // directory: + // - FlutterTestRunner.class tests, which bridge to Dart integration tests + // - Purely native tests + // Only the latter is supported by this command; the former will hang if + // run here because they will wait for a Dart call that will never come. + // + // This repository uses a convention of putting the former in a + // *ActivityTest.java file, so ignore that file when checking for tests. + // Also ignore DartIntegrationTest.java, which defines the annotation used + // below for filtering the former out when running tests. + // + // If those are the only files, then there are no tests to run here. + return integrationTestDirectory.existsSync() && + integrationTestDirectory + .listSync(recursive: true) + .whereType() + .any((File file) { + final String basename = file.basename; + return !basename.endsWith('ActivityTest.java') && + basename != 'DartIntegrationTest.java'; + }); } + final Iterable examples = getExamplesForPlugin(plugin); + + bool ranTests = false; bool failed = false; bool hasMissingBuild = false; - for (final Directory example in examplesWithTests) { + for (final Directory example in examples) { + final bool hasUnitTests = exampleHasUnitTests(example); + final bool hasIntegrationTests = + exampleHasNativeIntegrationTests(example); + + if (mode.unit && !hasUnitTests) { + _printNoExampleTestsMessage(example, 'Android unit'); + } + if (mode.integration && !hasIntegrationTests) { + _printNoExampleTestsMessage(example, 'Android integration'); + } + + final bool runUnitTests = mode.unit && hasUnitTests; + final bool runIntegrationTests = mode.integration && hasIntegrationTests; + if (!runUnitTests && !runIntegrationTests) { + continue; + } + final String exampleName = getPackageDescription(example); _printRunningExampleTestsMessage(example, 'Android'); @@ -221,17 +254,52 @@ this command. continue; } - final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest'], - workingDir: androidDirectory); - if (exitCode != 0) { - printError('$exampleName tests failed.'); - failed = true; + if (runUnitTests) { + print('Running unit tests...'); + final int exitCode = await processRunner.runAndStream( + gradleFile.path, ['testDebugUnitTest'], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName unit tests failed.'); + failed = true; + } + ranTests = true; } + + if (runIntegrationTests) { + // FlutterTestRunner-based tests will hang forever if run in a normal + // app build, since they wait for a Dart call from integration_test that + // will never come. Those tests have an extra annotation to allow + // filtering them out. + const String filter = + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + + print('Running integration tests...'); + final int exitCode = await processRunner.runAndStream( + gradleFile.path, + [ + 'app:connectedAndroidTest', + '-Pandroid.testInstrumentationRunnerArguments.$filter', + ], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName integration tests failed.'); + failed = true; + } + ranTests = true; + } + } + + if (failed) { + return _PlatformResult(RunState.failed, + error: hasMissingBuild + ? 'Examples must be built before testing.' + : null); + } + if (!ranTests) { + return _PlatformResult(RunState.skipped); } - return _PlatformResult(failed ? RunState.failed : RunState.succeeded, - error: - hasMissingBuild ? 'Examples must be built before testing.' : null); + return _PlatformResult(RunState.succeeded); } Future<_PlatformResult> _testIos(Directory plugin, _TestMode mode) { diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 7b2cdd4f410..02b3ca624b9 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.4.1 +version: 0.5.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index e656e2f2372..59ca17b25c0 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -16,6 +16,10 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; +const String _androidIntegrationTestFilter = + '-Pandroid.testInstrumentationRunnerArguments.' + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + final Map _kDeviceListMap = { 'runtimes': >[ { @@ -353,7 +357,7 @@ void main() { }); group('Android', () { - test('runs Java tests in Android implementation folder', () async { + test('runs Java unit tests in Android implementation folder', () async { final Directory plugin = createFakePlugin( 'plugin', packagesDir, @@ -383,7 +387,7 @@ void main() { ); }); - test('runs Java tests in example folder', () async { + test('runs Java unit tests in example folder', () async { final Directory plugin = createFakePlugin( 'plugin', packagesDir, @@ -413,6 +417,172 @@ void main() { ); }); + test('runs Java integration tests', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test( + 'ignores Java integration test files associated with integration_test', + () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java', + 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java', + 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/MainActivityTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + // Nothing should run since those files are all + // integration_test-specific. + expect( + processRunner.recordedCalls, + orderedEquals([]), + ); + }); + + test('runs all tests when present', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-unit', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-integration', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-integration']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ]), + ); + }); + test('fails when the app needs to be built', () async { createFakePlugin( 'plugin', @@ -444,6 +614,46 @@ void main() { ); }); + test('logs missing test types', () async { + // No unit tests. + createFakePlugin( + 'plugin1', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + // No integration tests. + createFakePlugin( + 'plugin2', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + ], + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android']); + + expect( + output, + containsAllInOrder([ + contains('No Android unit tests found for plugin1/example'), + contains('Running integration tests...'), + contains( + 'No Android integration tests found for plugin2/example'), + contains('Running unit tests...'), + ])); + }); + test('fails when a test fails', () async { final Directory pluginDir = createFakePlugin( 'plugin', @@ -478,7 +688,7 @@ void main() { expect( output, containsAllInOrder([ - contains('plugin/example tests failed.'), + contains('plugin/example unit tests failed.'), contains('The following packages had errors:'), contains('plugin') ]), @@ -518,7 +728,8 @@ void main() { expect( output, containsAllInOrder([ - contains('No Android tests found for plugin/example'), + contains('No Android unit tests found for plugin/example'), + contains('No Android integration tests found for plugin/example'), contains('SKIPPING: No tests found.'), ]), ); @@ -810,10 +1021,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path), + ProcessCall(androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], androidFolder.path), ProcessCall( 'xcrun', const [ @@ -1003,7 +1212,7 @@ void main() { output, containsAllInOrder([ contains('Running tests for Android...'), - contains('plugin/example tests failed.'), + contains('plugin/example unit tests failed.'), contains('Running tests for iOS...'), contains('Successfully ran iOS xctest for plugin/example'), contains('The following packages had errors:'), From 721421a091409d96e9350e6cd69db1b5ff6362b7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 18 Aug 2021 06:51:10 -0700 Subject: [PATCH 110/249] [flutter_plugin_tools] Add a command to lint Android code (#4206) Adds a new `lint-android` command to run `gradlew lint` on Android plugins. Also standardizes the names of the Cirrus tasks that run all the build and platform-specific (i.e., not Dart unit test) tests for each platform, as they were getting unnecessarily long and complex in some cases. Fixes https://github.com/flutter/flutter/issues/87071 --- script/tool/CHANGELOG.md | 1 + script/tool/lib/src/common/gradle.dart | 57 ++++++ script/tool/lib/src/common/xcode.dart | 2 +- .../lib/src/firebase_test_lab_command.dart | 46 +++-- script/tool/lib/src/lint_android_command.dart | 61 ++++++ script/tool/lib/src/main.dart | 2 + script/tool/lib/src/native_test_command.dart | 29 ++- script/tool/test/common/gradle_test.dart | 179 ++++++++++++++++++ .../tool/test/lint_android_command_test.dart | 158 ++++++++++++++++ 9 files changed, 495 insertions(+), 40 deletions(-) create mode 100644 script/tool/lib/src/common/gradle.dart create mode 100644 script/tool/lib/src/lint_android_command.dart create mode 100644 script/tool/test/common/gradle_test.dart create mode 100644 script/tool/test/lint_android_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 267019fe735..87917d63d3f 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT - Added Android native integration test support to `native-test`. +- Added a new `android-lint` command to lint Android plugin native code. ## 0.5.0 diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart new file mode 100644 index 00000000000..e7214bf2971 --- /dev/null +++ b/script/tool/lib/src/common/gradle.dart @@ -0,0 +1,57 @@ +// 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:platform/platform.dart'; + +import 'process_runner.dart'; + +const String _gradleWrapperWindows = 'gradlew.bat'; +const String _gradleWrapperNonWindows = 'gradlew'; + +/// A utility class for interacting with Gradle projects. +class GradleProject { + /// Creates an instance that runs commands for [project] with the given + /// [processRunner]. + /// + /// If [log] is true, commands run by this instance will long various status + /// messages. + GradleProject( + this.flutterProject, { + this.processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + }); + + /// The directory of a Flutter project to run Gradle commands in. + final Directory flutterProject; + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// The platform that commands are being run on. + final Platform platform; + + /// The project's 'android' directory. + Directory get androidDirectory => flutterProject.childDirectory('android'); + + /// The path to the Gradle wrapper file for the project. + File get gradleWrapper => androidDirectory.childFile( + platform.isWindows ? _gradleWrapperWindows : _gradleWrapperNonWindows); + + /// Whether or not the project is ready to have Gradle commands run on it + /// (i.e., whether the `flutter` tool has generated the necessary files). + bool isConfigured() => gradleWrapper.existsSync(); + + /// Runs a `gradlew` command with the given parameters. + Future runCommand( + String target, { + List arguments = const [], + }) { + return processRunner.runAndStream( + gradleWrapper.path, + [target, ...arguments], + workingDir: androidDirectory, + ); + } +} diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart index d6bbae419ed..83f681bcb49 100644 --- a/script/tool/lib/src/common/xcode.dart +++ b/script/tool/lib/src/common/xcode.dart @@ -15,7 +15,7 @@ const String _xcRunCommand = 'xcrun'; /// A utility class for interacting with the installed version of Xcode. class Xcode { - /// Creates an instance that runs commends with the given [processRunner]. + /// Creates an instance that runs commands with the given [processRunner]. /// /// If [log] is true, commands run by this instance will long various status /// messages. diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 8459f6c7015..fd2de97be4b 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -10,6 +10,7 @@ import 'package:platform/platform.dart'; import 'package:uuid/uuid.dart'; import 'common/core.dart'; +import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; @@ -74,8 +75,6 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { 'Runs tests in test_instrumentation folder using the ' 'instrumentation_test package.'; - static const String _gradleWrapper = 'gradlew'; - bool _firebaseProjectConfigured = false; Future _configureFirebaseProject() async { @@ -138,13 +137,15 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } // Ensures that gradle wrapper exists - if (!await _ensureGradleWrapperExists(androidDirectory)) { + final GradleProject project = GradleProject(exampleDirectory, + processRunner: processRunner, platform: platform); + if (!await _ensureGradleWrapperExists(project)) { return PackageResult.fail(['Unable to build example apk']); } await _configureFirebaseProject(); - if (!await _runGradle(androidDirectory, 'app:assembleAndroidTest')) { + if (!await _runGradle(project, 'app:assembleAndroidTest')) { return PackageResult.fail(['Unable to assemble androidTest']); } @@ -156,8 +157,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { for (final File test in _findIntegrationTestFiles(package)) { final String testName = getRelativePosixPath(test, from: package); print('Testing $testName...'); - if (!await _runGradle(androidDirectory, 'app:assembleDebug', - testFile: test)) { + if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) { printError('Could not build $testName'); errors.add('$testName failed to build'); continue; @@ -204,12 +204,12 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } - /// Checks that 'gradlew' exists in [androidDirectory], and if not runs a + /// Checks that Gradle has been configured for [project], and if not runs a /// Flutter build to generate it. /// /// Returns true if either gradlew was already present, or the build succeeds. - Future _ensureGradleWrapperExists(Directory androidDirectory) async { - if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { + Future _ensureGradleWrapperExists(GradleProject project) async { + if (!project.isConfigured()) { print('Running flutter build apk...'); final String experiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( @@ -219,7 +219,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { 'apk', if (experiment.isNotEmpty) '--enable-experiment=$experiment', ], - workingDir: androidDirectory); + workingDir: project.androidDirectory); if (exitCode != 0) { return false; @@ -228,15 +228,15 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { return true; } - /// Builds [target] using 'gradlew' in the given [directory]. Assumes - /// 'gradlew' already exists. + /// Builds [target] using Gradle in the given [project]. Assumes Gradle is + /// already configured. /// /// [testFile] optionally does the Flutter build with the given test file as /// the build target. /// /// Returns true if the command succeeds. Future _runGradle( - Directory directory, + GradleProject project, String target, { File? testFile, }) async { @@ -245,17 +245,15 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { ? Uri.encodeComponent('--enable-experiment=$experiment') : null; - final int exitCode = await processRunner.runAndStream( - directory.childFile(_gradleWrapper).path, - [ - target, - '-Pverbose=true', - if (testFile != null) '-Ptarget=${testFile.path}', - if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', - if (extraOptions != null) - '-Pextra-gen-snapshot-options=$extraOptions', - ], - workingDir: directory); + final int exitCode = await project.runCommand( + target, + arguments: [ + '-Pverbose=true', + if (testFile != null) '-Ptarget=${testFile.path}', + if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', + if (extraOptions != null) '-Pextra-gen-snapshot-options=$extraOptions', + ], + ); if (exitCode != 0) { return false; diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart new file mode 100644 index 00000000000..be6c6ed3241 --- /dev/null +++ b/script/tool/lib/src/lint_android_command.dart @@ -0,0 +1,61 @@ +// 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:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/gradle.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; + +/// Lint the CocoaPod podspecs and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintAndroidCommand extends PackageLoopingCommand { + /// Creates an instance of the linter command. + LintAndroidCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform); + + @override + final String name = 'lint-android'; + + @override + final String description = 'Runs "gradlew lint" on Android plugins.\n\n' + 'Requires the example to have been build at least once before running.'; + + @override + Future runForPackage(Directory package) async { + if (!pluginSupportsPlatform(kPlatformAndroid, package, + requiredMode: PlatformSupport.inline)) { + return PackageResult.skip( + 'Plugin does not have an Android implemenatation.'); + } + + final Directory exampleDirectory = package.childDirectory('example'); + final GradleProject project = GradleProject(exampleDirectory, + processRunner: processRunner, platform: platform); + + if (!project.isConfigured()) { + return PackageResult.fail(['Build example before linting']); + } + + final String packageName = package.basename; + + // Only lint one build mode to avoid extra work. + // Only lint the plugin project itself, to avoid failing due to errors in + // dependencies. + // + // TODO(stuartmorgan): Consider adding an XML parser to read and summarize + // all results. Currently, only the first three errors will be shown inline, + // and the rest have to be checked via the CI-uploaded artifact. + final int exitCode = await project.runCommand('$packageName:lintDebug'); + + return exitCode == 0 ? PackageResult.success() : PackageResult.fail(); + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 6001c5df7f0..e70cba24cc5 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -16,6 +16,7 @@ import 'drive_examples_command.dart'; import 'firebase_test_lab_command.dart'; import 'format_command.dart'; import 'license_check_command.dart'; +import 'lint_android_command.dart'; import 'lint_podspecs_command.dart'; import 'list_command.dart'; import 'native_test_command.dart'; @@ -51,6 +52,7 @@ void main(List args) { ..addCommand(FirebaseTestLabCommand(packagesDir)) ..addCommand(FormatCommand(packagesDir)) ..addCommand(LicenseCheckCommand(packagesDir)) + ..addCommand(LintAndroidCommand(packagesDir)) ..addCommand(LintPodspecsCommand(packagesDir)) ..addCommand(ListCommand(packagesDir)) ..addCommand(NativeTestCommand(packagesDir)) diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 9fc6a2912cc..0bd2ab45f63 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; import 'common/core.dart'; +import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; @@ -47,8 +48,6 @@ class NativeTestCommand extends PackageLoopingCommand { help: 'Runs native integration (UI) tests', defaultsTo: true); } - static const String _gradleWrapper = 'gradlew'; - // The device destination flags for iOS tests. List _iosDestinationFlags = []; @@ -243,9 +242,12 @@ this command. final String exampleName = getPackageDescription(example); _printRunningExampleTestsMessage(example, 'Android'); - final Directory androidDirectory = example.childDirectory('android'); - final File gradleFile = androidDirectory.childFile(_gradleWrapper); - if (!gradleFile.existsSync()) { + final GradleProject project = GradleProject( + example, + processRunner: processRunner, + platform: platform, + ); + if (!project.isConfigured()) { printError('ERROR: Run "flutter build apk" on $exampleName, or run ' 'this tool\'s "build-examples --apk" command, ' 'before executing tests.'); @@ -256,9 +258,7 @@ this command. if (runUnitTests) { print('Running unit tests...'); - final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest'], - workingDir: androidDirectory); + final int exitCode = await project.runCommand('testDebugUnitTest'); if (exitCode != 0) { printError('$exampleName unit tests failed.'); failed = true; @@ -275,13 +275,12 @@ this command. 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; print('Running integration tests...'); - final int exitCode = await processRunner.runAndStream( - gradleFile.path, - [ - 'app:connectedAndroidTest', - '-Pandroid.testInstrumentationRunnerArguments.$filter', - ], - workingDir: androidDirectory); + final int exitCode = await project.runCommand( + 'app:connectedAndroidTest', + arguments: [ + '-Pandroid.testInstrumentationRunnerArguments.$filter', + ], + ); if (exitCode != 0) { printError('$exampleName integration tests failed.'); failed = true; diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart new file mode 100644 index 00000000000..c24887d3d46 --- /dev/null +++ b/script/tool/test/common/gradle_test.dart @@ -0,0 +1,179 @@ +// 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:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/gradle.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + processRunner = RecordingProcessRunner(); + }); + + group('isConfigured', () { + test('reports true when configured on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + expect(project.isConfigured(), true); + }); + + test('reports true when configured on non-Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + expect(project.isConfigured(), true); + }); + + test('reports false when not configured on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/foo']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + expect(project.isConfigured(), false); + }); + + test('reports true when configured on non-Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/foo']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + expect(project.isConfigured(), false); + }); + }); + + group('runXcodeBuild', () { + test('runs without arguments', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew').path, + const [ + 'foo', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('runs with arguments', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + final int exitCode = await project.runCommand( + 'foo', + arguments: ['--bar', '--baz'], + ); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew').path, + const [ + 'foo', + '--bar', + '--baz', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('runs with the correct wrapper on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew.bat').path, + const [ + 'foo', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('returns error codes', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + processRunner.mockProcessesForExecutable[project.gradleWrapper.path] = + [ + MockProcess.failing(), + ]; + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 1); + }); + }); +} diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart new file mode 100644 index 00000000000..05ead220c15 --- /dev/null +++ b/script/tool/test/lint_android_command_test.dart @@ -0,0 +1,158 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.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/lint_android_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintAndroidCommand', () { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late MockPlatform mockPlatform; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + mockPlatform = MockPlatform(); + processRunner = RecordingProcessRunner(); + final LintAndroidCommand command = LintAndroidCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'lint_android_test', 'Test for $LintAndroidCommand'); + runner.addCommand(command); + }); + + test('runs gradle lint', () async { + final Directory pluginDir = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/android/gradlew', + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); + + final Directory androidDir = + pluginDir.childDirectory('example').childDirectory('android'); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidDir.childFile('gradlew').path, + const ['plugin1:lintDebug'], + androidDir.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('fails if gradlew is missing', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['lint-android'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Build example before linting'), + ], + )); + }); + + test('fails if linting finds issues', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); + + processRunner.mockProcessesForExecutable['gradlew'] = [ + MockProcess.failing(), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['lint-android'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Build example before linting'), + ], + )); + }); + + test('skips non-Android plugins', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implemenatation.') + ], + )); + }); + + test('skips non-inline plugins', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.federated + }); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implemenatation.') + ], + )); + }); + }); +} From 0f6d559f10b5b695caf862233612da8b0ba5deae Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 20 Aug 2021 07:58:24 -0700 Subject: [PATCH 111/249] Fix and test for 'implements' pubspec entry (#4242) The federated plugin spec calls for implementation packages to include an `implements` entry in the `plugins` section of the `pubspec.yaml` indicating what app-facing package it implements. Most of the described behaviors of the `flutter` tool aren't implemented yet, and the pub.dev features have `default_plugin` as a backstop, so we haven't noticed that they are mostly missing (or in one case, incorrect). To better future-proof the plugins, and to provide a better example to people looking at our plugins as examples of federation, this adds a CI check to make sure that we are correctly adding it, and fixes all of the missing/incorrect values it turned up. Fixes https://github.com/flutter/flutter/issues/88222 --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/pubspec_check_command.dart | 80 ++++- .../tool/test/pubspec_check_command_test.dart | 273 +++++++++++++++--- 3 files changed, 301 insertions(+), 53 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 87917d63d3f..063ae82c386 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,7 @@ - Added Android native integration test support to `native-test`. - Added a new `android-lint` command to lint Android plugin native code. +- Pubspec validation now checks for `implements` in implementation packages. ## 0.5.0 diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 539b170dbea..58aeca1447a 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -7,6 +7,7 @@ import 'package:git/git.dart'; import 'package:platform/platform.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; +import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; @@ -65,8 +66,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { @override Future runForPackage(Directory package) async { final File pubspec = package.childFile('pubspec.yaml'); - final bool passesCheck = !pubspec.existsSync() || - await _checkPubspec(pubspec, packageName: package.basename); + final bool passesCheck = + !pubspec.existsSync() || await _checkPubspec(pubspec, package: package); if (!passesCheck) { return PackageResult.fail(); } @@ -75,7 +76,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { Future _checkPubspec( File pubspecFile, { - required String packageName, + required Directory package, }) async { final String contents = pubspecFile.readAsStringSync(); final Pubspec? pubspec = _tryParsePubspec(contents); @@ -84,34 +85,43 @@ class PubspecCheckCommand extends PackageLoopingCommand { } final List pubspecLines = contents.split('\n'); - final List sectionOrder = pubspecLines.contains(' plugin:') - ? _majorPluginSections - : _majorPackageSections; + final bool isPlugin = pubspec.flutter?.containsKey('plugin') ?? false; + final List sectionOrder = + isPlugin ? _majorPluginSections : _majorPackageSections; bool passing = _checkSectionOrder(pubspecLines, sectionOrder); if (!passing) { - print('${indentation}Major sections should follow standard ' + printError('${indentation}Major sections should follow standard ' 'repository ordering:'); final String listIndentation = indentation * 2; - print('$listIndentation${sectionOrder.join('\n$listIndentation')}'); + printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); } if (pubspec.publishTo != 'none') { final List repositoryErrors = - _checkForRepositoryLinkErrors(pubspec, packageName: packageName); + _checkForRepositoryLinkErrors(pubspec, packageName: package.basename); if (repositoryErrors.isNotEmpty) { for (final String error in repositoryErrors) { - print('$indentation$error'); + printError('$indentation$error'); } passing = false; } if (!_checkIssueLink(pubspec)) { - print( + printError( '${indentation}A package should have an "issue_tracker" link to a ' 'search for open flutter/flutter bugs with the relevant label:\n' '${indentation * 2}$_expectedIssueLinkFormat'); passing = false; } + + if (isPlugin) { + final String? error = + _checkForImplementsError(pubspec, package: package); + if (error != null) { + printError('$indentation$error'); + passing = false; + } + } } return passing; @@ -168,4 +178,52 @@ class PubspecCheckCommand extends PackageLoopingCommand { .startsWith(_expectedIssueLinkFormat) == true; } + + // Validates the "implements" keyword for a plugin, returning an error + // string if there are any issues. + // + // Should only be called on plugin packages. + String? _checkForImplementsError( + Pubspec pubspec, { + required Directory package, + }) { + if (_isImplementationPackage(package)) { + final String? implements = + pubspec.flutter!['plugin']!['implements'] as String?; + final String expectedImplements = package.parent.basename; + if (implements == null) { + return 'Missing "implements: $expectedImplements" in "plugin" section.'; + } else if (implements != expectedImplements) { + return 'Expecetd "implements: $expectedImplements"; ' + 'found "implements: $implements".'; + } + } + return null; + } + + // Returns true if [packageName] appears to be an implementation package + // according to repository conventions. + bool _isImplementationPackage(Directory package) { + // An implementation package should be in a group folder... + final Directory parentDir = package.parent; + if (parentDir.path == packagesDir.path) { + return false; + } + final String packageName = package.basename; + final String parentName = parentDir.basename; + // ... whose name is a prefix of the package name. + if (!packageName.startsWith(parentName)) { + return false; + } + // A few known package names are not implementation packages; assume + // anything else is. (This is done instead of listing known implementation + // suffixes to allow for non-standard suffixes; e.g., to put several + // platforms in one package for code-sharing purposes.) + const Set nonImplementationSuffixes = { + '', // App-facing package. + '_platform_interface', // Platform interface package. + }; + final String suffix = packageName.substring(parentName.length); + return !nonImplementationSuffixes.contains(suffix); + } } diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 177ed7f25b4..a038e0c58fb 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -66,9 +66,13 @@ environment: '''; } - String flutterSection({bool isPlugin = false}) { - const String pluginEntry = ''' + String flutterSection({ + bool isPlugin = false, + String? implementedPackage, + }) { + final String pluginEntry = ''' plugin: +${implementedPackage == null ? '' : ' implements: $implementedPackage'} platforms: '''; return ''' @@ -177,12 +181,19 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found a "homepage" entry; only "repository" should be used.'), + ]), ); }); @@ -197,12 +208,18 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing "repository"'), + ]), ); }); @@ -217,12 +234,19 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found a "homepage" entry; only "repository" should be used.'), + ]), ); }); @@ -237,12 +261,18 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('A package should have an "issue_tracker" link'), + ]), ); }); @@ -257,12 +287,19 @@ ${devDependenciesSection()} ${environmentSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), ); }); @@ -277,12 +314,19 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), ); }); @@ -297,12 +341,19 @@ ${devDependenciesSection()} ${dependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), ); }); @@ -317,12 +368,150 @@ ${flutterSection(isPlugin: true)} ${dependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); - await expectLater( - result, - throwsA(isA()), + test('fails when an implemenation package is missing "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing "implements: plugin_a" in "plugin" section.'), + ]), + ); + }); + + test('fails when an implemenation package has the wrong "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Expecetd "implements: plugin_a"; ' + 'found "implements: plugin_a_foo".'), + ]), + ); + }); + + test('passes for a correct implemenation package', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a_foo...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for an app-facing package without "implements"', () async { + final Directory pluginDirectory = + createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a/plugin_a...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for a platform interface package without "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_platform_interface', + packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_platform_interface', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a_platform_interface...'), + contains('No issues found!'), + ]), ); }); }); From 5bbc1791cce807a675e51cdb388729528fa3a76e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 20 Aug 2021 11:50:56 -0700 Subject: [PATCH 112/249] [flutter_plugin_tools] Improve 'repository' check (#4244) Ensures that the full relative path in the 'repository' link is correct, not just the last segment. This ensure that path-level errors (e.g., linking to the group directory rather than the package itself for app-facing packages) are caught. Also fixes the errors that this improved check catches, including several cases where a previously unfederated package wasn't fixed when it was moved to a subdirectory. --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/pubspec_check_command.dart | 14 +++-- .../tool/test/pubspec_check_command_test.dart | 61 +++++++++++++++++-- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 063ae82c386..1881d1bb668 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -3,6 +3,7 @@ - Added Android native integration test support to `native-test`. - Added a new `android-lint` command to lint Android plugin native code. - Pubspec validation now checks for `implements` in implementation packages. +- Pubspec valitation now checks the full relative path of `repository` entries. ## 0.5.0 diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 58aeca1447a..0a066ab72ba 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -98,7 +98,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { if (pubspec.publishTo != 'none') { final List repositoryErrors = - _checkForRepositoryLinkErrors(pubspec, packageName: package.basename); + _checkForRepositoryLinkErrors(pubspec, package: package); if (repositoryErrors.isNotEmpty) { for (final String error in repositoryErrors) { printError('$indentation$error'); @@ -154,14 +154,18 @@ class PubspecCheckCommand extends PackageLoopingCommand { List _checkForRepositoryLinkErrors( Pubspec pubspec, { - required String packageName, + required Directory package, }) { final List errorMessages = []; if (pubspec.repository == null) { errorMessages.add('Missing "repository"'); - } else if (!pubspec.repository!.path.endsWith(packageName)) { - errorMessages - .add('The "repository" link should end with the package name.'); + } else { + final String relativePackagePath = + path.relative(package.path, from: packagesDir.parent.path); + if (!pubspec.repository!.path.endsWith(relativePackagePath)) { + errorMessages + .add('The "repository" link should end with the package path.'); + } } if (pubspec.homepage != null) { diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index a038e0c58fb..833f7b601e5 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -37,15 +37,29 @@ void main() { runner.addCommand(command); }); + /// Returns the top section of a pubspec.yaml for a package named [name], + /// for either a flutter/packages or flutter/plugins package depending on + /// the values of [isPlugin]. + /// + /// By default it will create a header that includes all of the expected + /// values, elements can be changed via arguments to create incorrect + /// entries. + /// + /// If [includeRepository] is true, by default the path in the link will + /// be "packages/[name]"; a different "packages"-relative path can be + /// provided with [repositoryPackagesDirRelativePath]. String headerSection( String name, { bool isPlugin = false, bool includeRepository = true, + String? repositoryPackagesDirRelativePath, bool includeHomepage = false, bool includeIssueTracker = true, }) { + final String repositoryPath = repositoryPackagesDirRelativePath ?? name; final String repoLink = 'https://github.com/flutter/' - '${isPlugin ? 'plugins' : 'packages'}/tree/master/packages/$name'; + '${isPlugin ? 'plugins' : 'packages'}/tree/master/' + 'packages/$repositoryPath'; final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; @@ -250,6 +264,32 @@ ${devDependenciesSection()} ); }); + test('fails when repository is incorrect', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The "repository" link should end with the package path.'), + ]), + ); + }); + test('fails when issue tracker is missing', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); @@ -446,7 +486,11 @@ ${devDependenciesSection()} 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true)} +${headerSection( + 'plugin_a_foo', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo', + )} ${environmentSection()} ${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} ${dependenciesSection()} @@ -470,7 +514,11 @@ ${devDependenciesSection()} createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a', isPlugin: true)} +${headerSection( + 'plugin_a', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', + )} ${environmentSection()} ${flutterSection(isPlugin: true)} ${dependenciesSection()} @@ -496,7 +544,12 @@ ${devDependenciesSection()} packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_platform_interface', isPlugin: true)} +${headerSection( + 'plugin_a_platform_interface', + isPlugin: true, + repositoryPackagesDirRelativePath: + 'plugin_a/plugin_a_platform_interface', + )} ${environmentSection()} ${flutterSection(isPlugin: true)} ${dependenciesSection()} From 74cf0287f92ec7caa3256b4412a1704c197a607d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Aug 2021 14:42:21 -0400 Subject: [PATCH 113/249] [flutter_plugin_tools] Improve process mocking (#4254) The mock process runner used in most of the tests had poor handling of stdout/stderr: - By default it would return the `List` output from the mock process, which was never correct since the parent process runner interface always sets encodings, thus the `dynamic` should always be of type `String` - The process for setting output on a `MockProcess` was awkward, since it was based on a `Stream>`, even though in every case what we actually want to do is just set the full output to a string. - A hack was at some point added (presumably due to the above issues) to bypass that flow at the process runner level, and instead return a `String` set there. That meant there were two ways of setting output (one of which that only worked for one of the ways of running processes) - That hack wasn't updated when the ability to return multiple mock processes instead of a single global mock process was added, so the API was even more confusing, and there was no way to set different output for different processes. This changes the test APIs so that: - `MockProcess` takes stdout and stderr as strings, and internally manages converting them to a `Stream>`. - `RecordingProcessRunner` correctly decodes and returns the output streams when constructing a process result. It also removes the resultStdout and resultStderr hacks, as well as the legacy `processToReturn` API, and converts all uses to the new structure, which is both simpler to use, and clearly associates output with specific processes. --- script/tool/test/analyze_command_test.dart | 4 +- .../test/build_examples_command_test.dart | 2 +- script/tool/test/common/gradle_test.dart | 2 +- script/tool/test/common/xcode_test.dart | 40 +++++--- .../test/drive_examples_command_test.dart | 10 +- .../test/firebase_test_lab_command_test.dart | 22 ++--- script/tool/test/format_command_test.dart | 30 +++--- .../tool/test/lint_android_command_test.dart | 2 +- .../tool/test/lint_podspecs_command_test.dart | 14 ++- script/tool/test/mocks.dart | 43 +++++--- .../tool/test/native_test_command_test.dart | 60 ++++++++---- .../tool/test/publish_check_command_test.dart | 98 ++++++++----------- .../test/publish_plugin_command_test.dart | 19 ++-- script/tool/test/test_command_test.dart | 10 +- script/tool/test/util.dart | 28 +++--- .../tool/test/xcode_analyze_command_test.dart | 4 +- 16 files changed, 197 insertions(+), 191 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index da2f0aba86c..502fa9a0634 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -211,7 +211,7 @@ void main() { createFakePlugin('foo', packagesDir); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess.failing() // flutter packages get + MockProcess(exitCode: 1) // flutter packages get ]; Error? commandError; @@ -233,7 +233,7 @@ void main() { createFakePlugin('foo', packagesDir); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess.failing() // dart analyze + MockProcess(exitCode: 1) // dart analyze ]; Error? commandError; diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 27489a50228..9c7291c31dd 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -63,7 +63,7 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess.failing() // flutter packages get + MockProcess(exitCode: 1) // flutter packages get ]; Error? commandError; diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart index c24887d3d46..3eac60baf3c 100644 --- a/script/tool/test/common/gradle_test.dart +++ b/script/tool/test/common/gradle_test.dart @@ -168,7 +168,7 @@ void main() { processRunner.mockProcessesForExecutable[project.gradleWrapper.path] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; final int exitCode = await project.runCommand('foo'); diff --git a/script/tool/test/common/xcode_test.dart b/script/tool/test/common/xcode_test.dart index 7e046a2446c..259d8ea36cd 100644 --- a/script/tool/test/common/xcode_test.dart +++ b/script/tool/test/common/xcode_test.dart @@ -94,8 +94,9 @@ void main() { } }; - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(devices); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(devices)), + ]; expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId); }); @@ -137,15 +138,16 @@ void main() { } }; - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(devices); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(devices)), + ]; expect(await xcode.findBestAvailableIphoneSimulator(), null); }); test('returns null if simctl fails', () async { processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; expect(await xcode.findBestAvailableIphoneSimulator(), null); @@ -216,7 +218,7 @@ void main() { test('returns error codes', () async { processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; final Directory directory = const LocalFileSystem().currentDirectory; @@ -247,8 +249,7 @@ void main() { group('projectHasTarget', () { test('returns true when present', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = ''' + const String stdout = ''' { "project" : { "configurations" : [ @@ -266,6 +267,9 @@ void main() { ] } }'''; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: stdout), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -287,8 +291,7 @@ void main() { }); test('returns false when not present', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = ''' + const String stdout = ''' { "project" : { "configurations" : [ @@ -305,6 +308,9 @@ void main() { ] } }'''; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: stdout), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -326,8 +332,9 @@ void main() { }); test('returns null for unexpected output', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = '{}'; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: '{}'), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -349,8 +356,9 @@ void main() { }); test('returns null for invalid output', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = ':)'; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: ':)'), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -372,7 +380,9 @@ void main() { }); test('returns null for failure', () async { - processRunner.processToReturn = MockProcess.failing(); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), // xcodebuild -list + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index c6893181e28..bbf865d3edf 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -60,12 +60,10 @@ void main() { final String output = '''${includeBanner ? updateBanner : ''}[${devices.join(',')}]'''; - final MockProcess mockDevicesProcess = MockProcess.succeeding(); - mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures + final MockProcess mockDevicesProcess = MockProcess(stdout: output); processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [mockDevicesProcess]; - processRunner.resultStdout = output; } test('fails if no platforms are provided', () async { @@ -151,7 +149,7 @@ void main() { // Simulate failure from `flutter devices`. processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess.failing()]; + [MockProcess(exitCode: 1)]; Error? commandError; final List output = await runCapturingPrint( @@ -954,8 +952,8 @@ void main() { .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ // No mock for 'devices', since it's running for macOS. - MockProcess.failing(), // 'drive' #1 - MockProcess.failing(), // 'drive' #2 + MockProcess(exitCode: 1), // 'drive' #1 + MockProcess(exitCode: 1), // 'drive' #2 ]; Error? commandError; diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 35697af3f5f..7716990b323 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -40,7 +40,7 @@ void main() { test('fails if gcloud auth fails', () async { processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -64,8 +64,8 @@ void main() { test('retries gcloud set', () async { processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess.succeeding(), // auth - MockProcess.failing(), // config + MockProcess(), // auth + MockProcess(exitCode: 1), // config ]; createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -245,10 +245,10 @@ void main() { ]); processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess.succeeding(), // auth - MockProcess.succeeding(), // config - MockProcess.failing(), // integration test #1 - MockProcess.succeeding(), // integration test #2 + MockProcess(), // auth + MockProcess(), // config + MockProcess(exitCode: 1), // integration test #1 + MockProcess(), // integration test #2 ]; Error? commandError; @@ -459,7 +459,7 @@ void main() { ]); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess.failing() // flutter build + MockProcess(exitCode: 1) // flutter build ]; Error? commandError; @@ -496,7 +496,7 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -533,8 +533,8 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.succeeding(), // assembleAndroidTest - MockProcess.failing(), // assembleDebug + MockProcess(), // assembleAndroidTest + MockProcess(exitCode: 1), // assembleDebug ]; Error? commandError; diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index b072e5d30aa..cf57a9d0dcf 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -115,7 +115,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess.failing()]; + [MockProcess(exitCode: 1)]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { @@ -167,7 +167,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['java'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; final List output = await runCapturingPrint( @@ -193,8 +193,8 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['java'] = [ - MockProcess.succeeding(), // check for working java - MockProcess.failing(), // format + MockProcess(), // check for working java + MockProcess(exitCode: 1), // format ]; Error? commandError; final List output = await runCapturingPrint( @@ -280,7 +280,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; final List output = await runCapturingPrint( @@ -335,8 +335,8 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess.succeeding(), // check for working clang-format - MockProcess.failing(), // format + MockProcess(), // check for working clang-format + MockProcess(exitCode: 1), // format ]; Error? commandError; final List output = await runCapturingPrint( @@ -418,11 +418,11 @@ void main() { ]; createFakePlugin('a_plugin', packagesDir, extraFiles: files); + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; processRunner.mockProcessesForExecutable['git'] = [ - MockProcess.succeeding(), + MockProcess(stdout: changedFilePath), ]; - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.resultStdout = changedFilePath; + Error? commandError; final List output = await runCapturingPrint(runner, ['format', '--fail-on-change'], @@ -448,7 +448,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['git'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; final List output = @@ -472,12 +472,12 @@ void main() { ]; createFakePlugin('a_plugin', packagesDir, extraFiles: files); + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; processRunner.mockProcessesForExecutable['git'] = [ - MockProcess.succeeding(), // ls-files - MockProcess.failing(), // diff + MockProcess(stdout: changedFilePath), // ls-files + MockProcess(exitCode: 1), // diff ]; - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.resultStdout = changedFilePath; + Error? commandError; final List output = await runCapturingPrint(runner, ['format', '--fail-on-change'], diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index 05ead220c15..d0805846863 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -101,7 +101,7 @@ void main() { }); processRunner.mockProcessesForExecutable['gradlew'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; Error? commandError; diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 51a4e626777..44247274028 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -75,11 +75,9 @@ void main() { ); processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess.succeeding(), - MockProcess.succeeding(), + MockProcess(stdout: 'Foo', stderr: 'Bar'), + MockProcess(), ]; - processRunner.resultStdout = 'Foo'; - processRunner.resultStderr = 'Bar'; final List output = await runCapturingPrint(runner, ['podspecs']); @@ -173,7 +171,7 @@ void main() { // Simulate failure from `which pod`. processRunner.mockProcessesForExecutable['which'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; Error? commandError; @@ -199,7 +197,7 @@ void main() { // Simulate failure from `pod`. processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; Error? commandError; @@ -227,8 +225,8 @@ void main() { // Simulate failure from the second call to `pod`. processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess.succeeding(), - MockProcess.failing(), + MockProcess(), + MockProcess(exitCode: 1), ]; Error? commandError; diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 0dcdedd3db0..3d0aef1b397 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; @@ -32,22 +33,32 @@ class MockPlatform extends Mock implements Platform { } class MockProcess extends Mock implements io.Process { - MockProcess(); - - /// A mock process that terminates with exitCode 0. - MockProcess.succeeding() { - exitCodeCompleter.complete(0); - } - - /// A mock process that terminates with exitCode 1. - MockProcess.failing() { - exitCodeCompleter.complete(1); + /// Creates a mock process with the given results. + /// + /// The default encodings match the ProcessRunner defaults; mocks for + /// processes run with a different encoding will need to be created with + /// the matching encoding. + MockProcess({ + int exitCode = 0, + String? stdout, + String? stderr, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding, + }) : _exitCode = exitCode { + if (stdout != null) { + _stdoutController.add(stdoutEncoding.encoder.convert(stdout)); + } + if (stderr != null) { + _stderrController.add(stderrEncoding.encoder.convert(stderr)); + } + _stdoutController.close(); + _stderrController.close(); } - final Completer exitCodeCompleter = Completer(); - final StreamController> stdoutController = + final int _exitCode; + final StreamController> _stdoutController = StreamController>(); - final StreamController> stderrController = + final StreamController> _stderrController = StreamController>(); final MockIOSink stdinMock = MockIOSink(); @@ -55,13 +66,13 @@ class MockProcess extends Mock implements io.Process { int get pid => 99; @override - Future get exitCode => exitCodeCompleter.future; + Future get exitCode async => _exitCode; @override - Stream> get stdout => stdoutController.stream; + Stream> get stdout => _stdoutController.stream; @override - Stream> get stderr => stderrController.stream; + Stream> get stderr => _stderrController.stream; @override IOSink get stdin => stdinMock; diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 59ca17b25c0..f367dc80182 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -122,11 +122,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - // Exit code 66 from testing indicates no tests. - final MockProcess noTestsProcessResult = MockProcess(); - noTestsProcessResult.exitCodeCompleter.complete(66); processRunner.mockProcessesForExecutable['xcrun'] = [ - noTestsProcessResult, + // Exit code 66 from testing indicates no tests. + MockProcess(exitCode: 66), ]; final List output = await runCapturingPrint(runner, ['native-test', '--macos']); @@ -239,12 +237,13 @@ void main() { 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); - final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(_kDeviceListMap); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl + ]; + await runCapturingPrint(runner, ['native-test', '--ios']); expect( @@ -673,7 +672,7 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -745,7 +744,7 @@ void main() { }); processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -775,9 +774,14 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + const Map projects = { + 'project': { + 'targets': ['RunnerTests', 'RunnerUITests'] + } + }; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + ]; final List output = await runCapturingPrint(runner, [ 'native-test', @@ -835,9 +839,14 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + const Map projects = { + 'project': { + 'targets': ['RunnerTests', 'RunnerUITests'] + } + }; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + ]; final List output = await runCapturingPrint(runner, [ 'native-test', @@ -895,9 +904,16 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); // Simulate a project with unit tests but no integration tests... - processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; + const Map projects = { + 'project': { + 'targets': ['RunnerTests'] + } + }; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + ]; + // ... then try to run only integration tests. final List output = await runCapturingPrint(runner, [ 'native-test', @@ -941,7 +957,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.failing(); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), // xcodebuild -list + ]; Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -1192,7 +1210,7 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -1243,11 +1261,11 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; // Simulate failing Android. processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 11de9f09548..65b0cb54547 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.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:collection'; import 'dart:convert'; import 'dart:io' as io; @@ -19,18 +18,18 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$PublishCheckProcessRunner tests', () { + group('$PublishCheckCommand tests', () { FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; - late PublishCheckProcessRunner processRunner; + late RecordingProcessRunner processRunner; late CommandRunner runner; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = PublishCheckProcessRunner(); + processRunner = RecordingProcessRunner(); final PublishCheckCommand publishCheckCommand = PublishCheckCommand( packagesDir, processRunner: processRunner, @@ -50,12 +49,11 @@ void main() { final Directory plugin2Dir = createFakePlugin('plugin_tools_test_package_b', packagesDir); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + MockProcess(), + ]; + await runCapturingPrint(runner, ['publish-check']); expect( @@ -75,11 +73,9 @@ void main() { test('fail on negative test', () async { createFakePlugin('plugin_tools_test_package_a', packagesDir); - final MockProcess process = MockProcess.failing(); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(exitCode: 1) + ]; expect( () => runCapturingPrint(runner, ['publish-check']), @@ -91,8 +87,9 @@ void main() { final Directory dir = createFakePlugin('c', packagesDir); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - final MockProcess process = MockProcess(); - processRunner.processesToReturn.add(process); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + ]; expect(() => runCapturingPrint(runner, ['publish-check']), throwsA(isA())); @@ -101,15 +98,14 @@ void main() { test('pass on prerelease if --allow-pre-release flag is on', () async { createFakePlugin('d', packagesDir); - const String preReleaseOutput = 'Package has 1 warning.' - 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; - - final MockProcess process = MockProcess.failing(); - process.stdoutController.add(preReleaseOutput.codeUnits); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + final MockProcess process = MockProcess( + exitCode: 1, + stdout: 'Package has 1 warning.\n' + 'Packages with an SDK constraint on a pre-release of the Dart ' + 'SDK should themselves be published as a pre-release version.'); + processRunner.mockProcessesForExecutable['flutter'] = [ + process, + ]; expect( runCapturingPrint( @@ -120,15 +116,14 @@ void main() { test('fail on prerelease if --allow-pre-release flag is off', () async { createFakePlugin('d', packagesDir); - const String preReleaseOutput = 'Package has 1 warning.' - 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; - - final MockProcess process = MockProcess.failing(); - process.stdoutController.add(preReleaseOutput.codeUnits); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + final MockProcess process = MockProcess( + exitCode: 1, + stdout: 'Package has 1 warning.\n' + 'Packages with an SDK constraint on a pre-release of the Dart ' + 'SDK should themselves be published as a pre-release version.'); + processRunner.mockProcessesForExecutable['flutter'] = [ + process, + ]; expect(runCapturingPrint(runner, ['publish-check']), throwsA(isA())); @@ -137,14 +132,9 @@ void main() { test('Success message on stderr is not printed as an error', () async { createFakePlugin('d', packagesDir); - const String publishOutput = 'Package has 0 warnings.'; - - final MockProcess process = MockProcess.succeeding(); - process.stderrController.add(publishOutput.codeUnits); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(stdout: 'Package has 0 warnings.'), + ]; final List output = await runCapturingPrint(runner, ['publish-check']); @@ -192,9 +182,6 @@ void main() { createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -258,9 +245,9 @@ void main() { createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + ]; final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -331,9 +318,9 @@ void main() { await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + ]; bool hasError = false; final List output = await runCapturingPrint( @@ -369,10 +356,3 @@ void main() { }); }); } - -class PublishCheckProcessRunner extends RecordingProcessRunner { - final Queue processesToReturn = Queue(); - - @override - io.Process get processToReturn => processesToReturn.removeFirst(); -} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index c7df8195264..9a937daa238 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -1060,7 +1060,7 @@ class TestProcessRunner extends ProcessRunner { String? mockPublishStdout; String? mockPublishStderr; - int? mockPublishCompleteCode; + int mockPublishCompleteCode = 0; @override Future run( @@ -1097,17 +1097,14 @@ class TestProcessRunner extends ProcessRunner { args[0] == 'pub' && args[1] == 'publish'); mockPublishArgs.addAll(args); - mockPublishProcess = MockProcess(); - if (mockPublishStdout != null) { - mockPublishProcess.stdoutController.add(utf8.encode(mockPublishStdout!)); - } - if (mockPublishStderr != null) { - mockPublishProcess.stderrController.add(utf8.encode(mockPublishStderr!)); - } - if (mockPublishCompleteCode != null) { - mockPublishProcess.exitCodeCompleter.complete(mockPublishCompleteCode); - } + mockPublishProcess = MockProcess( + exitCode: mockPublishCompleteCode, + stdout: mockPublishStdout, + stderr: mockPublishStderr, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); return mockPublishProcess; } } diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 503e24d0305..3b350f7d88a 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -67,8 +67,8 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess.failing(), // plugin 1 test - MockProcess.succeeding(), // plugin 2 test + MockProcess(exitCode: 1), // plugin 1 test + MockProcess(), // plugin 2 test ]; Error? commandError; @@ -132,7 +132,7 @@ void main() { extraFiles: ['test/empty_test.dart']); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess.failing(), // dart pub get + MockProcess(exitCode: 1), // dart pub get ]; Error? commandError; @@ -156,8 +156,8 @@ void main() { extraFiles: ['test/empty_test.dart']); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess.succeeding(), // dart pub get - MockProcess.failing(), // dart pub run test + MockProcess(), // dart pub get + MockProcess(exitCode: 1), // dart pub run test ]; Error? commandError; diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 1984a25cc43..10a85f49e81 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -265,15 +265,6 @@ class RecordingProcessRunner extends ProcessRunner { final Map> mockProcessesForExecutable = >{}; - /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. - String? resultStdout; - - /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. - String? resultStderr; - - // Deprecated--do not add new uses. Use mockProcessesForExecutable instead. - io.Process? processToReturn; - @override Future runAndStream( String executable, @@ -291,8 +282,7 @@ class RecordingProcessRunner extends ProcessRunner { return Future.value(exitCode); } - /// Returns [io.ProcessResult] created from [mockProcessesForExecutable], - /// [resultStdout], and [resultStderr]. + /// Returns [io.ProcessResult] created from [mockProcessesForExecutable]. @override Future run( String executable, @@ -306,10 +296,16 @@ class RecordingProcessRunner extends ProcessRunner { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); final io.Process? process = _getProcessToReturn(executable); + final List? processStdout = + await process?.stdout.transform(stdoutEncoding.decoder).toList(); + final String stdout = processStdout?.join('') ?? ''; + final List? processStderr = + await process?.stderr.transform(stderrEncoding.decoder).toList(); + final String stderr = processStderr?.join('') ?? ''; + final io.ProcessResult result = process == null ? io.ProcessResult(1, 0, '', '') - : io.ProcessResult(process.pid, await process.exitCode, - resultStdout ?? process.stdout, resultStderr ?? process.stderr); + : io.ProcessResult(process.pid, await process.exitCode, stdout, stderr); if (exitOnError && (result.exitCode != 0)) { throw io.ProcessException(executable, args); @@ -326,13 +322,11 @@ class RecordingProcessRunner extends ProcessRunner { } io.Process? _getProcessToReturn(String executable) { - io.Process? process; final List? processes = mockProcessesForExecutable[executable]; if (processes != null && processes.isNotEmpty) { - process = mockProcessesForExecutable[executable]!.removeAt(0); + return processes.removeAt(0); } - // Fall back to `processToReturn` for backwards compatibility. - return process ?? processToReturn; + return null; } } diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index b715ac531f5..790a526a8ae 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -131,7 +131,7 @@ void main() { }); processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -228,7 +228,7 @@ void main() { }); processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; From 41f1c806f2414d8aa68e48701d78d560d4e75c5a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Aug 2021 16:29:56 -0400 Subject: [PATCH 114/249] [flutter_plugin_tools] Introduce a class for packages (#4252) Packages are the primary conceptual object in the tool, but currently they are represented simply as Directory (or occasionally a path string). This introduces an object for packages and: - moves a number of existing utility methods into it - sweeps the code for the obvious cases of using `Directory` to represent a package, especially in method signatures and migrates them - notes a few places where we should migrate later, to avoid ballooning the size of the PR There are no doubt other cases not caught in the sweep, but this gives us a foundation both for new code, and to migrate incrementally toward as we find existing code that was missed. --- script/tool/lib/src/analyze_command.dart | 14 +- .../tool/lib/src/build_examples_command.dart | 11 +- .../src/common/package_looping_command.dart | 86 +++++------- .../tool/lib/src/common/plugin_command.dart | 54 +++----- script/tool/lib/src/common/plugin_utils.dart | 42 +----- .../lib/src/common/pub_version_finder.dart | 6 +- .../lib/src/common/repository_package.dart | 78 +++++++++++ .../src/create_all_plugins_app_command.dart | 6 +- .../tool/lib/src/drive_examples_command.dart | 34 ++--- .../lib/src/firebase_test_lab_command.dart | 26 ++-- script/tool/lib/src/lint_android_command.dart | 9 +- .../tool/lib/src/lint_podspecs_command.dart | 5 +- script/tool/lib/src/list_command.dart | 18 +-- script/tool/lib/src/native_test_command.dart | 50 +++---- .../tool/lib/src/publish_check_command.dart | 18 +-- .../tool/lib/src/publish_plugin_command.dart | 14 +- .../tool/lib/src/pubspec_check_command.dart | 19 +-- script/tool/lib/src/test_command.dart | 19 +-- .../tool/lib/src/version_check_command.dart | 19 +-- .../tool/lib/src/xcode_analyze_command.dart | 13 +- .../common/package_looping_command_test.dart | 67 +--------- .../tool/test/common/plugin_command_test.dart | 10 +- .../tool/test/common/plugin_utils_test.dart | 21 +-- .../test/common/pub_version_finder_test.dart | 6 +- .../test/common/repository_package_test.dart | 123 ++++++++++++++++++ script/tool/test/format_command_test.dart | 4 +- script/tool/test/util.dart | 2 + 27 files changed, 440 insertions(+), 334 deletions(-) create mode 100644 script/tool/lib/src/common/repository_package.dart create mode 100644 script/tool/test/common/repository_package_test.dart diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 2b728e2b907..faad7f4736e 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -5,13 +5,14 @@ import 'dart:async'; import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; import 'package:platform/platform.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; +import 'common/plugin_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitPackagesGetFailed = 3; @@ -55,8 +56,9 @@ class AnalyzeCommand extends PackageLoopingCommand { final bool hasLongOutput = false; /// Checks that there are no unexpected analysis_options.yaml files. - bool _hasUnexpecetdAnalysisOptions(Directory package) { - final List files = package.listSync(recursive: true); + bool _hasUnexpecetdAnalysisOptions(RepositoryPackage package) { + final List files = + package.directory.listSync(recursive: true); for (final FileSystemEntity file in files) { if (file.basename != 'analysis_options.yaml' && file.basename != '.analysis_options') { @@ -87,7 +89,7 @@ class AnalyzeCommand extends PackageLoopingCommand { Future _runPackagesGetOnTargetPackages() async { final List packageDirectories = await getTargetPackagesAndSubpackages() - .map((PackageEnumerationEntry package) => package.directory) + .map((PackageEnumerationEntry entry) => entry.package.directory) .toList(); final Set packagePaths = packageDirectories.map((Directory dir) => dir.path).toSet(); @@ -135,13 +137,13 @@ class AnalyzeCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { if (_hasUnexpecetdAnalysisOptions(package)) { return PackageResult.fail(['Unexpected local analysis options']); } final int exitCode = await processRunner.runAndStream( _dartBinaryPath, ['analyze', '--fatal-infos'], - workingDir: package); + workingDir: package.directory); if (exitCode != 0) { return PackageResult.fail(); } diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 0cac09980c9..ac5e84b7c3c 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -11,6 +11,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// Key for APK. const String _platformFlagApk = 'apk'; @@ -96,7 +97,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final List errors = []; final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries @@ -126,9 +127,9 @@ class BuildExamplesCommand extends PackageLoopingCommand { } print(''); - for (final Directory example in getExamplesForPlugin(package)) { + for (final RepositoryPackage example in package.getExamples()) { final String packageName = - getRelativePosixPath(example, from: packagesDir); + getRelativePosixPath(example.directory, from: packagesDir); for (final _PlatformDetails platform in buildPlatforms) { String buildPlatform = platform.label; @@ -149,7 +150,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { } Future _buildExample( - Directory example, + RepositoryPackage example, String flutterBuildType, { List extraBuildFlags = const [], }) async { @@ -164,7 +165,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], - workingDir: example, + workingDir: example.directory, ); return exitCode == 0; } diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 0e0976ecc6a..00caeb30ef4 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -13,6 +13,7 @@ import 'package:platform/platform.dart'; import 'core.dart'; import 'plugin_command.dart'; import 'process_runner.dart'; +import 'repository_package.dart'; /// Possible outcomes of a command run for a package. enum RunState { @@ -84,7 +85,7 @@ abstract class PackageLoopingCommand extends PluginCommand { int _otherWarningCount = 0; /// The package currently being run by [runForPackage]. - PackageEnumerationEntry? _currentPackage; + PackageEnumerationEntry? _currentPackageEntry; /// Called during [run] before any calls to [runForPackage]. This provides an /// opportunity to fail early if the command can't be run (e.g., because the @@ -97,7 +98,7 @@ abstract class PackageLoopingCommand extends PluginCommand { /// be included in the final error summary (e.g., a command that only has a /// single failure mode), or strings that should be listed for that package /// in the final summary. An empty list indicates success. - Future runForPackage(Directory package); + Future runForPackage(RepositoryPackage package); /// Called during [run] after all calls to [runForPackage]. This provides an /// opportunity to do any cleanup of run-level state. @@ -155,31 +156,13 @@ abstract class PackageLoopingCommand extends PluginCommand { /// things that might be useful to someone debugging an unexpected result. void logWarning(String warningMessage) { print(Colorize(warningMessage)..yellow()); - if (_currentPackage != null) { - _packagesWithWarnings.add(_currentPackage!); + if (_currentPackageEntry != null) { + _packagesWithWarnings.add(_currentPackageEntry!); } else { ++_otherWarningCount; } } - /// Returns the identifying name to use for [package]. - /// - /// Implementations should not expect a specific format for this string, since - /// it uses heuristics to try to be precise without being overly verbose. If - /// an exact format (e.g., published name, or basename) is required, that - /// should be used instead. - String getPackageDescription(Directory package) { - String packageName = getRelativePosixPath(package, from: packagesDir); - final List components = p.posix.split(packageName); - // For the common federated plugin pattern of `foo/foo_subpackage`, drop - // the first part since it's not useful. - if (components.length >= 2 && - components[1].startsWith('${components[0]}_')) { - packageName = p.posix.joinAll(components.sublist(1)); - } - return packageName; - } - /// Returns the relative path from [from] to [entity] in Posix style. /// /// This should be used when, for example, printing package-relative paths in @@ -219,36 +202,36 @@ abstract class PackageLoopingCommand extends PluginCommand { Future _runInternal() async { _packagesWithWarnings.clear(); _otherWarningCount = 0; - _currentPackage = null; + _currentPackageEntry = null; await initializeRun(); - final List packages = includeSubpackages + final List targetPackages = includeSubpackages ? await getTargetPackagesAndSubpackages(filterExcluded: false).toList() : await getTargetPackages(filterExcluded: false).toList(); final Map results = {}; - for (final PackageEnumerationEntry package in packages) { - _currentPackage = package; - _printPackageHeading(package); + for (final PackageEnumerationEntry entry in targetPackages) { + _currentPackageEntry = entry; + _printPackageHeading(entry); // Command implementations should never see excluded packages; they are // included at this level only for logging. - if (package.excluded) { - results[package] = PackageResult.exclude(); + if (entry.excluded) { + results[entry] = PackageResult.exclude(); continue; } - final PackageResult result = await runForPackage(package.directory); + final PackageResult result = await runForPackage(entry.package); if (result.state == RunState.skipped) { final String message = '${indentation}SKIPPING: ${result.details.first}'; captureOutput ? print(message) : print(Colorize(message)..darkGray()); } - results[package] = result; + results[entry] = result; } - _currentPackage = null; + _currentPackageEntry = null; completeRun(); @@ -256,13 +239,13 @@ abstract class PackageLoopingCommand extends PluginCommand { // If there were any errors reported, summarize them and exit. if (results.values .any((PackageResult result) => result.state == RunState.failed)) { - _printFailureSummary(packages, results); + _printFailureSummary(targetPackages, results); return false; } // Otherwise, print a summary of what ran for ease of auditing that all the // expected tests ran. - _printRunSummary(packages, results); + _printRunSummary(targetPackages, results); print('\n'); _printSuccess('No issues found!'); @@ -283,9 +266,9 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Something is always printed to make it easier to distinguish between /// a command running for a package and producing no output, and a command /// not having been run for a package. - void _printPackageHeading(PackageEnumerationEntry package) { - final String packageDisplayName = getPackageDescription(package.directory); - String heading = package.excluded + void _printPackageHeading(PackageEnumerationEntry entry) { + final String packageDisplayName = entry.package.displayName; + String heading = entry.excluded ? 'Not running for $packageDisplayName; excluded' : 'Running for $packageDisplayName'; if (hasLongOutput) { @@ -295,16 +278,15 @@ abstract class PackageLoopingCommand extends PluginCommand { || $heading ============================================================ '''; - } else if (!package.excluded) { + } else if (!entry.excluded) { heading = '$heading...'; } if (captureOutput) { print(heading); } else { final Colorize colorizeHeading = Colorize(heading); - print(package.excluded - ? colorizeHeading.darkGray() - : colorizeHeading.cyan()); + print( + entry.excluded ? colorizeHeading.darkGray() : colorizeHeading.cyan()); } } @@ -349,17 +331,18 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Prints a one-line-per-package overview of the run results for each /// package. - void _printPerPackageRunOverview(List packages, + void _printPerPackageRunOverview( + List packageEnumeration, {required Set skipped}) { print('Run overview:'); - for (final PackageEnumerationEntry package in packages) { - final bool hadWarning = _packagesWithWarnings.contains(package); + for (final PackageEnumerationEntry entry in packageEnumeration) { + final bool hadWarning = _packagesWithWarnings.contains(entry); Styles style; String summary; - if (package.excluded) { + if (entry.excluded) { summary = 'excluded'; style = Styles.DARK_GRAY; - } else if (skipped.contains(package)) { + } else if (skipped.contains(entry)) { summary = 'skipped'; style = hadWarning ? Styles.LIGHT_YELLOW : Styles.DARK_GRAY; } else { @@ -373,18 +356,18 @@ abstract class PackageLoopingCommand extends PluginCommand { if (!captureOutput) { summary = (Colorize(summary)..apply(style)).toString(); } - print(' ${getPackageDescription(package.directory)} - $summary'); + print(' ${entry.package.displayName} - $summary'); } print(''); } /// Prints a summary of all of the failures from [results]. - void _printFailureSummary(List packages, + void _printFailureSummary(List packageEnumeration, Map results) { const String indentation = ' '; _printError(failureListHeader); - for (final PackageEnumerationEntry package in packages) { - final PackageResult result = results[package]!; + for (final PackageEnumerationEntry entry in packageEnumeration) { + final PackageResult result = results[entry]!; if (result.state == RunState.failed) { final String errorIndentation = indentation * 2; String errorDetails = ''; @@ -392,8 +375,7 @@ abstract class PackageLoopingCommand extends PluginCommand { errorDetails = ':\n$errorIndentation${result.details.join('\n$errorIndentation')}'; } - _printError( - '$indentation${getPackageDescription(package.directory)}$errorDetails'); + _printError('$indentation${entry.package.displayName}$errorDetails'); } } _printError(failureListFooter); diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 10f42336087..ec51261ab61 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -14,15 +14,18 @@ import 'package:yaml/yaml.dart'; import 'core.dart'; import 'git_version_finder.dart'; import 'process_runner.dart'; +import 'repository_package.dart'; /// An entry in package enumeration for APIs that need to include extra /// data about the entry. class PackageEnumerationEntry { - /// Creates a new entry for the given package directory. - PackageEnumerationEntry(this.directory, {required this.excluded}); + /// Creates a new entry for the given package. + PackageEnumerationEntry(this.package, {required this.excluded}); - /// The package's location. - final Directory directory; + /// The package this entry corresponds to. Be sure to check `excluded` before + /// using this, as having an entry does not necessarily mean that the package + /// should be included in the processing of the enumeration. + final RepositoryPackage package; /// Whether or not this package was excluded by the command invocation. final bool excluded; @@ -225,7 +228,7 @@ abstract class PluginCommand extends Command { final List allPlugins = await _getAllPackages().toList(); allPlugins.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => - p1.directory.path.compareTo(p2.directory.path)); + p1.package.path.compareTo(p2.package.path)); final int shardSize = allPlugins.length ~/ shardCount + (allPlugins.length % shardCount == 0 ? 0 : 1); final int start = min(shardIndex * shardSize, allPlugins.length); @@ -287,7 +290,8 @@ abstract class PluginCommand extends Command { // A top-level Dart package is a plugin package. if (_isDartPackage(entity)) { if (plugins.isEmpty || plugins.contains(p.basename(entity.path))) { - yield PackageEnumerationEntry(entity as Directory, + yield PackageEnumerationEntry( + RepositoryPackage(entity as Directory), excluded: excludedPluginNames.contains(entity.basename)); } } else if (entity is Directory) { @@ -305,7 +309,8 @@ abstract class PluginCommand extends Command { if (plugins.isEmpty || plugins.contains(relativePath) || plugins.contains(basenamePath)) { - yield PackageEnumerationEntry(subdir as Directory, + yield PackageEnumerationEntry( + RepositoryPackage(subdir as Directory), excluded: excludedPluginNames.contains(basenamePath) || excludedPluginNames.contains(packageName) || excludedPluginNames.contains(relativePath)); @@ -327,26 +332,26 @@ abstract class PluginCommand extends Command { await for (final PackageEnumerationEntry plugin in getTargetPackages(filterExcluded: filterExcluded)) { yield plugin; - yield* plugin.directory + yield* plugin.package.directory .list(recursive: true, followLinks: false) .where(_isDartPackage) .map((FileSystemEntity directory) => PackageEnumerationEntry( - directory as Directory, // _isDartPackage guarantees this works. + // _isDartPackage guarantees that this cast is valid. + RepositoryPackage(directory as Directory), excluded: plugin.excluded)); } } - /// Returns the files contained, recursively, within the plugins + /// Returns the files contained, recursively, within the packages /// involved in this command execution. Stream getFiles() { - return getTargetPackages() - .map((PackageEnumerationEntry entry) => entry.directory) - .asyncExpand((Directory folder) => getFilesForPackage(folder)); + return getTargetPackages().asyncExpand( + (PackageEnumerationEntry entry) => getFilesForPackage(entry.package)); } /// Returns the files contained, recursively, within [package]. - Stream getFilesForPackage(Directory package) { - return package + Stream getFilesForPackage(RepositoryPackage package) { + return package.directory .list(recursive: true, followLinks: false) .where((FileSystemEntity entity) => entity is File) .cast(); @@ -358,25 +363,6 @@ abstract class PluginCommand extends Command { 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. diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 0277b78d566..d9c42e220c0 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:yaml/yaml.dart'; import 'core.dart'; @@ -16,7 +17,7 @@ enum PlatformSupport { federated, } -/// Returns whether the given directory contains a Flutter [platform] plugin. +/// Returns whether the given [package] is a Flutter [platform] plugin. /// /// It checks this by looking for the following pattern in the pubspec: /// @@ -27,7 +28,7 @@ enum PlatformSupport { /// /// If [requiredMode] is provided, the plugin must have the given type of /// implementation in order to return true. -bool pluginSupportsPlatform(String platform, FileSystemEntity entity, +bool pluginSupportsPlatform(String platform, RepositoryPackage package, {PlatformSupport? requiredMode}) { assert(platform == kPlatformIos || platform == kPlatformAndroid || @@ -35,14 +36,9 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity, platform == kPlatformMacos || platform == kPlatformWindows || platform == kPlatformLinux); - if (entity is! Directory) { - return false; - } - try { - final File pubspecFile = entity.childFile('pubspec.yaml'); final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + loadYaml(package.pubspecFile.readAsStringSync()) as YamlMap; final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; if (flutterSection == null) { return false; @@ -78,33 +74,3 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity, return false; } } - -/// Returns whether the given directory contains a Flutter Android plugin. -bool isAndroidPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformAndroid, entity); -} - -/// Returns whether the given directory contains a Flutter iOS plugin. -bool isIosPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformIos, entity); -} - -/// Returns whether the given directory contains a Flutter web plugin. -bool isWebPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformWeb, entity); -} - -/// Returns whether the given directory contains a Flutter Windows plugin. -bool isWindowsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformWindows, entity); -} - -/// Returns whether the given directory contains a Flutter macOS plugin. -bool isMacOsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformMacos, entity); -} - -/// Returns whether the given directory contains a Flutter linux plugin. -bool isLinuxPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformLinux, entity); -} diff --git a/script/tool/lib/src/common/pub_version_finder.dart b/script/tool/lib/src/common/pub_version_finder.dart index ebac473de7a..572cb913aa7 100644 --- a/script/tool/lib/src/common/pub_version_finder.dart +++ b/script/tool/lib/src/common/pub_version_finder.dart @@ -27,10 +27,10 @@ class PubVersionFinder { /// Get the package version on pub. Future getPackageVersion( - {required String package}) async { - assert(package.isNotEmpty); + {required String packageName}) async { + assert(packageName.isNotEmpty); final Uri pubHostUri = Uri.parse(pubHost); - final Uri url = pubHostUri.replace(path: '/packages/$package.json'); + final Uri url = pubHostUri.replace(path: '/packages/$packageName.json'); final http.Response response = await httpClient.get(url); if (response.statusCode == 404) { diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart new file mode 100644 index 00000000000..f6601d39b79 --- /dev/null +++ b/script/tool/lib/src/common/repository_package.dart @@ -0,0 +1,78 @@ +// 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:path/path.dart' as p; + +import 'core.dart'; + +/// A package in the repository. +// +// TODO(stuartmorgan): Add more package-related info here, such as an on-demand +// cache of the parsed pubspec. +class RepositoryPackage { + /// Creates a representation of the package at [directory]. + RepositoryPackage(this.directory); + + /// The location of the package. + final Directory directory; + + /// The path to the package. + String get path => directory.path; + + /// Returns the string to use when referring to the package in user-targeted + /// messages. + /// + /// Callers should not expect a specific format for this string, since + /// it uses heuristics to try to be precise without being overly verbose. If + /// an exact format (e.g., published name, or basename) is required, that + /// should be used instead. + String get displayName { + List components = directory.fileSystem.path.split(directory.path); + // Remove everything up to the packages directory. + final int packagesIndex = components.indexOf('packages'); + if (packagesIndex != -1) { + components = components.sublist(packagesIndex + 1); + } + // For the common federated plugin pattern of `foo/foo_subpackage`, drop + // the first part since it's not useful. + if (components.length >= 2 && + components[1].startsWith('${components[0]}_')) { + components = components.sublist(1); + } + return p.posix.joinAll(components); + } + + /// The package's top-level pubspec.yaml. + File get pubspecFile => directory.childFile('pubspec.yaml'); + + /// Returns the Flutter example packages contained in the package, if any. + Iterable getExamples() { + final Directory exampleDirectory = directory.childDirectory('example'); + if (!exampleDirectory.existsSync()) { + return []; + } + if (isFlutterPackage(exampleDirectory)) { + return [RepositoryPackage(exampleDirectory)]; + } + // 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 exampleDirectory + .listSync() + .where((FileSystemEntity entity) => isFlutterPackage(entity)) + // isFlutterPackage guarantees that the cast to Directory is safe. + .map((FileSystemEntity entity) => + RepositoryPackage(entity as Directory)); + } + + /// Returns the example directory, assuming there is only one. + /// + /// DO NOT USE THIS METHOD. It exists only to easily find code that was + /// written to use a single example and needs to be restructured to handle + /// multiple examples. New code should always use [getExamples]. + // TODO(stuartmorgan): Eliminate all uses of this. + RepositoryPackage getSingleExampleDeprecated() => + RepositoryPackage(directory.childDirectory('example')); +} 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 e1cee6f3fe7..6dbebf2f5c7 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -11,6 +11,7 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; const String _outputDirectoryFlag = 'output-dir'; @@ -170,10 +171,11 @@ class CreateAllPluginsAppCommand extends PluginCommand { final Map pathDependencies = {}; - await for (final PackageEnumerationEntry package in getTargetPackages()) { + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + final RepositoryPackage package = entry.package; final Directory pluginDirectory = package.directory; final String pluginName = pluginDirectory.basename; - final File pubspecFile = pluginDirectory.childFile('pubspec.yaml'); + final File pubspecFile = package.pubspecFile; final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec.publishTo != 'none') { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 7e800ed5486..3605dcce1f2 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -12,6 +12,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitNoPlatformFlags = 2; const int _exitNoAvailableDevice = 3; @@ -119,9 +120,9 @@ class DriveExamplesCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { - if (package.basename.endsWith('_platform_interface') && - !package.childDirectory('example').existsSync()) { + Future runForPackage(RepositoryPackage package) async { + if (package.directory.basename.endsWith('_platform_interface') && + !package.getSingleExampleDeprecated().directory.existsSync()) { // Platform interface packages generally aren't intended to have // examples, and don't need integration tests, so skip rather than fail. return PackageResult.skip( @@ -140,16 +141,16 @@ class DriveExamplesCommand extends PackageLoopingCommand { // If there is no supported target platform, skip the plugin. if (deviceFlags.isEmpty) { return PackageResult.skip( - '${getPackageDescription(package)} does not support any requested platform.'); + '${package.displayName} does not support any requested platform.'); } int examplesFound = 0; bool testsRan = false; final List errors = []; - for (final Directory example in getExamplesForPlugin(package)) { + for (final RepositoryPackage example in package.getExamples()) { ++examplesFound; final String exampleName = - getRelativePosixPath(example, from: packagesDir); + getRelativePosixPath(example.directory, from: packagesDir); final List drivers = await _getDrivers(example); if (drivers.isEmpty) { @@ -173,7 +174,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (testTargets.isEmpty) { final String driverRelativePath = - getRelativePosixPath(driver, from: package); + getRelativePosixPath(driver, from: package.directory); printError( 'Found $driverRelativePath, but no integration_test/*_test.dart files.'); errors.add('No test files for $driverRelativePath'); @@ -185,7 +186,8 @@ class DriveExamplesCommand extends PackageLoopingCommand { example, driver, testTargets, deviceFlags: deviceFlags); for (final File failingTarget in failingTargets) { - errors.add(getRelativePosixPath(failingTarget, from: package)); + errors.add( + getRelativePosixPath(failingTarget, from: package.directory)); } } } @@ -229,10 +231,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { return deviceIds; } - Future> _getDrivers(Directory example) async { + Future> _getDrivers(RepositoryPackage example) async { final List drivers = []; - final Directory driverDir = example.childDirectory('test_driver'); + final Directory driverDir = example.directory.childDirectory('test_driver'); if (driverDir.existsSync()) { await for (final FileSystemEntity driver in driverDir.list()) { if (driver is File && driver.basename.endsWith('_test.dart')) { @@ -253,10 +255,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { return testFile.existsSync() ? testFile : null; } - Future> _getIntegrationTests(Directory example) async { + Future> _getIntegrationTests(RepositoryPackage example) async { final List tests = []; final Directory integrationTestDir = - example.childDirectory('integration_test'); + example.directory.childDirectory('integration_test'); if (integrationTestDir.existsSync()) { await for (final FileSystemEntity file in integrationTestDir.list()) { @@ -278,7 +280,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { /// - `['-d', 'web-server', '--web-port=', '--browser-name=]` /// for web Future> _driveTests( - Directory example, + RepositoryPackage example, File driver, List targets, { required List deviceFlags, @@ -296,11 +298,11 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', '--driver', - getRelativePosixPath(driver, from: example), + getRelativePosixPath(driver, from: example.directory), '--target', - getRelativePosixPath(target, from: example), + getRelativePosixPath(target, from: example.directory), ], - workingDir: example); + workingDir: example.directory); if (exitCode != 0) { failures.add(target); } diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index fd2de97be4b..4fc47c0da70 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -13,6 +13,7 @@ import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitGcloudAuthFailed = 2; @@ -117,13 +118,13 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { - final Directory exampleDirectory = package.childDirectory('example'); + Future runForPackage(RepositoryPackage package) async { + final RepositoryPackage example = package.getSingleExampleDeprecated(); final Directory androidDirectory = - exampleDirectory.childDirectory('android'); + example.directory.childDirectory('android'); if (!androidDirectory.existsSync()) { return PackageResult.skip( - '${getPackageDescription(exampleDirectory)} does not support Android.'); + '${example.displayName} does not support Android.'); } if (!androidDirectory @@ -137,7 +138,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } // Ensures that gradle wrapper exists - final GradleProject project = GradleProject(exampleDirectory, + final GradleProject project = GradleProject(example.directory, processRunner: processRunner, platform: platform); if (!await _ensureGradleWrapperExists(project)) { return PackageResult.fail(['Unable to build example apk']); @@ -155,7 +156,8 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { // test file's run. int resultsCounter = 0; for (final File test in _findIntegrationTestFiles(package)) { - final String testName = getRelativePosixPath(test, from: package); + final String testName = + getRelativePosixPath(test, from: package.directory); print('Testing $testName...'); if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) { printError('Could not build $testName'); @@ -165,7 +167,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { final String buildId = getStringArg('build-id'); final String testRunId = getStringArg('test-run-id'); final String resultsDir = - 'plugins_android_test/${getPackageDescription(package)}/$buildId/$testRunId/${resultsCounter++}/'; + 'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/'; final List args = [ 'firebase', 'test', @@ -186,7 +188,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { args.addAll(['--device', device]); } final int exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: exampleDirectory); + workingDir: example.directory); if (exitCode != 0) { printError('Test failure for $testName'); @@ -262,9 +264,11 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } /// Finds and returns all integration test files for [package]. - Iterable _findIntegrationTestFiles(Directory package) sync* { - final Directory integrationTestDir = - package.childDirectory('example').childDirectory('integration_test'); + Iterable _findIntegrationTestFiles(RepositoryPackage package) sync* { + final Directory integrationTestDir = package + .getSingleExampleDeprecated() + .directory + .childDirectory('integration_test'); if (!integrationTestDir.existsSync()) { return; diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index be6c6ed3241..a7b5c4f2e8b 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -10,6 +10,7 @@ import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// Lint the CocoaPod podspecs and run unit tests. /// @@ -30,22 +31,22 @@ class LintAndroidCommand extends PackageLoopingCommand { 'Requires the example to have been build at least once before running.'; @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { if (!pluginSupportsPlatform(kPlatformAndroid, package, requiredMode: PlatformSupport.inline)) { return PackageResult.skip( 'Plugin does not have an Android implemenatation.'); } - final Directory exampleDirectory = package.childDirectory('example'); - final GradleProject project = GradleProject(exampleDirectory, + final RepositoryPackage example = package.getSingleExampleDeprecated(); + final GradleProject project = GradleProject(example.directory, processRunner: processRunner, platform: platform); if (!project.isConfigured()) { return PackageResult.fail(['Build example before linting']); } - final String packageName = package.basename; + final String packageName = package.directory.basename; // Only lint one build mode to avoid extra work. // Only lint the plugin project itself, to avoid failing due to errors in diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index d0d93fcb79b..ee44a82da5b 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -12,6 +12,7 @@ import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitUnsupportedPlatform = 2; const int _exitPodNotInstalled = 3; @@ -64,7 +65,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final List errors = []; final List podspecs = await _podspecsToLint(package); @@ -82,7 +83,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } - Future> _podspecsToLint(Directory package) async { + Future> _podspecsToLint(RepositoryPackage package) async { final List podspecs = await getFilesForPackage(package).where((File entity) { final String filePath = entity.path; diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 29a8ceb1278..e45c09bfd2e 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; /// A command to list different types of repository content. class ListCommand extends PluginCommand { @@ -39,23 +40,22 @@ class ListCommand extends PluginCommand { Future run() async { switch (getStringArg(_type)) { case _plugin: - await for (final PackageEnumerationEntry package - in getTargetPackages()) { - print(package.directory.path); + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + print(entry.package.path); } break; case _example: - final Stream examples = getTargetPackages() - .map((PackageEnumerationEntry entry) => entry.directory) - .expand(getExamplesForPlugin); - await for (final Directory package in examples) { + final Stream examples = getTargetPackages() + .expand( + (PackageEnumerationEntry entry) => entry.package.getExamples()); + await for (final RepositoryPackage package in examples) { print(package.path); } break; case _package: - await for (final PackageEnumerationEntry package + await for (final PackageEnumerationEntry entry in getTargetPackagesAndSubpackages()) { - print(package.directory.path); + print(entry.package.path); } break; case _file: diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 0bd2ab45f63..725cf23a2e9 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -10,6 +10,7 @@ import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; import 'common/xcode.dart'; const String _unitTestFlag = 'unit'; @@ -115,7 +116,7 @@ this command. } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final List testPlatforms = []; for (final String platform in _requestedPlatforms) { if (pluginSupportsPlatform(platform, package, @@ -171,23 +172,24 @@ this command. : PackageResult.success(); } - Future<_PlatformResult> _testAndroid(Directory plugin, _TestMode mode) async { - bool exampleHasUnitTests(Directory example) { - return example + Future<_PlatformResult> _testAndroid( + RepositoryPackage plugin, _TestMode mode) async { + bool exampleHasUnitTests(RepositoryPackage example) { + return example.directory .childDirectory('android') .childDirectory('app') .childDirectory('src') .childDirectory('test') .existsSync() || - example.parent + example.directory.parent .childDirectory('android') .childDirectory('src') .childDirectory('test') .existsSync(); } - bool exampleHasNativeIntegrationTests(Directory example) { - final Directory integrationTestDirectory = example + bool exampleHasNativeIntegrationTests(RepositoryPackage example) { + final Directory integrationTestDirectory = example.directory .childDirectory('android') .childDirectory('app') .childDirectory('src') @@ -216,12 +218,12 @@ this command. }); } - final Iterable examples = getExamplesForPlugin(plugin); + final Iterable examples = plugin.getExamples(); bool ranTests = false; bool failed = false; bool hasMissingBuild = false; - for (final Directory example in examples) { + for (final RepositoryPackage example in examples) { final bool hasUnitTests = exampleHasUnitTests(example); final bool hasIntegrationTests = exampleHasNativeIntegrationTests(example); @@ -239,11 +241,11 @@ this command. continue; } - final String exampleName = getPackageDescription(example); + final String exampleName = example.displayName; _printRunningExampleTestsMessage(example, 'Android'); final GradleProject project = GradleProject( - example, + example.directory, processRunner: processRunner, platform: platform, ); @@ -301,12 +303,12 @@ this command. return _PlatformResult(RunState.succeeded); } - Future<_PlatformResult> _testIos(Directory plugin, _TestMode mode) { + Future<_PlatformResult> _testIos(RepositoryPackage plugin, _TestMode mode) { return _runXcodeTests(plugin, 'iOS', mode, extraFlags: _iosDestinationFlags); } - Future<_PlatformResult> _testMacOS(Directory plugin, _TestMode mode) { + Future<_PlatformResult> _testMacOS(RepositoryPackage plugin, _TestMode mode) { return _runXcodeTests(plugin, 'macOS', mode); } @@ -316,7 +318,7 @@ this command. /// The tests targets must be added to the Xcode project of the example app, /// usually at "example/{ios,macos}/Runner.xcworkspace". Future<_PlatformResult> _runXcodeTests( - Directory plugin, + RepositoryPackage plugin, String platform, _TestMode mode, { List extraFlags = const [], @@ -330,11 +332,11 @@ this command. // Assume skipped until at least one test has run. RunState overallResult = RunState.skipped; - for (final Directory example in getExamplesForPlugin(plugin)) { - final String exampleName = getPackageDescription(example); + for (final RepositoryPackage example in plugin.getExamples()) { + final String exampleName = example.displayName; if (testTarget != null) { - final Directory project = example + final Directory project = example.directory .childDirectory(platform.toLowerCase()) .childDirectory('Runner.xcodeproj'); final bool? hasTarget = @@ -351,7 +353,7 @@ this command. _printRunningExampleTestsMessage(example, platform); final int exitCode = await _xcode.runXcodeBuild( - example, + example.directory, actions: ['test'], workspace: '${platform.toLowerCase()}/Runner.xcworkspace', scheme: 'Runner', @@ -387,20 +389,22 @@ this command. /// Prints a standard format message indicating that [platform] tests for /// [plugin]'s [example] are about to be run. - void _printRunningExampleTestsMessage(Directory example, String platform) { - print('Running $platform tests for ${getPackageDescription(example)}...'); + void _printRunningExampleTestsMessage( + RepositoryPackage example, String platform) { + print('Running $platform tests for ${example.displayName}...'); } /// Prints a standard format message indicating that no tests were found for /// [plugin]'s [example] for [platform]. - void _printNoExampleTestsMessage(Directory example, String platform) { - print('No $platform tests found for ${getPackageDescription(example)}'); + void _printNoExampleTestsMessage(RepositoryPackage example, String platform) { + print('No $platform tests found for ${example.displayName}'); } } // The type for a function that takes a plugin directory and runs its native // tests for a specific platform. -typedef _TestFunction = Future<_PlatformResult> Function(Directory, _TestMode); +typedef _TestFunction = Future<_PlatformResult> Function( + RepositoryPackage, _TestMode); /// A collection of information related to a specific platform. class _PlatformDetails { diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index fda68a6a74a..ab9f5f14749 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -16,6 +16,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; /// A command to check that packages are publishable via 'dart publish'. class PublishCheckCommand extends PackageLoopingCommand { @@ -75,7 +76,7 @@ class PublishCheckCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final _PublishCheckResult? result = await _passesPublishCheck(package); if (result == null) { return PackageResult.skip('Package is marked as unpublishable.'); @@ -114,8 +115,8 @@ class PublishCheckCommand extends PackageLoopingCommand { } } - Pubspec? _tryParsePubspec(Directory package) { - final File pubspecFile = package.childFile('pubspec.yaml'); + Pubspec? _tryParsePubspec(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; try { return Pubspec.parse(pubspecFile.readAsStringSync()); @@ -127,12 +128,12 @@ class PublishCheckCommand extends PackageLoopingCommand { } } - Future _hasValidPublishCheckRun(Directory package) async { + Future _hasValidPublishCheckRun(RepositoryPackage package) async { print('Running pub publish --dry-run:'); final io.Process process = await processRunner.start( flutterCommand, ['pub', 'publish', '--', '--dry-run'], - workingDirectory: package, + workingDirectory: package.directory, ); final StringBuffer outputBuffer = StringBuffer(); @@ -183,8 +184,9 @@ class PublishCheckCommand extends PackageLoopingCommand { /// Returns the result of the publish check, or null if the package is marked /// as unpublishable. - Future<_PublishCheckResult?> _passesPublishCheck(Directory package) async { - final String packageName = package.basename; + Future<_PublishCheckResult?> _passesPublishCheck( + RepositoryPackage package) async { + final String packageName = package.directory.basename; final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { print('no pubspec'); @@ -219,7 +221,7 @@ class PublishCheckCommand extends PackageLoopingCommand { Future<_PublishCheckResult> _checkPublishingStatus( {required String packageName, required Version? version}) async { final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: packageName); + await _pubVersionFinder.getPackageVersion(packageName: packageName); switch (pubVersionFinderResponse.result) { case PubVersionFinderResult.success: return pubVersionFinderResponse.versions.contains(version) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 8bcb9e37e8e..6e1658f6f6e 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -140,9 +140,9 @@ class PublishPluginCommand extends PluginCommand { @override Future run() async { - final String package = getStringArg(_packageOption); + final String packageName = getStringArg(_packageOption); final bool publishAllChanged = getBoolArg(_allChangedFlag); - if (package.isEmpty && !publishAllChanged) { + if (packageName.isEmpty && !publishAllChanged) { _print( 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); throw ToolExit(1); @@ -176,7 +176,7 @@ class PublishPluginCommand extends PluginCommand { ); } else { successful = await _publishAndTagPackage( - packageDir: _getPackageDir(package), + packageDir: _getPackageDir(packageName), remoteForTagPush: remote, ); } @@ -202,7 +202,7 @@ class PublishPluginCommand extends PluginCommand { await baseGitDir.runCommand(['tag', '--sort=-committerdate']); final List existingTags = (existingTagsResult.stdout as String) .split('\n') - ..removeWhere((String element) => element.isEmpty); + ..removeWhere((String element) => element.isEmpty); final List packagesReleased = []; final List packagesFailed = []; @@ -307,7 +307,7 @@ Safe to ignore if the package is deleted in this commit. // Check if the package named `packageName` with `version` has already published. final Version version = pubspec.version!; final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: pubspec.name); + await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); if (pubVersionFinderResponse.versions.contains(version)) { final String tagsForPackageWithSameVersion = existingTags.firstWhere( (String tag) => @@ -390,8 +390,8 @@ Safe to ignore if the package is deleted in this commit. // Returns the packageDirectory based on the package name. // Throws ToolExit if the `package` doesn't exist. - Directory _getPackageDir(String package) { - final Directory packageDir = packagesDir.childDirectory(package); + Directory _getPackageDir(String packageName) { + final Directory packageDir = packagesDir.childDirectory(packageName); if (!packageDir.existsSync()) { _print('${packageDir.absolute.path} does not exist.'); throw ToolExit(1); diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 0a066ab72ba..def2adaf278 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -10,6 +10,7 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// A command to enforce pubspec conventions across the repository. /// @@ -64,8 +65,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { bool get includeSubpackages => true; @override - Future runForPackage(Directory package) async { - final File pubspec = package.childFile('pubspec.yaml'); + Future runForPackage(RepositoryPackage package) async { + final File pubspec = package.pubspecFile; final bool passesCheck = !pubspec.existsSync() || await _checkPubspec(pubspec, package: package); if (!passesCheck) { @@ -76,7 +77,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { Future _checkPubspec( File pubspecFile, { - required Directory package, + required RepositoryPackage package, }) async { final String contents = pubspecFile.readAsStringSync(); final Pubspec? pubspec = _tryParsePubspec(contents); @@ -154,7 +155,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { List _checkForRepositoryLinkErrors( Pubspec pubspec, { - required Directory package, + required RepositoryPackage package, }) { final List errorMessages = []; if (pubspec.repository == null) { @@ -189,12 +190,12 @@ class PubspecCheckCommand extends PackageLoopingCommand { // Should only be called on plugin packages. String? _checkForImplementsError( Pubspec pubspec, { - required Directory package, + required RepositoryPackage package, }) { if (_isImplementationPackage(package)) { final String? implements = pubspec.flutter!['plugin']!['implements'] as String?; - final String expectedImplements = package.parent.basename; + final String expectedImplements = package.directory.parent.basename; if (implements == null) { return 'Missing "implements: $expectedImplements" in "plugin" section.'; } else if (implements != expectedImplements) { @@ -207,13 +208,13 @@ class PubspecCheckCommand extends PackageLoopingCommand { // Returns true if [packageName] appears to be an implementation package // according to repository conventions. - bool _isImplementationPackage(Directory package) { + bool _isImplementationPackage(RepositoryPackage package) { // An implementation package should be in a group folder... - final Directory parentDir = package.parent; + final Directory parentDir = package.directory.parent; if (parentDir.path == packagesDir.path) { return false; } - final String packageName = package.basename; + final String packageName = package.directory.basename; final String parentName = parentDir.basename; // ... whose name is a prefix of the package name. if (!packageName.startsWith(parentName)) { diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 9dfe66b7926..5a0b43d3b22 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -9,6 +9,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// A command to run Dart unit tests for packages. class TestCommand extends PackageLoopingCommand { @@ -36,13 +37,13 @@ class TestCommand extends PackageLoopingCommand { 'This command requires "flutter" to be in your path.'; @override - Future runForPackage(Directory package) async { - if (!package.childDirectory('test').existsSync()) { + Future runForPackage(RepositoryPackage package) async { + if (!package.directory.childDirectory('test').existsSync()) { return PackageResult.skip('No test/ directory.'); } bool passed; - if (isFlutterPackage(package)) { + if (isFlutterPackage(package.directory)) { passed = await _runFlutterTests(package); } else { passed = await _runDartTests(package); @@ -51,7 +52,7 @@ class TestCommand extends PackageLoopingCommand { } /// Runs the Dart tests for a Flutter package, returning true on success. - Future _runFlutterTests(Directory package) async { + Future _runFlutterTests(RepositoryPackage package) async { final String experiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( @@ -61,21 +62,21 @@ class TestCommand extends PackageLoopingCommand { '--color', if (experiment.isNotEmpty) '--enable-experiment=$experiment', // TODO(ditman): Remove this once all plugins are migrated to 'drive'. - if (isWebPlugin(package)) '--platform=chrome', + if (pluginSupportsPlatform(kPlatformWeb, package)) '--platform=chrome', ], - workingDir: package, + workingDir: package.directory, ); return exitCode == 0; } /// Runs the Dart tests for a non-Flutter package, returning true on success. - Future _runDartTests(Directory package) async { + Future _runDartTests(RepositoryPackage package) async { // Unlike `flutter test`, `pub run test` does not automatically get // packages int exitCode = await processRunner.runAndStream( 'dart', ['pub', 'get'], - workingDir: package, + workingDir: package.directory, ); if (exitCode != 0) { printError('Unable to fetch dependencies.'); @@ -92,7 +93,7 @@ class TestCommand extends PackageLoopingCommand { if (experiment.isNotEmpty) '--enable-experiment=$experiment', 'test', ], - workingDir: package, + workingDir: package.directory, ); return exitCode == 0; diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 67c56378288..67a81b967a8 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -16,6 +16,7 @@ import 'common/git_version_finder.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; /// Categories of version change types. enum NextVersionType { @@ -133,7 +134,7 @@ class VersionCheckCommand extends PackageLoopingCommand { Future initializeRun() async {} @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { // No remaining checks make sense, so fail immediately. @@ -196,7 +197,7 @@ class VersionCheckCommand extends PackageLoopingCommand { /// the name from pubspec.yaml, not the on disk name if different.) Future _fetchPreviousVersionFromPub(String packageName) async { final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: packageName); + await _pubVersionFinder.getPackageVersion(packageName: packageName); switch (pubVersionFinderResponse.result) { case PubVersionFinderResult.success: return pubVersionFinderResponse.versions.first; @@ -214,10 +215,10 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} /// Returns the version of [package] from git at the base comparison hash. Future _getPreviousVersionFromGit( - Directory package, { + RepositoryPackage package, { required GitVersionFinder gitVersionFinder, }) async { - final File pubspecFile = package.childFile('pubspec.yaml'); + final File pubspecFile = package.pubspecFile; final String relativePath = path.relative(pubspecFile.absolute.path, from: (await gitDir).path); // Use Posix-style paths for git. @@ -230,7 +231,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} /// Returns the state of the verison of [package] relative to the comparison /// base (git or pub, depending on flags). Future<_CurrentVersionState> _getVersionState( - Directory package, { + RepositoryPackage package, { required Pubspec pubspec, }) async { // This method isn't called unless `version` is non-null. @@ -310,7 +311,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} /// /// Returns false if the CHANGELOG fails validation. Future _validateChangelogVersion( - Directory package, { + RepositoryPackage package, { required Pubspec pubspec, required bool pubspecVersionChanged, }) async { @@ -318,7 +319,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final Version fromPubspec = pubspec.version!; // get first version from CHANGELOG - final File changelog = package.childFile('CHANGELOG.md'); + final File changelog = package.directory.childFile('CHANGELOG.md'); final List lines = changelog.readAsLinesSync(); String? firstLineWithText; final Iterator iterator = lines.iterator; @@ -386,8 +387,8 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return true; } - Pubspec? _tryParsePubspec(Directory package) { - final File pubspecFile = package.childFile('pubspec.yaml'); + Pubspec? _tryParsePubspec(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; try { final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index 27cd8c43514..3d34dab9f08 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -9,6 +9,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; import 'common/xcode.dart'; /// The command to run Xcode's static analyzer on plugins. @@ -42,7 +43,7 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final bool testIos = getBoolArg(kPlatformIos) && pluginSupportsPlatform(kPlatformIos, package, requiredMode: PlatformSupport.inline); @@ -78,18 +79,18 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { /// Analyzes [plugin] for [platform], returning true if it passed analysis. Future _analyzePlugin( - Directory plugin, + RepositoryPackage plugin, String platform, { List extraFlags = const [], }) async { bool passing = true; - for (final Directory example in getExamplesForPlugin(plugin)) { + for (final RepositoryPackage example in plugin.getExamples()) { // Running tests and static analyzer. - final String examplePath = - getRelativePosixPath(example, from: plugin.parent); + final String examplePath = getRelativePosixPath(example.directory, + from: plugin.directory.parent); print('Running $platform tests and analyzer for $examplePath...'); final int exitCode = await _xcode.runXcodeBuild( - example, + example.directory, actions: ['analyze'], workspace: '${platform.toLowerCase()}/Runner.xcworkspace', scheme: 'Runner', diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 00e64ddc21f..721923ae9c6 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -11,6 +11,7 @@ import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; @@ -578,64 +579,6 @@ void main() { ])); }); }); - - group('utility', () { - test('getPackageDescription prints packageDir-relative paths by default', - () async { - final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir, platform: mockPlatform); - - expect( - command.getPackageDescription(packagesDir.childDirectory('foo')), - 'foo', - ); - expect( - command.getPackageDescription(packagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')), - 'foo/bar/baz', - ); - }); - - test('getPackageDescription always uses Posix-style paths', () async { - mockPlatform.isWindows = true; - final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir, platform: mockPlatform); - - expect( - command.getPackageDescription(packagesDir.childDirectory('foo')), - 'foo', - ); - expect( - command.getPackageDescription(packagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')), - 'foo/bar/baz', - ); - }); - - test( - 'getPackageDescription elides group name in grouped federated plugin structure', - () async { - final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir, platform: mockPlatform); - - expect( - command.getPackageDescription(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_platform_interface')), - 'a_plugin_platform_interface', - ); - expect( - command.getPackageDescription(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_web')), - 'a_plugin_web', - ); - }); - }); } class TestPackageLoopingCommand extends PackageLoopingCommand { @@ -699,18 +642,18 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { checkedPackages.add(package.path); - final File warningFile = package.childFile(_warningFile); + final File warningFile = package.directory.childFile(_warningFile); if (warningFile.existsSync()) { final List warnings = warningFile.readAsLinesSync(); warnings.forEach(logWarning); } - final File skipFile = package.childFile(_skipFile); + final File skipFile = package.directory.childFile(_skipFile); if (skipFile.existsSync()) { return PackageResult.skip(skipFile.readAsStringSync()); } - final File errorFile = package.childFile(_errorFile); + final File errorFile = package.directory.childFile(_errorFile); if (errorFile.existsSync()) { return PackageResult.fail(errorFile.readAsLinesSync()); } diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 2f332aa8eb5..10bdff4e9c5 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -498,7 +498,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory package) => package.path) + .map((Directory packageDir) => packageDir.path) .toList())); } }); @@ -541,7 +541,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory package) => package.path) + .map((Directory packageDir) => packageDir.path) .toList())); } }); @@ -594,7 +594,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory package) => package.path) + .map((Directory packageDir) => packageDir.path) .toList())); } }); @@ -620,8 +620,8 @@ class SamplePluginCommand extends PluginCommand { @override Future run() async { - await for (final PackageEnumerationEntry package in getTargetPackages()) { - plugins.add(package.directory.path); + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + plugins.add(entry.package.path); } } } diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index c32c3f8e02b..7f1ba2add00 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:file/memory.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/common/repository_package.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -21,7 +22,8 @@ void main() { group('pluginSupportsPlatform', () { test('no platforms', () async { - final Directory plugin = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + RepositoryPackage(createFakePlugin('plugin', packagesDir)); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isFalse); expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); @@ -32,7 +34,8 @@ void main() { }); test('all platforms', () async { - final Directory plugin = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformAndroid: PlatformSupport.inline, kPlatformIos: PlatformSupport.inline, @@ -40,7 +43,7 @@ void main() { kPlatformMacos: PlatformSupport.inline, kPlatformWeb: PlatformSupport.inline, kPlatformWindows: PlatformSupport.inline, - }); + })); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(kPlatformIos, plugin), isTrue); @@ -51,7 +54,7 @@ void main() { }); test('some platforms', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -59,7 +62,7 @@ void main() { kPlatformLinux: PlatformSupport.inline, kPlatformWeb: PlatformSupport.inline, }, - ); + )); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); @@ -70,7 +73,7 @@ void main() { }); test('inline plugins are only detected as inline', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -81,7 +84,7 @@ void main() { kPlatformWeb: PlatformSupport.inline, kPlatformWindows: PlatformSupport.inline, }, - ); + )); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, @@ -135,7 +138,7 @@ void main() { test('federated plugins are only detected as federated', () async { const String pluginName = 'plugin'; - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( pluginName, packagesDir, platformSupport: { @@ -146,7 +149,7 @@ void main() { kPlatformWeb: PlatformSupport.federated, kPlatformWindows: PlatformSupport.federated, }, - ); + )); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, diff --git a/script/tool/test/common/pub_version_finder_test.dart b/script/tool/test/common/pub_version_finder_test.dart index 7d8658a907e..1692cf214ab 100644 --- a/script/tool/test/common/pub_version_finder_test.dart +++ b/script/tool/test/common/pub_version_finder_test.dart @@ -19,7 +19,7 @@ void main() { }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); + await finder.getPackageVersion(packageName: 'some_package'); expect(response.versions, isEmpty); expect(response.result, PubVersionFinderResult.noPackageFound); @@ -33,7 +33,7 @@ void main() { }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); + await finder.getPackageVersion(packageName: 'some_package'); expect(response.versions, isEmpty); expect(response.result, PubVersionFinderResult.fail); @@ -64,7 +64,7 @@ void main() { }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); + await finder.getPackageVersion(packageName: 'some_package'); expect(response.versions, [ Version.parse('2.0.0'), diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart new file mode 100644 index 00000000000..5c5624312f5 --- /dev/null +++ b/script/tool/test/common/repository_package_test.dart @@ -0,0 +1,123 @@ +// 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/repository_package.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('displayName', () { + test('prints packageDir-relative paths by default', () async { + expect( + RepositoryPackage(packagesDir.childDirectory('foo')).displayName, + 'foo', + ); + expect( + RepositoryPackage(packagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('handles third_party/packages/', () async { + expect( + RepositoryPackage(packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages') + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('always uses Posix-style paths', () async { + final Directory windowsPackagesDir = createPackagesDirectory( + fileSystem: MemoryFileSystem(style: FileSystemStyle.windows)); + + expect( + RepositoryPackage(windowsPackagesDir.childDirectory('foo')).displayName, + 'foo', + ); + expect( + RepositoryPackage(windowsPackagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('elides group name in grouped federated plugin structure', () async { + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_platform_interface')) + .displayName, + 'a_plugin_platform_interface', + ); + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_platform_web')) + .displayName, + 'a_plugin_platform_web', + ); + }); + + // The app-facing package doesn't get elided to avoid potential confusion + // with the group folder itself. + test('does not elide group name for app-facing packages', () async { + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin')) + .displayName, + 'a_plugin/a_plugin', + ); + }); + }); + + group('getExamples', () { + test('handles a single example', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + + final List examples = + RepositoryPackage(plugin).getExamples().toList(); + + expect(examples.length, 1); + expect(examples[0].path, plugin.childDirectory('example').path); + }); + + test('handles multiple examples', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + + final List examples = + RepositoryPackage(plugin).getExamples().toList(); + + expect(examples.length, 2); + expect(examples[0].path, + plugin.childDirectory('example').childDirectory('example1').path); + expect(examples[1].path, + plugin.childDirectory('example').childDirectory('example2').path); + }); + }); +} diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index cf57a9d0dcf..e2bf1e3e6e8 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -49,10 +49,10 @@ void main() { /// Returns a modified version of a list of [relativePaths] that are relative /// to [package] to instead be relative to [packagesDir]. List _getPackagesDirRelativePaths( - Directory package, List relativePaths) { + Directory packageDir, List relativePaths) { final p.Context path = analyzeCommand.path; final String relativeBase = - path.relative(package.path, from: packagesDir.path); + path.relative(packageDir.path, from: packagesDir.path); return relativePaths .map((String relativePath) => path.join(relativeBase, relativePath)) .toList(); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 10a85f49e81..05aebe82fd7 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -46,6 +46,7 @@ Directory createPackagesDirectory( /// /// [extraFiles] is an optional list of plugin-relative paths, using Posix /// separators, of extra files to create in the plugin. +// TODO(stuartmorgan): Convert the return to a RepositoryPackage. Directory createFakePlugin( String name, Directory parentDirectory, { @@ -77,6 +78,7 @@ Directory createFakePlugin( /// /// [extraFiles] is an optional list of package-relative paths, using unix-style /// separators, of extra files to create in the package. +// TODO(stuartmorgan): Convert the return to a RepositoryPackage. Directory createFakePackage( String name, Directory parentDirectory, { From 26bed31674cfb7f3a064575edd434426551c51fd Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Aug 2021 21:22:36 -0400 Subject: [PATCH 115/249] [flutter_plugin_tool] Migrate publish_plugin_command_test to runCapturingPrint (#4260) Finishes the migration of tool tests to `runCapturingPrint`. This makes the tests much less verbose, and makes it match the rest of the tool tests. Also adds the use of `printError` for error output, now that it's trivial to do so. --- .../tool/lib/src/publish_plugin_command.dart | 70 ++-- .../test/publish_plugin_command_test.dart | 355 ++++++++++-------- 2 files changed, 239 insertions(+), 186 deletions(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 6e1658f6f6e..5a75ce6af89 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -11,6 +11,7 @@ import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; @@ -48,15 +49,15 @@ class PublishPluginCommand extends PluginCommand { PublishPluginCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - Print print = print, + Platform platform = const LocalPlatform(), io.Stdin? stdinput, GitDir? gitDir, http.Client? httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), - _print = print, _stdin = stdinput ?? io.stdin, - super(packagesDir, processRunner: processRunner, gitDir: gitDir) { + super(packagesDir, + platform: platform, processRunner: processRunner, gitDir: gitDir) { argParser.addOption( _packageOption, help: 'The package to publish.' @@ -133,7 +134,6 @@ class PublishPluginCommand extends PluginCommand { 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; - final Print _print; final io.Stdin _stdin; StreamSubscription? _stdinSubscription; final PubVersionFinder _pubVersionFinder; @@ -143,12 +143,12 @@ class PublishPluginCommand extends PluginCommand { final String packageName = getStringArg(_packageOption); final bool publishAllChanged = getBoolArg(_allChangedFlag); if (packageName.isEmpty && !publishAllChanged) { - _print( + printError( 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); throw ToolExit(1); } - _print('Checking local repo...'); + print('Checking local repo...'); final GitDir repository = await gitDir; final bool shouldPushTag = getBoolArg(_pushTagsOption); @@ -163,9 +163,9 @@ class PublishPluginCommand extends PluginCommand { } remote = _RemoteInfo(name: remoteName, url: remoteUrl); } - _print('Local repo is ready!'); + print('Local repo is ready!'); if (getBoolArg(_dryRunFlag)) { - _print('=============== DRY RUN ==============='); + print('=============== DRY RUN ==============='); } bool successful; @@ -193,11 +193,11 @@ class PublishPluginCommand extends PluginCommand { final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); if (changedPubspecs.isEmpty) { - _print('No version updates in this commit.'); + print('No version updates in this commit.'); return true; } - _print('Getting existing tags...'); + print('Getting existing tags...'); final io.ProcessResult existingTagsResult = await baseGitDir.runCommand(['tag', '--sort=-committerdate']); final List existingTags = (existingTagsResult.stdout as String) @@ -228,7 +228,7 @@ class PublishPluginCommand extends PluginCommand { packagesFailed.add(pubspecFile.parent.basename); continue; } - _print('\n'); + print('\n'); if (await _publishAndTagPackage( packageDir: pubspecFile.parent, remoteForTagPush: remoteForTagPush, @@ -237,13 +237,13 @@ class PublishPluginCommand extends PluginCommand { } else { packagesFailed.add(pubspecFile.parent.basename); } - _print('\n'); + print('\n'); } if (packagesReleased.isNotEmpty) { - _print('Packages released: ${packagesReleased.join(', ')}'); + print('Packages released: ${packagesReleased.join(', ')}'); } if (packagesFailed.isNotEmpty) { - _print( + printError( 'Failed to release the following packages: ${packagesFailed.join(', ')}, see above for details.'); } return packagesFailed.isEmpty; @@ -268,7 +268,7 @@ class PublishPluginCommand extends PluginCommand { return false; } } - _print('Released [${packageDir.basename}] successfully.'); + print('Released [${packageDir.basename}] successfully.'); return true; } @@ -278,7 +278,7 @@ class PublishPluginCommand extends PluginCommand { required List existingTags, }) async { if (!pubspecFile.existsSync()) { - _print(''' + print(''' The file at The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. Safe to ignore if the package is deleted in this commit. '''); @@ -299,7 +299,7 @@ Safe to ignore if the package is deleted in this commit. } if (pubspec.version == null) { - _print( + printError( 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); return _CheckNeedsReleaseResult.failure; } @@ -314,14 +314,14 @@ Safe to ignore if the package is deleted in this commit. tag.split('-v').first == pubspec.name && tag.split('-v').last == version.toString(), orElse: () => ''); - _print( + print( 'The version $version of ${pubspec.name} has already been published'); if (tagsForPackageWithSameVersion.isEmpty) { - _print( + printError( 'However, the git release tag for this version (${pubspec.name}-v$version) is not found. Please manually fix the tag then run the command again.'); return _CheckNeedsReleaseResult.failure; } else { - _print('skip.'); + print('skip.'); return _CheckNeedsReleaseResult.noRelease; } } @@ -340,7 +340,7 @@ Safe to ignore if the package is deleted in this commit. if (!publishOK) { return false; } - _print('Package published!'); + print('Package published!'); return true; } @@ -353,7 +353,7 @@ Safe to ignore if the package is deleted in this commit. _RemoteInfo? remoteForPush, }) async { final String tag = _getTag(packageDir); - _print('Tagging release $tag...'); + print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await processRunner.run( 'git', @@ -370,7 +370,7 @@ Safe to ignore if the package is deleted in this commit. return true; } - _print('Pushing tag to ${remoteForPush.name}...'); + print('Pushing tag to ${remoteForPush.name}...'); return await _pushTagToRemote( tag: tag, remote: remoteForPush, @@ -381,9 +381,9 @@ Safe to ignore if the package is deleted in this commit. await _stdinSubscription?.cancel(); _stdinSubscription = null; if (successful) { - _print('Done!'); + print('Done!'); } else { - _print('Failed, see above for details.'); + printError('Failed, see above for details.'); throw ToolExit(1); } } @@ -393,7 +393,7 @@ Safe to ignore if the package is deleted in this commit. Directory _getPackageDir(String packageName) { final Directory packageDir = packagesDir.childDirectory(packageName); if (!packageDir.existsSync()) { - _print('${packageDir.absolute.path} does not exist.'); + printError('${packageDir.absolute.path} does not exist.'); throw ToolExit(1); } return packageDir; @@ -412,7 +412,7 @@ Safe to ignore if the package is deleted in this commit. final String statusOutput = statusResult.stdout as String; if (statusOutput.isNotEmpty) { - _print( + printError( "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" '$statusOutput\n' 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); @@ -435,7 +435,7 @@ Safe to ignore if the package is deleted in this commit. Future _publish(Directory packageDir) async { final List publishFlags = getStringListArg(_pubFlagsOption); - _print( + print( 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); if (getBoolArg(_dryRunFlag)) { return true; @@ -451,18 +451,14 @@ Safe to ignore if the package is deleted in this commit. final io.Process publish = await processRunner.start( flutterCommand, ['pub', 'publish'] + publishFlags, workingDirectory: packageDir); - publish.stdout - .transform(utf8.decoder) - .listen((String data) => _print(data)); - publish.stderr - .transform(utf8.decoder) - .listen((String data) => _print(data)); + publish.stdout.transform(utf8.decoder).listen((String data) => print(data)); + publish.stderr.transform(utf8.decoder).listen((String data) => print(data)); _stdinSubscription ??= _stdin .transform(utf8.decoder) .listen((String data) => publish.stdin.writeln(data)); final int result = await publish.exitCode; if (result != 0) { - _print('Publish ${packageDir.basename} failed.'); + printError('Publish ${packageDir.basename} failed.'); return false; } return true; @@ -490,10 +486,10 @@ Safe to ignore if the package is deleted in this commit. }) async { assert(remote != null && tag != null); if (!getBoolArg(_skipConfirmationFlag)) { - _print('Ready to push $tag to ${remote.url} (y/n)?'); + print('Ready to push $tag to ${remote.url} (y/n)?'); final String? input = _stdin.readLineSync(); if (input?.toLowerCase() != 'y') { - _print('Tag push canceled.'); + print('Tag push canceled.'); return false; } } diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 9a937daa238..576d3a4c88c 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -24,7 +24,6 @@ import 'util.dart'; void main() { const String testPluginName = 'foo'; - late List printedMessages; late Directory testRoot; late Directory packagesDir; @@ -62,13 +61,9 @@ void main() { await gitDir.runCommand(['commit', '-m', 'Initial commit']); processRunner = TestProcessRunner(); mockStdin = MockStdin(); - printedMessages = []; commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand(packagesDir, - processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), - stdinput: mockStdin, - gitDir: gitDir)); + processRunner: processRunner, stdinput: mockStdin, gitDir: gitDir)); }); tearDown(() { @@ -77,50 +72,66 @@ void main() { group('Initial validation', () { test('requires a package flag', () async { - await expectLater(() => commandRunner.run(['publish-plugin']), - throwsA(isA())); - expect( - printedMessages.last, contains('Must specify a package to publish.')); + Error? commandError; + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output.last, contains('Must specify a package to publish.')); }); test('requires an existing flag', () async { - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - 'iamerror', - '--no-push-tags' - ]), - throwsA(isA())); - - expect(printedMessages.last, contains('iamerror does not exist')); + Error? commandError; + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--package', 'iamerror', '--no-push-tags'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output.last, contains('iamerror does not exist')); }); test('refuses to proceed with dirty files', () async { pluginDir.childFile('tmp').createSync(); - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags' - ]), - throwsA(isA())); + Error? commandError; + final List output = await runCapturingPrint( + commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags' + ], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); expect( - printedMessages, - containsAllInOrder([ - 'There are files in the package directory that haven\'t been saved in git. Refusing to publish these files:\n\n?? packages/foo/tmp\n\nIf the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.', - 'Failed, see above for details.', + output, + containsAllInOrder([ + contains('There are files in the package directory that haven\'t ' + 'been saved in git. Refusing to publish these files:\n\n' + '?? packages/foo/tmp\n\n' + 'If the directory should be clean, you can run `git clean -xdf && ' + 'git reset --hard HEAD` to wipe all local changes.'), + contains('Failed, see above for details.'), ])); }); test('fails immediately if the remote doesn\'t exist', () async { - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), - throwsA(isA())); + Error? commandError; + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect(processRunner.results.last.stderr, contains('No such remote')); }); @@ -128,7 +139,8 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -136,7 +148,7 @@ void main() { '--no-tag-release' ]); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('can publish non-flutter package', () async { @@ -149,20 +161,28 @@ void main() { await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--package', packageName, '--no-push-tags', '--no-tag-release' ]); - expect(printedMessages.last, 'Done!'); + + expect(output.last, 'Done!'); }); }); group('Publishes package', () { test('while showing all output from pub publish to the user', () async { - final Future publishCommand = commandRunner.run([ + processRunner.mockPublishStdout = 'Foo'; + processRunner.mockPublishStderr = 'Bar'; + processRunner.mockPublishCompleteCode = 0; + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -170,28 +190,21 @@ void main() { '--no-tag-release' ]); - processRunner.mockPublishStdout = 'Foo'; - processRunner.mockPublishStderr = 'Bar'; - processRunner.mockPublishCompleteCode = 0; - - await publishCommand; - - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); + expect(output, contains('Foo')); + expect(output, contains('Bar')); }); test('forwards input from the user to `pub publish`', () async { - final Future publishCommand = commandRunner.run([ + mockStdin.mockUserInputs.add(utf8.encode('user input')); + processRunner.mockPublishCompleteCode = 0; + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, '--no-push-tags', '--no-tag-release' ]); - mockStdin.mockUserInputs.add(utf8.encode('user input')); - processRunner.mockPublishCompleteCode = 0; - - await publishCommand; expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); @@ -199,7 +212,8 @@ void main() { test('forwards --pub-publish-flags to pub publish', () async { processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -221,7 +235,8 @@ void main() { () async { processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); - await commandRunner.run([ + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -241,23 +256,30 @@ void main() { test('throws if pub publish fails', () async { processRunner.mockPublishCompleteCode = 128; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release', - ]), - throwsA(isA())); - - expect(printedMessages, contains('Publish foo failed.')); + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Publish foo failed.'), + ])); }); test('publish, dry run', () async { - // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. - processRunner.mockPublishCompleteCode = 1; - await commandRunner.run([ + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -268,7 +290,7 @@ void main() { expect(processRunner.pushTagsArgs, isEmpty); expect( - printedMessages, + output, containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', @@ -280,7 +302,8 @@ void main() { group('Tags release', () { test('with the version and name from the pubspec.yaml', () async { processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -295,16 +318,24 @@ void main() { test('only if publishing succeeded', () async { processRunner.mockPublishCompleteCode = 128; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - ]), - throwsA(isA())); - - expect(printedMessages, contains('Publish foo failed.')); + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Publish foo failed.'), + ])); final String? tag = (await gitDir.runCommand( ['show-ref', '$testPluginName-v0.0.1'], throwOnError: false)) @@ -322,22 +353,28 @@ void main() { test('requires user confirmation', () async { processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'help'; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - ]), - throwsA(isA())); - - expect(printedMessages, contains('Tag push canceled.')); + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output, contains('Tag push canceled.')); }); test('to upstream by default', () async { await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -346,7 +383,7 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('does not ask for user input if the --skip-confirmation flag is on', @@ -354,7 +391,9 @@ void main() { await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--skip-confirmation', '--package', @@ -364,7 +403,7 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('to upstream by default, dry run', () async { @@ -372,12 +411,13 @@ void main() { // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; - await commandRunner.run( + + final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--package', testPluginName, '--dry-run']); expect(processRunner.pushTagsArgs, isEmpty); expect( - printedMessages, + output, containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', @@ -392,7 +432,9 @@ void main() { ['remote', 'add', 'origin', 'http://localhost:8001']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -403,12 +445,14 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'origin'); expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('only if tagging and pushing to remotes are both enabled', () async { processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -416,7 +460,7 @@ void main() { ]); expect(processRunner.pushTagsArgs.isEmpty, isTrue); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); }); @@ -450,7 +494,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -473,10 +516,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -523,7 +568,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -543,8 +587,10 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + processRunner.pushTagsArgs.clear(); // Non-federated @@ -554,11 +600,12 @@ void main() { createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 0 when running `pub publish`. - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + output.addAll(await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~'])); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -597,7 +644,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -617,14 +663,17 @@ void main() { // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; - await commandRunner.run([ + + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--all-changed', '--base-sha=HEAD~', '--dry-run' ]); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -662,7 +711,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -683,10 +731,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -704,7 +754,6 @@ void main() { expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); processRunner.pushTagsArgs.clear(); - printedMessages.clear(); final List plugin1Pubspec = pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); @@ -724,10 +773,10 @@ void main() { await gitDir .runCommand(['commit', '-m', 'Update versions to 0.0.2']); - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + final List output2 = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( - printedMessages, + output2, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -769,7 +818,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -790,10 +838,11 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -811,7 +860,6 @@ void main() { expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); processRunner.pushTagsArgs.clear(); - printedMessages.clear(); final List plugin1Pubspec = pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); @@ -830,10 +878,10 @@ void main() { 'Update plugin1 versions to 0.0.2, delete plugin2' ]); - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + final List output2 = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( - printedMessages, + output2, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -872,7 +920,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -895,10 +942,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -935,7 +984,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -956,10 +1004,17 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await expectLater( - () => commandRunner.run( - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']), - throwsA(isA())); + + Error? commandError; + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--all-changed', + '--base-sha=HEAD~' + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect(processRunner.pushTagsArgs, isEmpty); }); @@ -984,10 +1039,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -1011,7 +1068,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -1029,23 +1085,24 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Done!' ])); expect( - printedMessages.contains( + output.contains( 'Running `pub publish ` in ${flutterPluginTools.path}...\n', ), isFalse); expect(processRunner.pushTagsArgs, isEmpty); processRunner.pushTagsArgs.clear(); - printedMessages.clear(); }); }); } From da97a527ad294561b832bf802988fb5eca261b07 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 25 Aug 2021 16:39:58 -0400 Subject: [PATCH 116/249] [flutter_plugin_tools] Convert publish tests to mock git (#4263) Replaces the use of an actual git repository on the filesystem with mock git output and an in-memory filesystem. This: - makes the tests more hermetic. - simplifies the setup of some tests considerably, avoiding the need to run the command once to set up the expected state before running a second time for the intended test. - eliminates some of the special handling in the test's custom process runner (making it easier to eliminate in a PR that will follow after). Also adds some output checking in a couple of tests that didn't have enough to ensure that they were necessarily testing the right thing (e.g., testing that a specific thing didn't happen, but not checking that the publish step that could have caused that thing to happen even ran at all). --- .../tool/lib/src/publish_plugin_command.dart | 24 +- .../test/publish_plugin_command_test.dart | 563 +++++++++--------- 2 files changed, 285 insertions(+), 302 deletions(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 5a75ce6af89..be9e6d30012 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -355,11 +355,9 @@ Safe to ignore if the package is deleted in this commit. final String tag = _getTag(packageDir); print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await processRunner.run( - 'git', + final io.ProcessResult result = await (await gitDir).runCommand( ['tag', tag], - workingDir: packageDir, - logOnError: true, + throwOnError: false, ); if (result.exitCode != 0) { return false; @@ -400,11 +398,9 @@ Safe to ignore if the package is deleted in this commit. } Future _checkGitStatus(Directory packageDir) async { - final io.ProcessResult statusResult = await processRunner.run( - 'git', + final io.ProcessResult statusResult = await (await gitDir).runCommand( ['status', '--porcelain', '--ignored', packageDir.absolute.path], - workingDir: packageDir, - logOnError: true, + throwOnError: false, ); if (statusResult.exitCode != 0) { return false; @@ -421,11 +417,9 @@ Safe to ignore if the package is deleted in this commit. } Future _verifyRemote(String remote) async { - final io.ProcessResult getRemoteUrlResult = await processRunner.run( - 'git', + final io.ProcessResult getRemoteUrlResult = await (await gitDir).runCommand( ['remote', 'get-url', remote], - workingDir: packagesDir, - logOnError: true, + throwOnError: false, ); if (getRemoteUrlResult.exitCode != 0) { return null; @@ -494,11 +488,9 @@ Safe to ignore if the package is deleted in this commit. } } if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await processRunner.run( - 'git', + final io.ProcessResult result = await (await gitDir).runCommand( ['push', remote.name, tag], - workingDir: packagesDir, - logOnError: true, + throwOnError: false, ); if (result.exitCode != 0) { return false; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 576d3a4c88c..40018b6edb6 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -8,34 +8,31 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; -import 'package:file/local.dart'; +import 'package:file/memory.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:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import 'common/plugin_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; void main() { const String testPluginName = 'foo'; - late Directory testRoot; late Directory packagesDir; late Directory pluginDir; - late GitDir gitDir; + late MockGitDir gitDir; late TestProcessRunner processRunner; + late RecordingProcessRunner gitProcessRunner; late CommandRunner commandRunner; late MockStdin mockStdin; - // This test uses a local file system instead of an in memory one throughout - // so that git actually works. In setup we initialize a mono repo of plugins - // with one package and commit everything to Git. - const FileSystem fileSystem = LocalFileSystem(); + late FileSystem fileSystem; void _createMockCredentialFile() { final String credentialPath = PublishPluginCommand.getCredentialPath(); @@ -45,20 +42,26 @@ void main() { } setUp(() async { - testRoot = fileSystem.systemTempDirectory - .createTempSync('publish_plugin_command_test-'); - // The temp directory can have symbolic links, which won't match git output; - // use a fully resolved version to avoid potential path comparison issues. - testRoot = fileSystem.directory(testRoot.resolveSymbolicLinksSync()); - packagesDir = createPackagesDirectory(parentDir: testRoot); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + // TODO(stuartmorgan): Move this from setup to individual tests. pluginDir = createFakePlugin(testPluginName, packagesDir, examples: []); assert(pluginDir != null && pluginDir.existsSync()); - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Initial commit']); + + gitProcessRunner = RecordingProcessRunner(); + gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Attach the first argument to the command to make targeting the mock + // results easier. + final String gitCommand = arguments.removeAt(0); + return gitProcessRunner.run('git-$gitCommand', arguments); + }); + processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') @@ -66,10 +69,6 @@ void main() { processRunner: processRunner, stdinput: mockStdin, gitDir: gitDir)); }); - tearDown(() { - testRoot.deleteSync(recursive: true); - }); - group('Initial validation', () { test('requires a package flag', () async { Error? commandError; @@ -79,7 +78,11 @@ void main() { }); expect(commandError, isA()); - expect(output.last, contains('Must specify a package to publish.')); + expect( + output, + containsAllInOrder([ + contains('Must specify a package to publish.'), + ])); }); test('requires an existing flag', () async { @@ -91,11 +94,14 @@ void main() { }); expect(commandError, isA()); - expect(output.last, contains('iamerror does not exist')); + expect(output, + containsAllInOrder([contains('iamerror does not exist')])); }); test('refuses to proceed with dirty files', () async { - pluginDir.childFile('tmp').createSync(); + gitProcessRunner.mockProcessesForExecutable['git-status'] = [ + MockProcess(stdout: '?? ${pluginDir.childFile('tmp').path}\n') + ]; Error? commandError; final List output = await runCapturingPrint( @@ -114,7 +120,7 @@ void main() { containsAllInOrder([ contains('There are files in the package directory that haven\'t ' 'been saved in git. Refusing to publish these files:\n\n' - '?? packages/foo/tmp\n\n' + '?? /packages/foo/tmp\n\n' 'If the directory should be clean, you can run `git clean -xdf && ' 'git reset --hard HEAD` to wipe all local changes.'), contains('Failed, see above for details.'), @@ -122,20 +128,32 @@ void main() { }); test('fails immediately if the remote doesn\'t exist', () async { + gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + MockProcess(exitCode: 1), + ]; + Error? commandError; - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - testPluginName - ], errorHandler: (Error e) { + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--package', testPluginName], + errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); - expect(processRunner.results.last.stderr, contains('No such remote')); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to find URL for remote upstream; cannot push tags'), + ])); }); test("doesn't validate the remote if it's not pushing tags", () async { + // Checking the remote should fail. + gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + MockProcess(exitCode: 1), + ]; + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; @@ -148,17 +166,18 @@ void main() { '--no-tag-release' ]); - expect(output.last, 'Done!'); + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Package published!'), + contains('Released [$testPluginName] successfully.'), + ])); }); test('can publish non-flutter package', () async { const String packageName = 'a_package'; createFakePackage(packageName, packagesDir); - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; @@ -171,7 +190,15 @@ void main() { '--no-tag-release' ]); - expect(output.last, 'Done!'); + expect( + output, + containsAllInOrder( + [ + contains('Running `pub publish ` in /packages/a_package...'), + contains('Package published!'), + ], + ), + ); }); }); @@ -190,8 +217,12 @@ void main() { '--no-tag-release' ]); - expect(output, contains('Foo')); - expect(output, contains('Bar')); + expect( + output, + containsAllInOrder([ + contains('Foo'), + contains('Bar'), + ])); }); test('forwards input from the user to `pub publish`', () async { @@ -288,7 +319,10 @@ void main() { '--no-tag-release', ]); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); expect( output, containsAllInOrder([ @@ -310,10 +344,10 @@ void main() { '--no-push-tags', ]); - final String? tag = (await gitDir - .runCommand(['show-ref', '$testPluginName-v0.0.1'])) - .stdout as String?; - expect(tag, isNotEmpty); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-tag', ['$testPluginName-v0.0.1'], null))); }); test('only if publishing succeeded', () async { @@ -336,20 +370,14 @@ void main() { containsAllInOrder([ contains('Publish foo failed.'), ])); - final String? tag = (await gitDir.runCommand( - ['show-ref', '$testPluginName-v0.0.1'], - throwOnError: false)) - .stdout as String?; - expect(tag, isEmpty); + expect( + gitProcessRunner.recordedCalls, + isNot(contains( + const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); }); }); group('Pushes tags', () { - setUp(() async { - await gitDir.runCommand( - ['remote', 'add', 'upstream', 'http://localhost:8000']); - }); - test('requires user confirmation', () async { processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'help'; @@ -369,7 +397,6 @@ void main() { }); test('to upstream by default', () async { - await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -380,15 +407,19 @@ void main() { testPluginName, ]); - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall('git-push', + ['upstream', '$testPluginName-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Released [$testPluginName] successfully.'), + ])); }); test('does not ask for user input if the --skip-confirmation flag is on', () async { - await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); @@ -400,14 +431,18 @@ void main() { testPluginName, ]); - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall('git-push', + ['upstream', '$testPluginName-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Released [$testPluginName] successfully.'), + ])); }); test('to upstream by default, dry run', () async { - await gitDir.runCommand(['tag', 'garbage']); // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; @@ -415,7 +450,10 @@ void main() { final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--package', testPluginName, '--dry-run']); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); expect( output, containsAllInOrder([ @@ -428,8 +466,6 @@ void main() { }); test('to different remotes based on a flag', () async { - await gitDir.runCommand( - ['remote', 'add', 'origin', 'http://localhost:8001']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -442,10 +478,15 @@ void main() { 'origin', ]); - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'origin'); - expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['origin', '$testPluginName-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Released [$testPluginName] successfully.'), + ])); }); test('only if tagging and pushing to remotes are both enabled', () async { @@ -459,20 +500,21 @@ void main() { '--no-tag-release', ]); - expect(processRunner.pushTagsArgs.isEmpty, isTrue); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Package published!'), + contains('Released [$testPluginName] successfully.'), + ])); }); }); group('Auto release (all-changed flag)', () { - setUp(() async { - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand( - ['remote', 'add', 'upstream', 'http://localhost:8000']); - }); - test('can release newly created plugins', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', @@ -511,8 +553,11 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), ); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -530,13 +575,14 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); test('can release newly created plugins, while there are existing plugins', @@ -578,11 +624,24 @@ void main() { ); commandRunner.addCommand(command); - // Prepare an exiting plugin and tag it + // The existing plugin. createFakePlugin('plugin0', packagesDir); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - await gitDir.runCommand(['tag', 'plugin0-v0.0.1']); + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + // federated + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); + + // Git results for plugin0 having been released already, and plugin1 and + // plugin2 being new. + gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + MockProcess(stdout: 'plugin0-v0.0.1\n') + ]; + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; @@ -591,19 +650,6 @@ void main() { final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - processRunner.pushTagsArgs.clear(); - - // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); - // federated - final Directory pluginDir2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - - output.addAll(await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~'])); - expect( output, containsAllInOrder([ @@ -614,13 +660,14 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); test('can release newly created plugins, dry run', () async { @@ -658,10 +705,12 @@ void main() { // federated final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. - processRunner.mockPublishCompleteCode = 1; + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint( @@ -687,18 +736,21 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test('version change triggers releases.', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', - 'versions': [], + 'versions': ['0.0.1'], }; const Map httpResponsePlugin2 = { 'name': 'plugin2', - 'versions': [], + 'versions': ['0.0.1'], }; final MockClient mockClient = MockClient((http.Request request) async { @@ -722,57 +774,23 @@ void main() { commandRunner.addCommand(command); // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), + version: '0.0.2'); + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', - 'Done!' - ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); - - processRunner.pushTagsArgs.clear(); - - final List plugin1Pubspec = - pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); - plugin1Pubspec[plugin1Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.2'; - pluginDir1 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin1Pubspec.join('\n')); - final List plugin2Pubspec = - pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); - plugin2Pubspec[plugin2Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.2'; - pluginDir2 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin2Pubspec.join('\n')); - await gitDir.runCommand(['add', '-A']); - await gitDir - .runCommand(['commit', '-m', 'Update versions to 0.0.2']); - final List output2 = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( @@ -785,14 +803,14 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); }); test( @@ -800,12 +818,12 @@ void main() { () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', - 'versions': [], + 'versions': ['0.0.1'], }; const Map httpResponsePlugin2 = { 'name': 'plugin2', - 'versions': [], + 'versions': ['0.0.1'], }; final MockClient mockClient = MockClient((http.Request request) async { @@ -829,55 +847,23 @@ void main() { commandRunner.addCommand(command); // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); + pluginDir2.deleteSync(recursive: true); + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - expect( - output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', - 'Done!' - ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); - - processRunner.pushTagsArgs.clear(); - - final List plugin1Pubspec = - pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); - plugin1Pubspec[plugin1Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.2'; - pluginDir1 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin1Pubspec.join('\n')); - - pluginDir2.deleteSync(recursive: true); - - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand([ - 'commit', - '-m', - 'Update plugin1 versions to 0.0.2, delete plugin2' - ]); - final List output2 = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( @@ -890,15 +876,13 @@ void main() { 'Packages released: plugin1', 'Done!' ])); - - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs.length, 3); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); }); - test('Exiting versions do not trigger release, also prints out message.', + test('Existing versions do not trigger release, also prints out message.', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', @@ -931,17 +915,23 @@ void main() { commandRunner.addCommand(command); // Non-federated - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'), + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - await gitDir.runCommand(['tag', 'plugin1-v0.0.2']); - await gitDir.runCommand(['tag', 'plugin2-v0.0.2']); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + MockProcess( + stdout: 'plugin1-v0.0.2\n' + 'plugin2-v0.0.2\n') + ]; final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); @@ -958,11 +948,14 @@ void main() { 'Done!' ])); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test( - 'Exiting versions do not trigger release, but fail if the tags do not exist.', + 'Existing versions do not trigger release, but fail if the tags do not exist.', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', @@ -995,27 +988,41 @@ void main() { commandRunner.addCommand(command); // Non-federated - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'), + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; Error? commandError; - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--all-changed', - '--base-sha=HEAD~' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~'], + errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + output, + containsAllInOrder([ + contains('The version 0.0.2 of plugin1 has already been published'), + contains( + 'However, the git release tag for this version (plugin1-v0.0.2) is not found.'), + contains('The version 0.0.2 of plugin2 has already been published'), + contains( + 'However, the git release tag for this version (plugin2-v0.0.2) is not found.'), + ])); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test('No version change does not release any plugins', () async { @@ -1025,20 +1032,11 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - - pluginDir1.childFile('plugin1.dart').createSync(); - pluginDir2.childFile('plugin2.dart').createSync(); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add dart files']); - - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('plugin1.dart').path}\n' + '${pluginDir2.childFile('plugin2.dart').path}\n') + ]; final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); @@ -1051,7 +1049,10 @@ void main() { 'No version updates in this commit.', 'Done!' ])); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test('Do not release flutter_plugin_tools', () async { @@ -1080,11 +1081,9 @@ void main() { final Directory flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: flutterPluginTools.childFile('pubspec.yaml').path) + ]; final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); @@ -1101,19 +1100,18 @@ void main() { 'Running `pub publish ` in ${flutterPluginTools.path}...\n', ), isFalse); - expect(processRunner.pushTagsArgs, isEmpty); - processRunner.pushTagsArgs.clear(); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); }); } class TestProcessRunner extends ProcessRunner { - final List results = []; // Most recent returned publish process. late MockProcess mockPublishProcess; final List mockPublishArgs = []; - final MockProcessResult mockPushTagsResult = MockProcessResult(); - final List pushTagsArgs = []; String? mockPublishStdout; String? mockPublishStderr; @@ -1129,15 +1127,8 @@ class TestProcessRunner extends ProcessRunner { Encoding stdoutEncoding = io.systemEncoding, Encoding stderrEncoding = io.systemEncoding, }) async { - // Don't ever really push tags. - if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { - pushTagsArgs.addAll(args); - return mockPushTagsResult; - } - final io.ProcessResult result = io.Process.runSync(executable, args, workingDirectory: workingDir?.path); - results.add(result); if (result.exitCode != 0) { throw ToolExit(result.exitCode); } From f4a6fc84a4d0c6783b9196ab65b2f208077690fb Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 09:10:15 -0400 Subject: [PATCH 117/249] [flutter_plugin_tool] Fix CHANGELOG validation failure summary (#4266) The error summary for a CHANGELOG validation failure was written when the only thing being checked was that the versions matched, but now there are other ways to fail as well (i.e., leaving NEXT). This fixes the summary message to be more generic so that it doesn't mislead people who hit validation failures. While adding the test for this message, I discovered that almost all of the tests were actually talking to pub.dev, causing their behavior to in some cases depend on whether a package with that name happened to have been published, and if so what its version was. In order to make the tests hermetic and predictable, this fixes that by making all tests use a mock HTTP client. --- .../tool/lib/src/version_check_command.dart | 2 +- .../tool/test/version_check_command_test.dart | 66 +++++++------------ 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 67a81b967a8..6b49c40d66b 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -178,7 +178,7 @@ class VersionCheckCommand extends PackageLoopingCommand { if (!(await _validateChangelogVersion(package, pubspec: pubspec, pubspecVersionChanged: versionChanged))) { - errors.add('pubspec.yaml and CHANGELOG.md have different versions'); + errors.add('CHANGELOG.md failed validation.'); } return errors.isEmpty diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 7765073feb0..9ab7c57089a 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -54,11 +54,15 @@ void main() { late List> gitDirCommands; Map gitShowResponses; late MockGitDir gitDir; + // Ignored if mockHttpResponse is set. + int mockHttpStatus; + Map? mockHttpResponse; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); + gitDirCommands = >[]; gitShowResponses = {}; gitDir = MockGitDir(); @@ -81,9 +85,21 @@ void main() { } return Future.value(mockProcessResult); }); + + // Default to simulating the plugin never having been published. + mockHttpStatus = 404; + mockHttpResponse = null; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(mockHttpResponse), + mockHttpResponse == null ? mockHttpStatus : 200); + }); + processRunner = RecordingProcessRunner(); final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform, gitDir: gitDir); + processRunner: processRunner, + platform: mockPlatform, + gitDir: gitDir, + httpClient: mockClient); runner = CommandRunner( 'version_check_command', 'Test for $VersionCheckCommand'); @@ -456,7 +472,9 @@ void main() { output, containsAllInOrder([ contains('When bumping the version for release, the NEXT section ' - 'should be incorporated into the new version\'s release notes.') + 'should be incorporated into the new version\'s release notes.'), + contains('plugin:\n' + ' CHANGELOG.md failed validation.'), ]), ); }); @@ -497,7 +515,7 @@ void main() { }); test('allows valid against pub', () async { - const Map httpResponse = { + mockHttpResponse = { 'name': 'some_package', 'versions': [ '0.0.1', @@ -505,15 +523,6 @@ void main() { '1.0.0', ], }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -531,22 +540,13 @@ void main() { }); test('denies invalid against pub', () async { - const Map httpResponse = { + mockHttpResponse = { 'name': 'some_package', 'versions': [ '0.0.1', '0.0.2', ], }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -578,15 +578,7 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N test( 'throw and print error message if http request failed when checking against pub', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('xx', 400); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); + mockHttpStatus = 400; createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -609,7 +601,7 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N contains(''' ${indentation}Error fetching version on pub for plugin. ${indentation}HTTP Status 400 -${indentation}HTTP response: xx +${indentation}HTTP response: null ''') ]), ); @@ -617,15 +609,7 @@ ${indentation}HTTP response: xx test('when checking against pub, allow any version if http status is 404.', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('xx', 404); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); + mockHttpStatus = 404; createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { From 419cbe735483b101a426e6fd71eb61e0f22895f3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:01:06 -0400 Subject: [PATCH 118/249] [flutter_plugin_tools] Check 'implements' for unpublished plugins (#4273) --- .../tool/lib/src/pubspec_check_command.dart | 19 +++--- .../tool/test/pubspec_check_command_test.dart | 64 +++++++++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index def2adaf278..29f9ea733a0 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -97,6 +97,16 @@ class PubspecCheckCommand extends PackageLoopingCommand { printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); } + if (isPlugin) { + final String? error = _checkForImplementsError(pubspec, package: package); + if (error != null) { + printError('$indentation$error'); + passing = false; + } + } + + // Ignore metadata that's only relevant for published packages if the + // packages is not intended for publishing. if (pubspec.publishTo != 'none') { final List repositoryErrors = _checkForRepositoryLinkErrors(pubspec, package: package); @@ -114,15 +124,6 @@ class PubspecCheckCommand extends PackageLoopingCommand { '${indentation * 2}$_expectedIssueLinkFormat'); passing = false; } - - if (isPlugin) { - final String? error = - _checkForImplementsError(pubspec, package: package); - if (error != null) { - printError('$indentation$error'); - passing = false; - } - } } return passing; diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 833f7b601e5..c5d36013c40 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -55,6 +55,7 @@ void main() { String? repositoryPackagesDirRelativePath, bool includeHomepage = false, bool includeIssueTracker = true, + bool publishable = true, }) { final String repositoryPath = repositoryPackagesDirRelativePath ?? name; final String repoLink = 'https://github.com/flutter/' @@ -69,6 +70,7 @@ ${includeRepository ? 'repository: $repoLink' : ''} ${includeHomepage ? 'homepage: $repoLink' : ''} ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} version: 1.0.0 +${publishable ? '' : 'publish_to: \'none\''} '''; } @@ -567,5 +569,67 @@ ${devDependenciesSection()} ]), ); }); + + test('validates some properties even for unpublished packages', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + // Environment section is in the wrong location. + // Missing 'implements'. + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true, publishable: false)} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +${environmentSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + contains('Missing "implements: plugin_a" in "plugin" section.'), + ]), + ); + }); + + test('ignores some checks for unpublished packages', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + // Missing metadata that is only useful for published packages, such as + // repository and issue tracker. + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection( + 'plugin', + isPlugin: true, + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin...'), + contains('No issues found!'), + ]), + ); + }); }); } From e7ef3168bf27a3485f543596a189fd9a8ba5c5de Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:05:28 -0400 Subject: [PATCH 119/249] [flutter_plugin_tools] Move publish tests to RecordingProcessRunner (#4269) Replaces almost all of the `TestProcessRunner`, which was specific to the `publish` tests, with the repo-standard `RecordingProcessRunner` (which now has most of the capabilities these tests need). This finishes aligning these tests with the rest of the repository tests, so they will be easier to maintain as part of the overall repository. To support this, `RecordingProcessRunner` was modified slightly to return a succeeding, no-output process by default for `start`. That makes it consistent with its existing `run` behavior, so is a good change in general. --- .../tool/test/publish_check_command_test.dart | 17 - .../test/publish_plugin_command_test.dart | 333 ++++++++---------- script/tool/test/util.dart | 5 +- 3 files changed, 152 insertions(+), 203 deletions(-) diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 65b0cb54547..e1ab0e224e4 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -49,11 +49,6 @@ void main() { final Directory plugin2Dir = createFakePlugin('plugin_tools_test_package_b', packagesDir); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - MockProcess(), - ]; - await runCapturingPrint(runner, ['publish-check']); expect( @@ -87,10 +82,6 @@ void main() { final Directory dir = createFakePlugin('c', packagesDir); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - ]; - expect(() => runCapturingPrint(runner, ['publish-check']), throwsA(isA())); }); @@ -245,10 +236,6 @@ void main() { createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - ]; - final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -318,10 +305,6 @@ void main() { await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - ]; - bool hasError = false; final List output = await runCapturingPrint( runner, ['publish-check', '--machine'], diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 40018b6edb6..663c2633a9d 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -10,7 +10,6 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.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:http/http.dart' as http; import 'package:http/testing.dart'; @@ -23,13 +22,11 @@ import 'mocks.dart'; import 'util.dart'; void main() { - const String testPluginName = 'foo'; + final String flutterCommand = getFlutterCommand(const LocalPlatform()); late Directory packagesDir; - late Directory pluginDir; late MockGitDir gitDir; late TestProcessRunner processRunner; - late RecordingProcessRunner gitProcessRunner; late CommandRunner commandRunner; late MockStdin mockStdin; late FileSystem fileSystem; @@ -44,25 +41,21 @@ void main() { setUp(() async { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - // TODO(stuartmorgan): Move this from setup to individual tests. - pluginDir = - createFakePlugin(testPluginName, packagesDir, examples: []); - assert(pluginDir != null && pluginDir.existsSync()); - gitProcessRunner = RecordingProcessRunner(); + processRunner = TestProcessRunner(); gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { final List arguments = invocation.positionalArguments[0]! as List; - // Attach the first argument to the command to make targeting the mock - // results easier. + // Route git calls through the process runner, to make mock output + // consistent with outer processes. Attach the first argument to the + // command to make targeting the mock results easier. final String gitCommand = arguments.removeAt(0); - return gitProcessRunner.run('git-$gitCommand', arguments); + return processRunner.run('git-$gitCommand', arguments); }); - processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand(packagesDir, @@ -99,18 +92,17 @@ void main() { }); test('refuses to proceed with dirty files', () async { - gitProcessRunner.mockProcessesForExecutable['git-status'] = [ + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable['git-status'] = [ MockProcess(stdout: '?? ${pluginDir.childFile('tmp').path}\n') ]; Error? commandError; - final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--package', 'foo', '--no-push-tags'], + errorHandler: (Error e) { commandError = e; }); @@ -128,13 +120,15 @@ void main() { }); test('fails immediately if the remote doesn\'t exist', () async { - gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable['git-remote'] = [ MockProcess(exitCode: 1), ]; Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', testPluginName], + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'foo'], errorHandler: (Error e) { commandError = e; }); @@ -149,19 +143,18 @@ void main() { }); test("doesn't validate the remote if it's not pushing tags", () async { + createFakePlugin('foo', packagesDir, examples: []); + // Checking the remote should fail. - gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + processRunner.mockProcessesForExecutable['git-remote'] = [ MockProcess(exitCode: 1), ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - - final List output = - await runCapturingPrint(commandRunner, [ + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release' ]); @@ -169,17 +162,15 @@ void main() { expect( output, containsAllInOrder([ - contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Running `pub publish ` in /packages/foo...'), contains('Package published!'), - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('can publish non-flutter package', () async { const String packageName = 'a_package'; createFakePackage(packageName, packagesDir); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; final List output = await runCapturingPrint( commandRunner, [ @@ -204,15 +195,21 @@ void main() { group('Publishes package', () { test('while showing all output from pub publish to the user', () async { - processRunner.mockPublishStdout = 'Foo'; - processRunner.mockPublishStderr = 'Bar'; - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); - final List output = - await runCapturingPrint(commandRunner, [ + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess( + stdout: 'Foo', + stderr: 'Bar', + stdoutEncoding: utf8, + stderrEncoding: utf8) // pub publish + ]; + + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release' ]); @@ -226,13 +223,14 @@ void main() { }); test('forwards input from the user to `pub publish`', () async { + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.mockUserInputs.add(utf8.encode('user input')); - processRunner.mockPublishCompleteCode = 0; await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release' ]); @@ -242,35 +240,38 @@ void main() { }); test('forwards --pub-publish-flags to pub publish', () async { - processRunner.mockPublishCompleteCode = 0; + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release', '--pub-publish-flags', '--dry-run,--server=foo' ]); - expect(processRunner.mockPublishArgs.length, 4); - expect(processRunner.mockPublishArgs[0], 'pub'); - expect(processRunner.mockPublishArgs[1], 'publish'); - expect(processRunner.mockPublishArgs[2], '--dry-run'); - expect(processRunner.mockPublishArgs[3], '--server=foo'); + expect( + processRunner.recordedCalls, + contains(ProcessCall( + flutterCommand, + const ['pub', 'publish', '--dry-run', '--server=foo'], + pluginDir.path))); }); test( '--skip-confirmation flag automatically adds --force to --pub-publish-flags', () async { - processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release', '--skip-confirmation', @@ -278,22 +279,27 @@ void main() { '--server=foo' ]); - expect(processRunner.mockPublishArgs.length, 4); - expect(processRunner.mockPublishArgs[0], 'pub'); - expect(processRunner.mockPublishArgs[1], 'publish'); - expect(processRunner.mockPublishArgs[2], '--server=foo'); - expect(processRunner.mockPublishArgs[3], '--force'); + expect( + processRunner.recordedCalls, + contains(ProcessCall( + flutterCommand, + const ['pub', 'publish', '--server=foo', '--force'], + pluginDir.path))); }); test('throws if pub publish fails', () async { - processRunner.mockPublishCompleteCode = 128; + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess(exitCode: 128) // pub publish + ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release', ], errorHandler: (Error e) { @@ -309,18 +315,21 @@ void main() { }); test('publish, dry run', () async { + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--dry-run', '--no-push-tags', '--no-tag-release', ]); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( @@ -335,30 +344,31 @@ void main() { group('Tags release', () { test('with the version and name from the pubspec.yaml', () async { - processRunner.mockPublishCompleteCode = 0; - + createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', ]); - expect( - gitProcessRunner.recordedCalls, - contains(const ProcessCall( - 'git-tag', ['$testPluginName-v0.0.1'], null))); + expect(processRunner.recordedCalls, + contains(const ProcessCall('git-tag', ['foo-v0.0.1'], null))); }); test('only if publishing succeeded', () async { - processRunner.mockPublishCompleteCode = 128; + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess(exitCode: 128) // pub publish + ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', ], errorHandler: (Error e) { commandError = e; @@ -371,7 +381,7 @@ void main() { contains('Publish foo failed.'), ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, isNot(contains( const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); }); @@ -379,7 +389,8 @@ void main() { group('Pushes tags', () { test('requires user confirmation', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'help'; Error? commandError; @@ -387,7 +398,7 @@ void main() { await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', ], errorHandler: (Error e) { commandError = e; }); @@ -397,61 +408,63 @@ void main() { }); test('to upstream by default', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', ]); expect( - gitProcessRunner.recordedCalls, - contains(const ProcessCall('git-push', - ['upstream', '$testPluginName-v0.0.1'], null))); + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('does not ask for user input if the --skip-confirmation flag is on', () async { - processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); + createFakePlugin('foo', packagesDir, examples: []); final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--skip-confirmation', '--package', - testPluginName, + 'foo', ]); expect( - gitProcessRunner.recordedCalls, - contains(const ProcessCall('git-push', - ['upstream', '$testPluginName-v0.0.1'], null))); + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('to upstream by default, dry run', () async { - // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. - processRunner.mockPublishCompleteCode = 1; + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', testPluginName, '--dry-run']); + ['publish-plugin', '--package', 'foo', '--dry-run']); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( @@ -459,57 +472,58 @@ void main() { containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', - 'Tagging release $testPluginName-v0.0.1...', + 'Tagging release foo-v0.0.1...', 'Pushing tag to upstream...', 'Done!' ])); }); test('to different remotes based on a flag', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--remote', 'origin', ]); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( - 'git-push', ['origin', '$testPluginName-v0.0.1'], null))); + 'git-push', ['origin', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('only if tagging and pushing to remotes are both enabled', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-tag-release', ]); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( output, containsAllInOrder([ - contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Running `pub publish ` in /packages/foo...'), contains('Package published!'), - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); }); @@ -553,13 +567,11 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), ); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, @@ -576,11 +588,11 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); @@ -634,17 +646,15 @@ void main() { // Git results for plugin0 having been released already, and plugin1 and // plugin2 being new. - gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + processRunner.mockProcessesForExecutable['git-tag'] = [ MockProcess(stdout: 'plugin0-v0.0.1\n') ]; - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, @@ -661,11 +671,11 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); @@ -706,7 +716,7 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') @@ -737,7 +747,7 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -781,14 +791,12 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, @@ -804,11 +812,11 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); }); @@ -854,14 +862,12 @@ void main() { createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); pluginDir2.deleteSync(recursive: true); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, @@ -877,7 +883,7 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); }); @@ -922,12 +928,12 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + processRunner.mockProcessesForExecutable['git-tag'] = [ MockProcess( stdout: 'plugin1-v0.0.2\n' 'plugin2-v0.0.2\n') @@ -949,7 +955,7 @@ void main() { ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -995,7 +1001,7 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') @@ -1020,7 +1026,7 @@ void main() { 'However, the git release tag for this version (plugin2-v0.0.2) is not found.'), ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -1032,7 +1038,7 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('plugin1.dart').path}\n' '${pluginDir2.childFile('plugin2.dart').path}\n') @@ -1050,7 +1056,7 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -1081,7 +1087,7 @@ void main() { final Directory flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: flutterPluginTools.childFile('pubspec.yaml').path) ]; @@ -1101,75 +1107,41 @@ void main() { ), isFalse); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); }); } -class TestProcessRunner extends ProcessRunner { +/// An extension of [RecordingProcessRunner] that stores 'flutter pub publish' +/// calls so that their input streams can be checked in tests. +class TestProcessRunner extends RecordingProcessRunner { // Most recent returned publish process. late MockProcess mockPublishProcess; - final List mockPublishArgs = []; - - String? mockPublishStdout; - String? mockPublishStderr; - int mockPublishCompleteCode = 0; - - @override - 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 = io.Process.runSync(executable, args, - workingDirectory: workingDir?.path); - if (result.exitCode != 0) { - throw ToolExit(result.exitCode); - } - return result; - } @override Future start(String executable, List args, {Directory? workingDirectory}) async { - /// Never actually publish anything. Start is always and only used for this - /// since it returns something we can route stdin through. - assert(executable == getFlutterCommand(const LocalPlatform()) && + final io.Process process = + await super.start(executable, args, workingDirectory: workingDirectory); + if (executable == getFlutterCommand(const LocalPlatform()) && args.isNotEmpty && args[0] == 'pub' && - args[1] == 'publish'); - mockPublishArgs.addAll(args); - - mockPublishProcess = MockProcess( - exitCode: mockPublishCompleteCode, - stdout: mockPublishStdout, - stderr: mockPublishStderr, - stdoutEncoding: utf8, - stderrEncoding: utf8, - ); - return mockPublishProcess; + args[1] == 'publish') { + mockPublishProcess = process as MockProcess; + } + return process; } } class MockStdin extends Mock implements io.Stdin { List> mockUserInputs = >[]; - late StreamController> _controller; + final StreamController> _controller = StreamController>(); String? readLineOutput; @override Stream transform(StreamTransformer, S> streamTransformer) { - // In the test context, only one `PublishPluginCommand` object is created for a single test case. - // However, sometimes, we need to run multiple commands in a single test case. - // In such situation, this `MockStdin`'s StreamController might be listened to more than once, which is not allowed. - // - // Create a new controller every time so this Stdin could be listened to multiple times. - _controller = StreamController>(); mockUserInputs.forEach(_addUserInputsToSteam); return _controller.stream.transform(streamTransformer); } @@ -1189,12 +1161,3 @@ class MockStdin extends Mock implements io.Stdin { void _addUserInputsToSteam(List input) => _controller.add(input); } - -class MockProcessResult extends Mock implements io.ProcessResult { - MockProcessResult({int exitCode = 0}) : _exitCode = exitCode; - - final int _exitCode; - - @override - int get exitCode => _exitCode; -} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 05aebe82fd7..7bd94fb66e2 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -17,6 +17,8 @@ import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:quiver/collection.dart'; +import 'mocks.dart'; + /// Returns the exe name that command will use when running Flutter on /// [platform]. String getFlutterCommand(Platform platform) => @@ -320,7 +322,8 @@ class RecordingProcessRunner extends ProcessRunner { Future start(String executable, List args, {Directory? workingDirectory}) async { recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); - return Future.value(_getProcessToReturn(executable)); + return Future.value( + _getProcessToReturn(executable) ?? MockProcess()); } io.Process? _getProcessToReturn(String executable) { From dcf97f741f5410dda12f2a58c0d35a857dbec21d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:07:33 -0400 Subject: [PATCH 120/249] [flutter_plugin_tool] Add support for building UWP plugins (#4047) This allows building UWP plugin examples with `build-examples --winuwp`. As with previous pre-stable-template desktop support, this avoids the issue of unstable app templates by running `flutter create` on the fly before trying to build, so a template that will bitrot doesn't need to be checked in. Also adds no-op "support" for `drive-examples --winuwp`, with warnings about it not doing anything. This is to handle the fact that the LUCI recipe is shared between Win32 and UWP, and didn't conditionalize `drive`. Rather than change that, then change it back later, this just adds the no-op support now (since changing the tooling is much easier than changing LUCI recipes currently). This required some supporting tool changes: - Adds the ability to check for the new platform variants in a pubspec - Adds the ability to write test pubspecs that include variants, for testing Part of https://github.com/flutter/flutter/issues/82817 --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/build_examples_command.dart | 64 ++++++-- script/tool/lib/src/common/core.dart | 37 +++-- script/tool/lib/src/common/plugin_utils.dart | 39 ++++- .../tool/lib/src/drive_examples_command.dart | 26 ++- .../test/build_examples_command_test.dart | 129 ++++++++++++--- .../tool/test/common/plugin_utils_test.dart | 148 +++++++++++++----- .../create_all_plugins_app_command_test.dart | 2 +- .../test/drive_examples_command_test.dart | 112 ++++++++----- .../tool/test/lint_android_command_test.dart | 16 +- .../tool/test/native_test_command_test.dart | 124 +++++++-------- script/tool/test/test_command_test.dart | 4 +- script/tool/test/util.dart | 89 ++++++++--- .../tool/test/xcode_analyze_command_test.dart | 42 ++--- 14 files changed, 589 insertions(+), 244 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1881d1bb668..a32fb0016cb 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -4,6 +4,7 @@ - Added a new `android-lint` command to lint Android plugin native code. - Pubspec validation now checks for `implements` in implementation packages. - Pubspec valitation now checks the full relative path of `repository` entries. +- `build-examples` now supports UWP plugins via a `--winuwp` flag. ## 0.5.0 diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index ac5e84b7c3c..e441f61d564 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -16,7 +16,16 @@ import 'common/repository_package.dart'; /// Key for APK. const String _platformFlagApk = 'apk'; -const int _exitNoPlatformFlags = 2; +const int _exitNoPlatformFlags = 3; + +// Flutter build types. These are the values passed to `flutter build `. +const String _flutterBuildTypeAndroid = 'apk'; +const String _flutterBuildTypeIos = 'ios'; +const String _flutterBuildTypeLinux = 'linux'; +const String _flutterBuildTypeMacOS = 'macos'; +const String _flutterBuildTypeWeb = 'web'; +const String _flutterBuildTypeWin32 = 'windows'; +const String _flutterBuildTypeWinUwp = 'winuwp'; /// A command to build the example applications for packages. class BuildExamplesCommand extends PackageLoopingCommand { @@ -30,6 +39,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { argParser.addFlag(kPlatformMacos); argParser.addFlag(kPlatformWeb); argParser.addFlag(kPlatformWindows); + argParser.addFlag(kPlatformWinUwp); argParser.addFlag(kPlatformIos); argParser.addFlag(_platformFlagApk); argParser.addOption( @@ -46,33 +56,40 @@ class BuildExamplesCommand extends PackageLoopingCommand { _platformFlagApk: const _PlatformDetails( 'Android', pluginPlatform: kPlatformAndroid, - flutterBuildType: 'apk', + flutterBuildType: _flutterBuildTypeAndroid, ), kPlatformIos: const _PlatformDetails( 'iOS', pluginPlatform: kPlatformIos, - flutterBuildType: 'ios', + flutterBuildType: _flutterBuildTypeIos, extraBuildFlags: ['--no-codesign'], ), kPlatformLinux: const _PlatformDetails( 'Linux', pluginPlatform: kPlatformLinux, - flutterBuildType: 'linux', + flutterBuildType: _flutterBuildTypeLinux, ), kPlatformMacos: const _PlatformDetails( 'macOS', pluginPlatform: kPlatformMacos, - flutterBuildType: 'macos', + flutterBuildType: _flutterBuildTypeMacOS, ), kPlatformWeb: const _PlatformDetails( 'web', pluginPlatform: kPlatformWeb, - flutterBuildType: 'web', + flutterBuildType: _flutterBuildTypeWeb, ), kPlatformWindows: const _PlatformDetails( - 'Windows', + 'Win32', + pluginPlatform: kPlatformWindows, + pluginPlatformVariant: platformVariantWin32, + flutterBuildType: _flutterBuildTypeWin32, + ), + kPlatformWinUwp: const _PlatformDetails( + 'UWP', pluginPlatform: kPlatformWindows, - flutterBuildType: 'windows', + pluginPlatformVariant: platformVariantWinUwp, + flutterBuildType: _flutterBuildTypeWinUwp, ), }; @@ -107,7 +124,8 @@ class BuildExamplesCommand extends PackageLoopingCommand { final Set<_PlatformDetails> buildPlatforms = <_PlatformDetails>{}; final Set<_PlatformDetails> unsupportedPlatforms = <_PlatformDetails>{}; for (final _PlatformDetails platform in requestedPlatforms) { - if (pluginSupportsPlatform(platform.pluginPlatform, package)) { + if (pluginSupportsPlatform(platform.pluginPlatform, package, + variant: platform.pluginPlatformVariant)) { buildPlatforms.add(platform); } else { unsupportedPlatforms.add(platform); @@ -156,6 +174,22 @@ class BuildExamplesCommand extends PackageLoopingCommand { }) async { final String enableExperiment = getStringArg(kEnableExperiment); + // The UWP template is not yet stable, so the UWP directory + // needs to be created on the fly with 'flutter create .' + Directory? temporaryPlatformDirectory; + if (flutterBuildType == _flutterBuildTypeWinUwp) { + final Directory uwpDirectory = example.directory.childDirectory('winuwp'); + if (!uwpDirectory.existsSync()) { + print('Creating temporary winuwp folder'); + final int exitCode = await processRunner.runAndStream(flutterCommand, + ['create', '--platforms=$kPlatformWinUwp', '.'], + workingDir: example.directory); + if (exitCode == 0) { + temporaryPlatformDirectory = uwpDirectory; + } + } + } + final int exitCode = await processRunner.runAndStream( flutterCommand, [ @@ -167,6 +201,13 @@ class BuildExamplesCommand extends PackageLoopingCommand { ], workingDir: example.directory, ); + + if (temporaryPlatformDirectory != null && + temporaryPlatformDirectory.existsSync()) { + print('Cleaning up ${temporaryPlatformDirectory.path}'); + temporaryPlatformDirectory.deleteSync(recursive: true); + } + return exitCode == 0; } } @@ -176,6 +217,7 @@ class _PlatformDetails { const _PlatformDetails( this.label, { required this.pluginPlatform, + this.pluginPlatformVariant, required this.flutterBuildType, this.extraBuildFlags = const [], }); @@ -186,6 +228,10 @@ class _PlatformDetails { /// The key in a pubspec's platform: entry. final String pluginPlatform; + /// The supportedVariants key under a plugin's [pluginPlatform] entry, if + /// applicable. + final String? pluginPlatformVariant; + /// The `flutter build` build type. final String flutterBuildType; diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index b2be8f56d17..53778eccb87 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -10,24 +10,43 @@ import 'package:yaml/yaml.dart'; /// print destination. typedef Print = void Function(Object? object); -/// Key for windows platform. -const String kPlatformWindows = 'windows'; +/// Key for APK (Android) platform. +const String kPlatformAndroid = 'android'; -/// Key for macos platform. -const String kPlatformMacos = 'macos'; +/// Key for IPA (iOS) platform. +const String kPlatformIos = 'ios'; /// Key for linux platform. const String kPlatformLinux = 'linux'; -/// Key for IPA (iOS) platform. -const String kPlatformIos = 'ios'; - -/// Key for APK (Android) platform. -const String kPlatformAndroid = 'android'; +/// Key for macos platform. +const String kPlatformMacos = 'macos'; /// Key for Web platform. const String kPlatformWeb = 'web'; +/// Key for windows platform. +/// +/// Note that this corresponds to the Win32 variant for flutter commands like +/// `build` and `run`, but is a general platform containing all Windows +/// variants for purposes of the `platform` section of a plugin pubspec). +const String kPlatformWindows = 'windows'; + +/// Key for WinUWP platform. +/// +/// Note that UWP is a platform for the purposes of flutter commands like +/// `build` and `run`, but a variant of the `windows` platform for the purposes +/// of plugin pubspecs). +const String kPlatformWinUwp = 'winuwp'; + +/// Key for Win32 variant of the Windows platform. +const String platformVariantWin32 = 'win32'; + +/// Key for UWP variant of the Windows platform. +/// +/// See the note on [kPlatformWinUwp]. +const String platformVariantWinUwp = 'uwp'; + /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index d9c42e220c0..49da67655e9 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -28,8 +28,12 @@ enum PlatformSupport { /// /// If [requiredMode] is provided, the plugin must have the given type of /// implementation in order to return true. -bool pluginSupportsPlatform(String platform, RepositoryPackage package, - {PlatformSupport? requiredMode}) { +bool pluginSupportsPlatform( + String platform, + RepositoryPackage package, { + PlatformSupport? requiredMode, + String? variant, +}) { assert(platform == kPlatformIos || platform == kPlatformAndroid || platform == kPlatformWeb || @@ -65,9 +69,34 @@ bool pluginSupportsPlatform(String platform, RepositoryPackage package, } // 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); + if (requiredMode != null) { + final bool federated = platformEntry.containsKey('default_package'); + if (federated != (requiredMode == PlatformSupport.federated)) { + return false; + } + } + + // If a variant is specified, check for that variant. + if (variant != null) { + const String variantsKey = 'supportedVariants'; + if (platformEntry.containsKey(variantsKey)) { + if (!(platformEntry['supportedVariants']! as YamlList) + .contains(variant)) { + return false; + } + } else { + // Platforms with variants have a default variant when unspecified for + // backward compatibility. Must match the flutter tool logic. + const Map defaultVariants = { + kPlatformWindows: platformVariantWin32, + }; + if (variant != defaultVariants[platform]) { + return false; + } + } + } + + return true; } on FileSystemException { return false; } on YamlException { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 3605dcce1f2..b3434b0659f 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -36,7 +36,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { argParser.addFlag(kPlatformWeb, help: 'Runs the web implementation of the examples'); argParser.addFlag(kPlatformWindows, - help: 'Runs the Windows implementation of the examples'); + help: 'Runs the Windows (Win32) implementation of the examples'); + argParser.addFlag(kPlatformWinUwp, + help: + 'Runs the UWP implementation of the examples [currently a no-op]'); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -67,6 +70,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { kPlatformMacos, kPlatformWeb, kPlatformWindows, + kPlatformWinUwp, ]; final int platformCount = platformSwitches .where((String platform) => getBoolArg(platform)) @@ -81,6 +85,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { throw ToolExit(_exitNoPlatformFlags); } + if (getBoolArg(kPlatformWinUwp)) { + logWarning('Driving UWP applications is not yet supported'); + } + String? androidDevice; if (getBoolArg(kPlatformAndroid)) { final List devices = await _getDevicesForPlatform('android'); @@ -116,6 +124,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { ], if (getBoolArg(kPlatformWindows)) kPlatformWindows: ['-d', 'windows'], + // TODO(stuartmorgan): Check these flags once drive supports UWP: + // https://github.com/flutter/flutter/issues/82821 + if (getBoolArg(kPlatformWinUwp)) + kPlatformWinUwp: ['-d', 'winuwp'], }; } @@ -132,7 +144,17 @@ class DriveExamplesCommand extends PackageLoopingCommand { final List deviceFlags = []; for (final MapEntry> entry in _targetDeviceFlags.entries) { - if (pluginSupportsPlatform(entry.key, package)) { + final String platform = entry.key; + String? variant; + if (platform == kPlatformWindows) { + variant = platformVariantWin32; + } else if (platform == kPlatformWinUwp) { + variant = platformVariantWinUwp; + // TODO(stuartmorgan): Remove this once drive supports UWP. + // https://github.com/flutter/flutter/issues/82821 + return PackageResult.skip('Drive does not yet support UWP'); + } + if (pluginSupportsPlatform(platform, package, variant: variant)) { deviceFlags.addAll(entry.value); } else { print('Skipping unsupported platform ${entry.key}...'); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 9c7291c31dd..a17107c18e2 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -56,8 +56,8 @@ void main() { test('fails if building fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }); processRunner @@ -106,8 +106,8 @@ void main() { test('building for iOS', () async { mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -163,8 +163,8 @@ void main() { test('building for Linux', () async { mockPlatform.isLinux = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformLinux: PlatformSupport.inline, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -212,8 +212,8 @@ void main() { test('building for macOS', () async { mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -258,8 +258,8 @@ void main() { test('building for web', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -284,7 +284,7 @@ void main() { }); test( - 'building for Windows when plugin is not set up for Windows results in no-op', + 'building for win32 when plugin is not set up for Windows results in no-op', () async { mockPlatform.isWindows = true; createFakePlugin('plugin', packagesDir); @@ -296,7 +296,7 @@ void main() { output, containsAllInOrder([ contains('Running for plugin'), - contains('Windows is not supported by this plugin'), + contains('Win32 is not supported by this plugin'), ]), ); @@ -305,11 +305,11 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for Windows', () async { + test('building for win32', () async { mockPlatform.isWindows = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformWindows: PlatformSupport.inline + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -321,7 +321,7 @@ void main() { expect( output, containsAllInOrder([ - '\nBUILDING plugin/example for Windows', + '\nBUILDING plugin/example for Win32 (windows)', ]), ); @@ -335,6 +335,91 @@ void main() { ])); }); + test('building for UWP when plugin does not support UWP is a no-op', + () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('UWP is not supported by this plugin'), + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for UWP', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + containsAllInOrder([ + contains('BUILDING plugin/example for UWP (winuwp)'), + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'winuwp'], pluginExampleDirectory.path), + ])); + }); + + test('building for UWP creates a folder if necessary', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + contains('Creating temporary winuwp folder'), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['create', '--platforms=winuwp', '.'], + pluginExampleDirectory.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'winuwp'], pluginExampleDirectory.path), + ])); + }); + test( 'building for Android when plugin is not set up for Android results in no-op', () async { @@ -358,8 +443,8 @@ void main() { test('building for Android', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -387,8 +472,8 @@ void main() { test('enable-experiment flag for Android', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -409,8 +494,8 @@ void main() { test('enable-experiment flag for ios', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index 7f1ba2add00..2e08f725eb4 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -36,13 +36,13 @@ void main() { test('all platforms', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - kPlatformLinux: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - kPlatformWeb: PlatformSupport.inline, - kPlatformWindows: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), })); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); @@ -55,14 +55,12 @@ void main() { test('some platforms', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformLinux: PlatformSupport.inline, - kPlatformWeb: PlatformSupport.inline, - }, - )); + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + })); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); @@ -74,17 +72,15 @@ void main() { test('inline plugins are only detected as inline', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - kPlatformLinux: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - kPlatformWeb: PlatformSupport.inline, - kPlatformWindows: PlatformSupport.inline, - }, - )); + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + })); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, @@ -137,19 +133,16 @@ void main() { }); test('federated plugins are only detected as federated', () async { - const String pluginName = 'plugin'; final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - pluginName, - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.federated, - kPlatformIos: PlatformSupport.federated, - kPlatformLinux: PlatformSupport.federated, - kPlatformMacos: PlatformSupport.federated, - kPlatformWeb: PlatformSupport.federated, - kPlatformWindows: PlatformSupport.federated, - }, - )); + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.federated), + kPlatformIos: const PlatformDetails(PlatformSupport.federated), + kPlatformLinux: const PlatformDetails(PlatformSupport.federated), + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + kPlatformWeb: const PlatformDetails(PlatformSupport.federated), + kPlatformWindows: const PlatformDetails(PlatformSupport.federated), + })); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, @@ -200,5 +193,84 @@ void main() { requiredMode: PlatformSupport.inline), isFalse); }); + + test('windows without variants is only win32', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isFalse); + }); + + test('windows with both variants matches win32 and winuwp', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails( + PlatformSupport.federated, + variants: [platformVariantWin32, platformVariantWinUwp], + ), + })); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isTrue); + }); + + test('win32 plugin is only win32', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails( + PlatformSupport.federated, + variants: [platformVariantWin32], + ), + })); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isFalse); + }); + + test('winup plugin is only winuwp', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }, + )); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isFalse); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isTrue); + }); }); } diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 4439d13c362..0066cc53f61 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -21,7 +21,7 @@ void main() { setUp(() { // Since the core of this command is a call to 'flutter create', the test // has to use the real filesystem. Put everything possible in a unique - // temporary to minimize affect on the host system. + // temporary to minimize effect on the host system. fileSystem = const LocalFileSystem(); testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index bbf865d3edf..85d2326d068 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -127,8 +127,8 @@ void main() { 'example/test_driver/integration_test.dart', 'example/integration_test/foo_test.dart', ], - platformSupport: { - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -192,9 +192,9 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -242,9 +242,9 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -275,9 +275,9 @@ void main() { extraFiles: [ 'example/lib/main.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -311,9 +311,9 @@ void main() { 'example/integration_test/foo_test.dart', 'example/integration_test/ignore_me.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -397,8 +397,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformLinux: PlatformSupport.inline, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }, ); @@ -470,8 +470,8 @@ void main() { 'example/test_driver/plugin.dart', 'example/macos/macos.swift', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -541,8 +541,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -615,8 +615,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformWindows: PlatformSupport.inline + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }, ); @@ -654,6 +654,40 @@ void main() { ])); }); + test('driving UWP is a no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + variants: [platformVariantWinUwp]), + }, + ); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--winuwp', + ]); + + expect( + output, + containsAllInOrder([ + contains('Driving UWP applications is not yet supported'), + contains('Running for plugin'), + contains('SKIPPING: Drive does not yet support UWP'), + contains('No issues found!'), + ]), + ); + + // Output should be empty since running drive-examples --windows on a + // non-Windows plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on an Android plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', @@ -662,8 +696,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), }, ); @@ -712,8 +746,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -745,8 +779,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -800,9 +834,9 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -842,8 +876,8 @@ void main() { 'plugin', packagesDir, examples: [], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -874,8 +908,8 @@ void main() { 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -906,8 +940,8 @@ void main() { extraFiles: [ 'example/test_driver/integration_test.dart', ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -942,8 +976,8 @@ void main() { 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index d0805846863..5670a64f30d 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -43,8 +43,8 @@ void main() { final Directory pluginDir = createFakePlugin('plugin1', packagesDir, extraFiles: [ 'example/android/gradlew', - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline + ], platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }); final Directory androidDir = @@ -74,8 +74,8 @@ void main() { test('fails if gradlew is missing', () async { createFakePlugin('plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }); Error? commandError; @@ -96,8 +96,8 @@ void main() { test('fails if linting finds issues', () async { createFakePlugin('plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }); processRunner.mockProcessesForExecutable['gradlew'] = [ @@ -138,8 +138,8 @@ void main() { test('skips non-inline plugins', () async { createFakePlugin('plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.federated + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.federated) }); final List output = diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index f367dc80182..7b2a3d3ba39 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -115,8 +115,8 @@ void main() { test('reports skips with no tests', () async { final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -154,8 +154,8 @@ void main() { group('iOS', () { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final List output = await runCapturingPrint(runner, @@ -171,8 +171,8 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.federated + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.federated) }); final List output = await runCapturingPrint(runner, @@ -188,8 +188,8 @@ void main() { test('running with correct destination', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -234,8 +234,8 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -298,8 +298,8 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.federated, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), }); final List output = @@ -317,8 +317,8 @@ void main() { test('runs for macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -360,8 +360,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -390,8 +390,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -420,8 +420,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -455,8 +455,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -480,8 +480,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -519,8 +519,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -554,8 +554,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -586,8 +586,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/app/src/test/example_test.java', @@ -618,8 +618,8 @@ void main() { createFakePlugin( 'plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -630,8 +630,8 @@ void main() { createFakePlugin( 'plugin2', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -657,8 +657,8 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -716,8 +716,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, ); @@ -739,8 +739,8 @@ void main() { group('iOS/macOS', () { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -767,8 +767,8 @@ void main() { test('honors unit-only', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -832,8 +832,8 @@ void main() { test('honors integration-only', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -897,8 +897,8 @@ void main() { test('skips when the requested target is not present', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -950,8 +950,8 @@ void main() { test('fails if unable to check for requested target', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1007,10 +1007,10 @@ void main() { 'example/android/gradlew', 'android/src/test/example_test.java', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -1077,8 +1077,8 @@ void main() { test('runs only macOS for a macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1121,8 +1121,8 @@ void main() { test('runs only iOS for a iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -1193,9 +1193,9 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', @@ -1244,9 +1244,9 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 3b350f7d88a..f8aca38d347 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -180,8 +180,8 @@ void main() { 'plugin', packagesDir, extraFiles: ['test/empty_test.dart'], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 7bd94fb66e2..9b92a5d94ac 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -41,6 +41,21 @@ Directory createPackagesDirectory( return packagesDir; } +/// Details for platform support in a plugin. +@immutable +class PlatformDetails { + const PlatformDetails( + this.type, { + this.variants = const [], + }); + + /// The type of support for the platform. + final PlatformSupport type; + + /// Any 'supportVariants' to list in the pubspec. + final List variants; +} + /// Creates a plugin package with the given [name] in [packagesDirectory]. /// /// [platformSupport] is a map of platform string to the support details for @@ -54,8 +69,8 @@ Directory createFakePlugin( Directory parentDirectory, { List examples = const ['example'], List extraFiles = const [], - Map platformSupport = - const {}, + Map platformSupport = + const {}, String? version = '0.0.1', }) { final Directory pluginDirectory = createFakePackage(name, parentDirectory, @@ -143,8 +158,8 @@ void createFakePubspec( String name = 'fake_package', bool isFlutter = true, bool isPlugin = false, - Map platformSupport = - const {}, + Map platformSupport = + const {}, String publishTo = 'http://no_pub_server.com', String? version, }) { @@ -160,12 +175,11 @@ flutter: plugin: platforms: '''; - for (final MapEntry platform + for (final MapEntry platform in platformSupport.entries) { yaml += _pluginPlatformSection(platform.key, platform.value, name); } } - yaml += ''' dependencies: flutter: @@ -186,50 +200,73 @@ publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being } String _pluginPlatformSection( - String platform, PlatformSupport type, String packageName) { - if (type == PlatformSupport.federated) { - return ''' + String platform, PlatformDetails support, String packageName) { + String entry = ''; + // Build the main plugin entry. + if (support.type == PlatformSupport.federated) { + entry = ''' $platform: default_package: ${packageName}_$platform '''; - } - switch (platform) { - case kPlatformAndroid: - return ''' + } else { + switch (platform) { + case kPlatformAndroid: + entry = ''' android: package: io.flutter.plugins.fake pluginClass: FakePlugin '''; - case kPlatformIos: - return ''' + break; + case kPlatformIos: + entry = ''' ios: pluginClass: FLTFakePlugin '''; - case kPlatformLinux: - return ''' + break; + case kPlatformLinux: + entry = ''' linux: pluginClass: FakePlugin '''; - case kPlatformMacos: - return ''' + break; + case kPlatformMacos: + entry = ''' macos: pluginClass: FakePlugin '''; - case kPlatformWeb: - return ''' + break; + case kPlatformWeb: + entry = ''' web: pluginClass: FakePlugin fileName: ${packageName}_web.dart '''; - case kPlatformWindows: - return ''' + break; + case kPlatformWindows: + entry = ''' windows: pluginClass: FakePlugin '''; - default: - assert(false); - return ''; + break; + default: + assert(false, 'Unrecognized platform: $platform'); + break; + } } + + // Add any variants. + if (support.variants.isNotEmpty) { + entry += ''' + supportedVariants: +'''; + for (final String variant in support.variants) { + entry += ''' + - $variant +'''; + } + } + + return entry; } typedef _ErrorHandler = void Function(Error error); diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index 790a526a8ae..10008ae33a1 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -57,8 +57,8 @@ void main() { group('iOS', () { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final List output = @@ -70,8 +70,8 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.federated + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.federated) }); final List output = @@ -83,8 +83,8 @@ void main() { test('runs for iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -126,8 +126,8 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -172,8 +172,8 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.federated, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), }); final List output = await runCapturingPrint( @@ -186,8 +186,8 @@ void main() { test('runs for macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -223,8 +223,8 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -253,9 +253,9 @@ void main() { test('runs both iOS and macOS when supported', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -313,8 +313,8 @@ void main() { test('runs only macOS for a macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -354,8 +354,8 @@ void main() { test('runs only iOS for a iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = From bc3e0ecf857fab495098040606a9f357336b3d2c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 27 Aug 2021 11:36:03 -0400 Subject: [PATCH 121/249] Remove support for bypassing, or prompting for, git tagging (#4275) We never want a plugin to be published without tagging the release, so there's no reason to support the added complexity of these flags. Similarly, once someone has confirmed publishing, we don't want to give them an opt-out for doing the tag. --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/publish_plugin_command.dart | 69 +++----- .../test/publish_plugin_command_test.dart | 150 ++++-------------- script/tool/test/util.dart | 3 +- 4 files changed, 51 insertions(+), 173 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a32fb0016cb..b10237b4591 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,8 @@ - Pubspec validation now checks for `implements` in implementation packages. - Pubspec valitation now checks the full relative path of `repository` entries. - `build-examples` now supports UWP plugins via a `--winuwp` flag. +- **Breaking change**: `publish` no longer accepts `--no-tag-release` or + `--no-push-flags`. Releases now always tag and push. ## 0.5.0 diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index be9e6d30012..8432e342cda 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -66,23 +66,9 @@ class PublishPluginCommand extends PluginCommand { argParser.addMultiOption(_pubFlagsOption, help: 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); - argParser.addFlag( - _tagReleaseOption, - help: 'Whether or not to tag the release.', - defaultsTo: true, - negatable: true, - ); - argParser.addFlag( - _pushTagsOption, - help: - 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', - defaultsTo: true, - negatable: true, - ); argParser.addOption( _remoteOption, - help: - 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', + help: 'The name of the remote to push the tags to.', // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. defaultsTo: 'upstream', ); @@ -104,15 +90,12 @@ class PublishPluginCommand extends PluginCommand { ); argParser.addFlag(_skipConfirmationFlag, help: 'Run the command without asking for Y/N inputs.\n' - 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n' - 'It also skips the y/n inputs when pushing tags to remote.\n', + 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n', defaultsTo: false, negatable: true); } static const String _packageOption = 'package'; - static const String _tagReleaseOption = 'tag-release'; - static const String _pushTagsOption = 'push-tags'; static const String _pubFlagsOption = 'pub-publish-flags'; static const String _remoteOption = 'remote'; static const String _allChangedFlag = 'all-changed'; @@ -150,19 +133,14 @@ class PublishPluginCommand extends PluginCommand { print('Checking local repo...'); final GitDir repository = await gitDir; - - final bool shouldPushTag = getBoolArg(_pushTagsOption); - _RemoteInfo? remote; - if (shouldPushTag) { - final String remoteName = getStringArg(_remoteOption); - final String? remoteUrl = await _verifyRemote(remoteName); - if (remoteUrl == null) { - printError( - 'Unable to find URL for remote $remoteName; cannot push tags'); - throw ToolExit(1); - } - remote = _RemoteInfo(name: remoteName, url: remoteUrl); + final String remoteName = getStringArg(_remoteOption); + final String? remoteUrl = await _verifyRemote(remoteName); + if (remoteUrl == null) { + printError('Unable to find URL for remote $remoteName; cannot push tags'); + throw ToolExit(1); } + final _RemoteInfo remote = _RemoteInfo(name: remoteName, url: remoteUrl); + print('Local repo is ready!'); if (getBoolArg(_dryRunFlag)) { print('=============== DRY RUN ==============='); @@ -187,7 +165,7 @@ class PublishPluginCommand extends PluginCommand { Future _publishAllChangedPackages({ required GitDir baseGitDir, - _RemoteInfo? remoteForTagPush, + required _RemoteInfo remoteForTagPush, }) async { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final List changedPubspecs = @@ -249,24 +227,21 @@ class PublishPluginCommand extends PluginCommand { return packagesFailed.isEmpty; } - // Publish the package to pub with `pub publish`. - // If `_tagReleaseOption` is on, git tag the release. - // If `remoteForTagPush` is non-null, the tag will be pushed to that remote. + // Publish the package to pub with `pub publish`, then git tag the release + // and push the tag to [remoteForTagPush]. // Returns `true` if publishing and tagging are successful. Future _publishAndTagPackage({ required Directory packageDir, - _RemoteInfo? remoteForTagPush, + required _RemoteInfo remoteForTagPush, }) async { if (!await _publishPlugin(packageDir: packageDir)) { return false; } - if (getBoolArg(_tagReleaseOption)) { - if (!await _tagRelease( - packageDir: packageDir, - remoteForPush: remoteForTagPush, - )) { - return false; - } + if (!await _tagRelease( + packageDir: packageDir, + remoteForPush: remoteForTagPush, + )) { + return false; } print('Released [${packageDir.basename}] successfully.'); return true; @@ -479,14 +454,6 @@ Safe to ignore if the package is deleted in this commit. required _RemoteInfo remote, }) async { assert(remote != null && tag != null); - if (!getBoolArg(_skipConfirmationFlag)) { - print('Ready to push $tag to ${remote.url} (y/n)?'); - final String? input = _stdin.readLineSync(); - if (input?.toLowerCase() != 'y') { - print('Tag push canceled.'); - return false; - } - } if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await (await gitDir).runCommand( ['push', remote.name, tag], diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 663c2633a9d..927c146a874 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -80,8 +80,8 @@ void main() { test('requires an existing flag', () async { Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', 'iamerror', '--no-push-tags'], + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'iamerror'], errorHandler: (Error e) { commandError = e; }); @@ -100,8 +100,8 @@ void main() { ]; Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', 'foo', '--no-push-tags'], + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'foo'], errorHandler: (Error e) { commandError = e; }); @@ -141,56 +141,6 @@ void main() { 'Unable to find URL for remote upstream; cannot push tags'), ])); }); - - test("doesn't validate the remote if it's not pushing tags", () async { - createFakePlugin('foo', packagesDir, examples: []); - - // Checking the remote should fail. - processRunner.mockProcessesForExecutable['git-remote'] = [ - MockProcess(exitCode: 1), - ]; - - final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-push-tags', - '--no-tag-release' - ]); - - expect( - output, - containsAllInOrder([ - contains('Running `pub publish ` in /packages/foo...'), - contains('Package published!'), - contains('Released [foo] successfully.'), - ])); - }); - - test('can publish non-flutter package', () async { - const String packageName = 'a_package'; - createFakePackage(packageName, packagesDir); - - final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - packageName, - '--no-push-tags', - '--no-tag-release' - ]); - - expect( - output, - containsAllInOrder( - [ - contains('Running `pub publish ` in /packages/a_package...'), - contains('Package published!'), - ], - ), - ); - }); }); group('Publishes package', () { @@ -206,13 +156,7 @@ void main() { ]; final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-push-tags', - '--no-tag-release' - ]); + commandRunner, ['publish-plugin', '--package', 'foo']); expect( output, @@ -227,13 +171,8 @@ void main() { mockStdin.mockUserInputs.add(utf8.encode('user input')); - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-push-tags', - '--no-tag-release' - ]); + await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'foo']); expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); @@ -247,8 +186,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', - '--no-tag-release', '--pub-publish-flags', '--dry-run,--server=foo' ]); @@ -272,8 +209,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', - '--no-tag-release', '--skip-confirmation', '--pub-publish-flags', '--server=foo' @@ -300,8 +235,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', - '--no-tag-release', ], errorHandler: (Error e) { commandError = e; }); @@ -324,8 +257,6 @@ void main() { '--package', 'foo', '--dry-run', - '--no-push-tags', - '--no-tag-release', ]); expect( @@ -340,6 +271,28 @@ void main() { 'Done!' ])); }); + + test('can publish non-flutter package', () async { + const String packageName = 'a_package'; + createFakePackage(packageName, packagesDir); + + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + packageName, + ]); + + expect( + output, + containsAllInOrder( + [ + contains('Running `pub publish ` in /packages/a_package...'), + contains('Package published!'), + ], + ), + ); + }); }); group('Tags release', () { @@ -349,7 +302,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', ]); expect(processRunner.recordedCalls, @@ -369,7 +321,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', ], errorHandler: (Error e) { commandError = e; }); @@ -388,25 +339,6 @@ void main() { }); group('Pushes tags', () { - test('requires user confirmation', () async { - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.readLineOutput = 'help'; - - Error? commandError; - final List output = - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect(output, contains('Tag push canceled.')); - }); - test('to upstream by default', () async { createFakePlugin('foo', packagesDir, examples: []); @@ -502,30 +434,6 @@ void main() { contains('Released [foo] successfully.'), ])); }); - - test('only if tagging and pushing to remotes are both enabled', () async { - createFakePlugin('foo', packagesDir, examples: []); - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-tag-release', - ]); - - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - expect( - output, - containsAllInOrder([ - contains('Running `pub publish ` in /packages/foo...'), - contains('Package published!'), - contains('Released [foo] successfully.'), - ])); - }); }); group('Auto release (all-changed flag)', () { diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 9b92a5d94ac..74c03648923 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -107,7 +107,8 @@ Directory createFakePackage( final Directory packageDirectory = parentDirectory.childDirectory(name); packageDirectory.createSync(recursive: true); - createFakePubspec(packageDirectory, name: name, isFlutter: isFlutter); + createFakePubspec(packageDirectory, + name: name, isFlutter: isFlutter, version: version); createFakeCHANGELOG(packageDirectory, ''' ## $version * Some changes. From d3a20df4dc977a991c1460d4e6fcc8fab74be6c8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sat, 28 Aug 2021 12:39:15 -0400 Subject: [PATCH 122/249] [flutter_plugin_tools] Switch 'publish' from --package to --packages (#4285) Replaces the `publish`-command-specific `--package` flag with support for `--packages`, and unifies the flow with the existing looping for `--all-changed`. This better aligns the command's API with the rest of the commands, and reduces divergence in the two flows (e.g., `--package` would attempt to publish and fail if the package was already published, whereas now using `--packages` will use the flow that pre-checks against `pub.dev`). It also sets up a structure that will allow easily converting it to the new base package looping command that most other commands now use, which will be done in a follow-up. Since all calls now attempt to contact `pub.dev`, the tests have been adjusted to always mock the HTTP client so they will be hermetic. Part of https://github.com/flutter/flutter/issues/83413 --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/publish_plugin_command.dart | 171 +++++---- .../test/publish_plugin_command_test.dart | 334 +++++------------- 3 files changed, 168 insertions(+), 339 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index b10237b4591..634360461c8 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -7,6 +7,8 @@ - `build-examples` now supports UWP plugins via a `--winuwp` flag. - **Breaking change**: `publish` no longer accepts `--no-tag-release` or `--no-push-flags`. Releases now always tag and push. +- **Breaking change**: `publish`'s `--package` flag has been replaced with the + `--packages` flag used by most other packages. ## 0.5.0 diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 8432e342cda..aafe7868d8d 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -58,11 +59,6 @@ class PublishPluginCommand extends PluginCommand { _stdin = stdinput ?? io.stdin, super(packagesDir, platform: platform, processRunner: processRunner, gitDir: gitDir) { - argParser.addOption( - _packageOption, - help: 'The package to publish.' - 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', - ); argParser.addMultiOption(_pubFlagsOption, help: 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); @@ -75,8 +71,8 @@ class PublishPluginCommand extends PluginCommand { argParser.addFlag( _allChangedFlag, help: - 'Release all plugins that contains pubspec changes at the current commit compares to the base-sha.\n' - 'The $_packageOption option is ignored if this is on.', + 'Release all packages that contains pubspec changes at the current commit compares to the base-sha.\n' + 'The --packages option is ignored if this is on.', defaultsTo: false, ); argParser.addFlag( @@ -95,7 +91,6 @@ class PublishPluginCommand extends PluginCommand { negatable: true); } - static const String _packageOption = 'package'; static const String _pubFlagsOption = 'pub-publish-flags'; static const String _remoteOption = 'remote'; static const String _allChangedFlag = 'all-changed'; @@ -113,7 +108,7 @@ class PublishPluginCommand extends PluginCommand { @override final String description = - 'Attempts to publish the given plugin and tag its release on GitHub.\n' + 'Attempts to publish the given packages and tag the release(s) on GitHub.\n' 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; @@ -123,14 +118,6 @@ class PublishPluginCommand extends PluginCommand { @override Future run() async { - final String packageName = getStringArg(_packageOption); - final bool publishAllChanged = getBoolArg(_allChangedFlag); - if (packageName.isEmpty && !publishAllChanged) { - printError( - 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); - throw ToolExit(1); - } - print('Checking local repo...'); final GitDir repository = await gitDir; final String remoteName = getStringArg(_remoteOption); @@ -146,36 +133,52 @@ class PublishPluginCommand extends PluginCommand { print('=============== DRY RUN ==============='); } - bool successful; - if (publishAllChanged) { - successful = await _publishAllChangedPackages( - baseGitDir: repository, - remoteForTagPush: remote, - ); - } else { - successful = await _publishAndTagPackage( - packageDir: _getPackageDir(packageName), - remoteForTagPush: remote, - ); - } + final List packages = await _getPackagesToProcess() + .where((PackageEnumerationEntry entry) => !entry.excluded) + .toList(); + bool successful = true; + + successful = await _publishPackages( + packages, + baseGitDir: repository, + remoteForTagPush: remote, + ); - _pubVersionFinder.httpClient.close(); await _finish(successful); } - Future _publishAllChangedPackages({ + Stream _getPackagesToProcess() async* { + if (getBoolArg(_allChangedFlag)) { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + + for (final String pubspecPath in changedPubspecs) { + // Convert git's Posix-style paths to a path that matches the current + // filesystem. + final String localStylePubspecPath = + path.joinAll(p.posix.split(pubspecPath)); + final File pubspecFile = packagesDir.fileSystem + .directory((await gitDir).path) + .childFile(localStylePubspecPath); + yield PackageEnumerationEntry(RepositoryPackage(pubspecFile.parent), + excluded: false); + } + } else { + yield* getTargetPackages(filterExcluded: false); + } + } + + Future _publishPackages( + List packages, { required GitDir baseGitDir, required _RemoteInfo remoteForTagPush, }) async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final List changedPubspecs = - await gitVersionFinder.getChangedPubSpecs(); - if (changedPubspecs.isEmpty) { + if (packages.isEmpty) { print('No version updates in this commit.'); return true; } - print('Getting existing tags...'); final io.ProcessResult existingTagsResult = await baseGitDir.runCommand(['tag', '--sort=-committerdate']); final List existingTags = (existingTagsResult.stdout as String) @@ -185,16 +188,11 @@ class PublishPluginCommand extends PluginCommand { final List packagesReleased = []; final List packagesFailed = []; - for (final String pubspecPath in changedPubspecs) { - // Convert git's Posix-style paths to a path that matches the current - // filesystem. - final String localStylePubspecPath = - path.joinAll(p.posix.split(pubspecPath)); - final File pubspecFile = packagesDir.fileSystem - .directory(baseGitDir.path) - .childFile(localStylePubspecPath); + for (final PackageEnumerationEntry entry in packages) { + final RepositoryPackage package = entry.package; + final _CheckNeedsReleaseResult result = await _checkNeedsRelease( - pubspecFile: pubspecFile, + package: package, existingTags: existingTags, ); switch (result) { @@ -203,17 +201,15 @@ class PublishPluginCommand extends PluginCommand { case _CheckNeedsReleaseResult.noRelease: continue; case _CheckNeedsReleaseResult.failure: - packagesFailed.add(pubspecFile.parent.basename); + packagesFailed.add(package.displayName); continue; } print('\n'); - if (await _publishAndTagPackage( - packageDir: pubspecFile.parent, - remoteForTagPush: remoteForTagPush, - )) { - packagesReleased.add(pubspecFile.parent.basename); + if (await _publishAndTagPackage(package, + remoteForTagPush: remoteForTagPush)) { + packagesReleased.add(package.displayName); } else { - packagesFailed.add(pubspecFile.parent.basename); + packagesFailed.add(package.displayName); } print('\n'); } @@ -230,31 +226,32 @@ class PublishPluginCommand extends PluginCommand { // Publish the package to pub with `pub publish`, then git tag the release // and push the tag to [remoteForTagPush]. // Returns `true` if publishing and tagging are successful. - Future _publishAndTagPackage({ - required Directory packageDir, - required _RemoteInfo remoteForTagPush, + Future _publishAndTagPackage( + RepositoryPackage package, { + _RemoteInfo? remoteForTagPush, }) async { - if (!await _publishPlugin(packageDir: packageDir)) { + if (!await _publishPackage(package)) { return false; } if (!await _tagRelease( - packageDir: packageDir, + package, remoteForPush: remoteForTagPush, )) { return false; } - print('Released [${packageDir.basename}] successfully.'); + print('Published ${package.directory.basename} successfully.'); return true; } // Returns a [_CheckNeedsReleaseResult] that indicates the result. Future<_CheckNeedsReleaseResult> _checkNeedsRelease({ - required File pubspecFile, + required RepositoryPackage package, required List existingTags, }) async { + final File pubspecFile = package.pubspecFile; if (!pubspecFile.existsSync()) { print(''' -The file at The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. +The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. Safe to ignore if the package is deleted in this commit. '''); return _CheckNeedsReleaseResult.noRelease; @@ -279,7 +276,8 @@ Safe to ignore if the package is deleted in this commit. return _CheckNeedsReleaseResult.failure; } - // Check if the package named `packageName` with `version` has already published. + // Check if the package named `packageName` with `version` has already + // been published. final Version version = pubspec.version!; final PubVersionFinderResponse pubVersionFinderResponse = await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); @@ -303,15 +301,15 @@ Safe to ignore if the package is deleted in this commit. return _CheckNeedsReleaseResult.release; } - // Publish the plugin. + // Publish the package. // // Returns `true` if successful, `false` otherwise. - Future _publishPlugin({required Directory packageDir}) async { - final bool gitStatusOK = await _checkGitStatus(packageDir); + Future _publishPackage(RepositoryPackage package) async { + final bool gitStatusOK = await _checkGitStatus(package); if (!gitStatusOK) { return false; } - final bool publishOK = await _publish(packageDir); + final bool publishOK = await _publish(package); if (!publishOK) { return false; } @@ -319,15 +317,15 @@ Safe to ignore if the package is deleted in this commit. return true; } - // Tag the release with -v, and, if [remoteForTagPush] + // Tag the release with -v, and, if [remoteForTagPush] // is provided, push it to that remote. // // Return `true` if successful, `false` otherwise. - Future _tagRelease({ - required Directory packageDir, + Future _tagRelease( + RepositoryPackage package, { _RemoteInfo? remoteForPush, }) async { - final String tag = _getTag(packageDir); + final String tag = _getTag(package); print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await (await gitDir).runCommand( @@ -351,6 +349,7 @@ Safe to ignore if the package is deleted in this commit. } Future _finish(bool successful) async { + _pubVersionFinder.httpClient.close(); await _stdinSubscription?.cancel(); _stdinSubscription = null; if (successful) { @@ -361,20 +360,14 @@ Safe to ignore if the package is deleted in this commit. } } - // Returns the packageDirectory based on the package name. - // Throws ToolExit if the `package` doesn't exist. - Directory _getPackageDir(String packageName) { - final Directory packageDir = packagesDir.childDirectory(packageName); - if (!packageDir.existsSync()) { - printError('${packageDir.absolute.path} does not exist.'); - throw ToolExit(1); - } - return packageDir; - } - - Future _checkGitStatus(Directory packageDir) async { + Future _checkGitStatus(RepositoryPackage package) async { final io.ProcessResult statusResult = await (await gitDir).runCommand( - ['status', '--porcelain', '--ignored', packageDir.absolute.path], + [ + 'status', + '--porcelain', + '--ignored', + package.directory.absolute.path + ], throwOnError: false, ); if (statusResult.exitCode != 0) { @@ -402,10 +395,10 @@ Safe to ignore if the package is deleted in this commit. return getRemoteUrlResult.stdout as String?; } - Future _publish(Directory packageDir) async { + Future _publish(RepositoryPackage package) async { final List publishFlags = getStringListArg(_pubFlagsOption); - print( - 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); + print('Running `pub publish ${publishFlags.join(' ')}` in ' + '${package.directory.absolute.path}...\n'); if (getBoolArg(_dryRunFlag)) { return true; } @@ -419,7 +412,7 @@ Safe to ignore if the package is deleted in this commit. final io.Process publish = await processRunner.start( flutterCommand, ['pub', 'publish'] + publishFlags, - workingDirectory: packageDir); + workingDirectory: package.directory); publish.stdout.transform(utf8.decoder).listen((String data) => print(data)); publish.stderr.transform(utf8.decoder).listen((String data) => print(data)); _stdinSubscription ??= _stdin @@ -427,14 +420,14 @@ Safe to ignore if the package is deleted in this commit. .listen((String data) => publish.stdin.writeln(data)); final int result = await publish.exitCode; if (result != 0) { - printError('Publish ${packageDir.basename} failed.'); + printError('Publishing ${package.directory.basename} failed.'); return false; } return true; } - String _getTag(Directory packageDir) { - final File pubspecFile = packageDir.childFile('pubspec.yaml'); + String _getTag(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; final String name = pubspecYaml['name'] as String; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 927c146a874..ae3d768fcc7 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -30,6 +30,8 @@ void main() { late CommandRunner commandRunner; late MockStdin mockStdin; late FileSystem fileSystem; + // Map of package name to mock response. + late Map> mockHttpResponses; void _createMockCredentialFile() { final String credentialPath = PublishPluginCommand.getCredentialPath(); @@ -41,8 +43,20 @@ void main() { setUp(() async { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = TestProcessRunner(); + + mockHttpResponses = >{}; + final MockClient mockClient = MockClient((http.Request request) async { + final String packageName = + request.url.pathSegments.last.replaceAll('.json', ''); + final Map? response = mockHttpResponses[packageName]; + if (response != null) { + return http.Response(json.encode(response), 200); + } + // Default to simulating the plugin never having been published. + return http.Response('', 404); + }); + gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) @@ -58,39 +72,16 @@ void main() { mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand(packagesDir, - processRunner: processRunner, stdinput: mockStdin, gitDir: gitDir)); + ..addCommand(PublishPluginCommand( + packagesDir, + processRunner: processRunner, + stdinput: mockStdin, + gitDir: gitDir, + httpClient: mockClient, + )); }); group('Initial validation', () { - test('requires a package flag', () async { - Error? commandError; - final List output = await runCapturingPrint( - commandRunner, ['publish-plugin'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Must specify a package to publish.'), - ])); - }); - - test('requires an existing flag', () async { - Error? commandError; - final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'iamerror'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect(output, - containsAllInOrder([contains('iamerror does not exist')])); - }); - test('refuses to proceed with dirty files', () async { final Directory pluginDir = createFakePlugin('foo', packagesDir, examples: []); @@ -100,9 +91,11 @@ void main() { ]; Error? commandError; - final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'foo'], - errorHandler: (Error e) { + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + ], errorHandler: (Error e) { commandError = e; }); @@ -128,7 +121,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'foo'], + commandRunner, ['publish-plugin', '--packages=foo'], errorHandler: (Error e) { commandError = e; }); @@ -145,24 +138,34 @@ void main() { group('Publishes package', () { test('while showing all output from pub publish to the user', () async { - createFakePlugin('foo', packagesDir, examples: []); + createFakePlugin('plugin1', packagesDir, examples: []); + createFakePlugin('plugin2', packagesDir, examples: []); processRunner.mockProcessesForExecutable[flutterCommand] = [ MockProcess( stdout: 'Foo', stderr: 'Bar', stdoutEncoding: utf8, - stderrEncoding: utf8) // pub publish + stderrEncoding: utf8), // pub publish for plugin1 + MockProcess( + stdout: 'Baz', + stdoutEncoding: utf8, + stderrEncoding: utf8), // pub publish for plugin1 ]; - final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'foo']); + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--packages=plugin1,plugin2']); expect( output, containsAllInOrder([ + contains('Running `pub publish ` in /packages/plugin1...'), contains('Foo'), contains('Bar'), + contains('Package published!'), + contains('Running `pub publish ` in /packages/plugin2...'), + contains('Baz'), + contains('Package published!'), ])); }); @@ -172,7 +175,7 @@ void main() { mockStdin.mockUserInputs.add(utf8.encode('user input')); await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'foo']); + commandRunner, ['publish-plugin', '--packages=foo']); expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); @@ -184,17 +187,16 @@ void main() { await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', '--pub-publish-flags', - '--dry-run,--server=foo' + '--dry-run,--server=bar' ]); expect( processRunner.recordedCalls, contains(ProcessCall( flutterCommand, - const ['pub', 'publish', '--dry-run', '--server=foo'], + const ['pub', 'publish', '--dry-run', '--server=bar'], pluginDir.path))); }); @@ -207,18 +209,17 @@ void main() { await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', '--skip-confirmation', '--pub-publish-flags', - '--server=foo' + '--server=bar' ]); expect( processRunner.recordedCalls, contains(ProcessCall( flutterCommand, - const ['pub', 'publish', '--server=foo', '--force'], + const ['pub', 'publish', '--server=bar', '--force'], pluginDir.path))); }); @@ -233,8 +234,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', ], errorHandler: (Error e) { commandError = e; }); @@ -243,7 +243,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Publish foo failed.'), + contains('Publishing foo failed.'), ])); }); @@ -254,8 +254,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', '--dry-run', ]); @@ -279,8 +278,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - packageName, + '--packages=$packageName', ]); expect( @@ -300,8 +298,7 @@ void main() { createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', ]); expect(processRunner.recordedCalls, @@ -319,8 +316,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', ], errorHandler: (Error e) { commandError = e; }); @@ -329,7 +325,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Publish foo failed.'), + contains('Publishing foo failed.'), ])); expect( processRunner.recordedCalls, @@ -347,8 +343,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', ]); expect( @@ -358,7 +353,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Released [foo] successfully.'), + contains('Published foo successfully.'), ])); }); @@ -371,8 +366,7 @@ void main() { await runCapturingPrint(commandRunner, [ 'publish-plugin', '--skip-confirmation', - '--package', - 'foo', + '--packages=foo', ]); expect( @@ -382,7 +376,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Released [foo] successfully.'), + contains('Published foo successfully.'), ])); }); @@ -393,7 +387,7 @@ void main() { mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', 'foo', '--dry-run']); + ['publish-plugin', '--packages=foo', '--dry-run']); expect( processRunner.recordedCalls @@ -418,8 +412,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', '--remote', 'origin', ]); @@ -431,43 +424,23 @@ void main() { expect( output, containsAllInOrder([ - contains('Released [foo] successfully.'), + contains('Published foo successfully.'), ])); }); }); group('Auto release (all-changed flag)', () { test('can release newly created plugins', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': [], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': [], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated @@ -492,7 +465,7 @@ void main() { 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', + 'Packages released: plugin1, plugin2/plugin2', 'Done!' ])); expect( @@ -507,43 +480,21 @@ void main() { test('can release newly created plugins, while there are existing plugins', () async { - const Map httpResponsePlugin0 = { + mockHttpResponses['plugin0'] = { 'name': 'plugin0', 'versions': ['0.0.1'], }; - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': [], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': [], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin0.json') { - return http.Response(json.encode(httpResponsePlugin0), 200); - } else if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // The existing plugin. createFakePlugin('plugin0', packagesDir); // Non-federated @@ -575,7 +526,7 @@ void main() { 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', + 'Packages released: plugin1, plugin2/plugin2', 'Done!' ])); expect( @@ -589,35 +540,16 @@ void main() { }); test('can release newly created plugins, dry run', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': [], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': [], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated @@ -651,7 +583,7 @@ void main() { 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Tagging release plugin2-v0.0.1...', 'Pushing tag to upstream...', - 'Packages released: plugin1, plugin2', + 'Packages released: plugin1, plugin2/plugin2', 'Done!' ])); expect( @@ -661,36 +593,16 @@ void main() { }); test('version change triggers releases.', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.1'], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['0.0.1'], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); @@ -716,7 +628,7 @@ void main() { 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', + 'Packages released: plugin1, plugin2/plugin2', 'Done!' ])); expect( @@ -732,36 +644,16 @@ void main() { test( 'delete package will not trigger publish but exit the command successfully.', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.1'], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['0.0.1'], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); @@ -786,7 +678,7 @@ void main() { 'Checking local repo...', 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'The file at The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n', + 'The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n', 'Packages released: plugin1', 'Done!' ])); @@ -798,36 +690,16 @@ void main() { test('Existing versions do not trigger release, also prints out message.', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.2'], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['0.0.2'], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); @@ -871,36 +743,16 @@ void main() { test( 'Existing versions do not trigger release, but fail if the tags do not exist.', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.2'], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['0.0.2'], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); @@ -970,29 +822,11 @@ void main() { }); test('Do not release flutter_plugin_tools', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'flutter_plugin_tools', 'versions': [], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'flutter_plugin_tools.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - final Directory flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); processRunner.mockProcessesForExecutable['git-diff'] = [ From c7e7f651080cf8a7ad5d8e69c74b22930971c859 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 30 Aug 2021 14:51:58 -0400 Subject: [PATCH 123/249] [flutter_plugin_tool] Add support for running Windows unit tests (#4276) Implements support for `--windows` in `native-test`, for unit tests only. The structure of the new code has most of the new functionality in a generic utility for running GoogleTest test binaries, so that it can be trivially extended to Linux support in a follow-up once the Linux test PoC has landed. This runs the recently-added `url_launcher_windows` unit test. However, it's not yet run in CI since it needs LUCI bringup; that will be done one this support is in place. Requires new logic to check if a plugin contains native code, and some new test utility plumbing to generate plugins whose pubspecs indicate that they only contain Dart code to test it, to allow filtering filtering out the FFI-based Windows plugins. Part of flutter/flutter#82445 --- script/tool/CHANGELOG.md | 1 + script/tool/lib/src/common/file_utils.dart | 20 ++ script/tool/lib/src/common/plugin_utils.dart | 88 +++++--- script/tool/lib/src/native_test_command.dart | 89 +++++++- .../tool/lib/src/publish_plugin_command.dart | 12 +- script/tool/test/common/file_utils_test.dart | 32 +++ .../tool/test/common/plugin_utils_test.dart | 68 ++++++ .../tool/test/native_test_command_test.dart | 208 +++++++++++++++++- script/tool/test/util.dart | 73 +++--- 9 files changed, 510 insertions(+), 81 deletions(-) create mode 100644 script/tool/lib/src/common/file_utils.dart create mode 100644 script/tool/test/common/file_utils_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 634360461c8..0edb106f099 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,7 @@ - Pubspec validation now checks for `implements` in implementation packages. - Pubspec valitation now checks the full relative path of `repository` entries. - `build-examples` now supports UWP plugins via a `--winuwp` flag. +- `native-test` now supports `--windows` for unit tests. - **Breaking change**: `publish` no longer accepts `--no-tag-release` or `--no-push-flags`. Releases now always tag and push. - **Breaking change**: `publish`'s `--package` flag has been replaced with the diff --git a/script/tool/lib/src/common/file_utils.dart b/script/tool/lib/src/common/file_utils.dart new file mode 100644 index 00000000000..3c2f2f18f95 --- /dev/null +++ b/script/tool/lib/src/common/file_utils.dart @@ -0,0 +1,20 @@ +// 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'; + +/// Returns a [File] created by appending all but the last item in [components] +/// to [base] as subdirectories, then appending the last as a file. +/// +/// Example: +/// childFileWithSubcomponents(rootDir, ['foo', 'bar', 'baz.txt']) +/// creates a File representing /rootDir/foo/bar/baz.txt. +File childFileWithSubcomponents(Directory base, List components) { + Directory dir = base; + final String basename = components.removeLast(); + for (final String directoryName in components) { + dir = dir.childDirectory(directoryName); + } + return dir.childFile(basename); +} diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 49da67655e9..06af675e71e 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -17,7 +17,7 @@ enum PlatformSupport { federated, } -/// Returns whether the given [package] is a Flutter [platform] plugin. +/// Returns true if [package] is a Flutter [platform] plugin. /// /// It checks this by looking for the following pattern in the pubspec: /// @@ -30,7 +30,7 @@ enum PlatformSupport { /// implementation in order to return true. bool pluginSupportsPlatform( String platform, - RepositoryPackage package, { + RepositoryPackage plugin, { PlatformSupport? requiredMode, String? variant, }) { @@ -41,32 +41,12 @@ bool pluginSupportsPlatform( platform == kPlatformWindows || platform == kPlatformLinux); try { - final YamlMap pubspecYaml = - loadYaml(package.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 == kPlatformIos || platform == kPlatformAndroid; - } - return false; - } - final YamlMap? platformEntry = platforms[platform] as YamlMap?; + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); if (platformEntry == null) { return false; } + // If the platform entry is present, then it supports the platform. Check // for required mode if specified. if (requiredMode != null) { @@ -97,9 +77,67 @@ bool pluginSupportsPlatform( } return true; + } on YamlException { + return false; + } +} + +/// Returns true if [plugin] includes native code for [platform], as opposed to +/// being implemented entirely in Dart. +bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { + if (platform == kPlatformWeb) { + // Web plugins are always Dart-only. + return false; + } + try { + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); + if (platformEntry == null) { + return false; + } + // All other platforms currently use pluginClass for indicating the native + // code in the plugin. + final String? pluginClass = platformEntry['pluginClass'] as String?; + // TODO(stuartmorgan): Remove the check for 'none' once none of the plugins + // in the repository use that workaround. See + // https://github.com/flutter/flutter/issues/57497 for context. + return pluginClass != null && pluginClass != 'none'; } on FileSystemException { return false; } on YamlException { return false; } } + +/// Returns the +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +/// section from [plugin]'s pubspec.yaml, or null if either it is not present, +/// or the pubspec couldn't be read. +YamlMap? _readPlatformPubspecSectionForPlugin( + String platform, RepositoryPackage plugin) { + try { + final File pubspecFile = plugin.pubspecFile; + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + if (flutterSection == null) { + return null; + } + final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; + if (pluginSection == null) { + return null; + } + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; + if (platforms == null) { + return null; + } + return platforms[platform] as YamlMap?; + } on FileSystemException { + return null; + } on YamlException { + return null; + } +} diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 725cf23a2e9..5120ad10b87 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -40,6 +40,7 @@ class NativeTestCommand extends PackageLoopingCommand { argParser.addFlag(kPlatformAndroid, help: 'Runs Android tests'); argParser.addFlag(kPlatformIos, help: 'Runs iOS tests'); argParser.addFlag(kPlatformMacos, help: 'Runs macOS tests'); + argParser.addFlag(kPlatformWindows, help: 'Runs Windows tests'); // By default, both unit tests and integration tests are run, but provide // flags to disable one or the other. @@ -80,6 +81,7 @@ this command. kPlatformAndroid: _PlatformDetails('Android', _testAndroid), kPlatformIos: _PlatformDetails('iOS', _testIos), kPlatformMacos: _PlatformDetails('macOS', _testMacOS), + kPlatformWindows: _PlatformDetails('Windows', _testWindows), }; _requestedPlatforms = _platforms.keys .where((String platform) => getBoolArg(platform)) @@ -96,6 +98,11 @@ this command. throw ToolExit(exitInvalidArguments); } + if (getBoolArg(kPlatformWindows) && getBoolArg(_integrationTestFlag)) { + logWarning('This command currently only supports unit tests for Windows. ' + 'See https://github.com/flutter/flutter/issues/70233.'); + } + // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { String destination = getStringArg(_iosDestinationFlag); @@ -119,16 +126,20 @@ this command. Future runForPackage(RepositoryPackage package) async { final List testPlatforms = []; for (final String platform in _requestedPlatforms) { - if (pluginSupportsPlatform(platform, package, + if (!pluginSupportsPlatform(platform, package, requiredMode: PlatformSupport.inline)) { - testPlatforms.add(platform); - } else { print('No implementation for ${_platforms[platform]!.label}.'); + continue; } + if (!pluginHasNativeCodeForPlatform(platform, package)) { + print('No native code for ${_platforms[platform]!.label}.'); + continue; + } + testPlatforms.add(platform); } if (testPlatforms.isEmpty) { - return PackageResult.skip('Not implemented for target platform(s).'); + return PackageResult.skip('Nothing to test for target platform(s).'); } final _TestMode mode = _TestMode( @@ -228,6 +239,8 @@ this command. final bool hasIntegrationTests = exampleHasNativeIntegrationTests(example); + // TODO(stuartmorgan): Make !hasUnitTests fatal. See + // https://github.com/flutter/flutter/issues/85469 if (mode.unit && !hasUnitTests) { _printNoExampleTestsMessage(example, 'Android unit'); } @@ -335,6 +348,9 @@ this command. for (final RepositoryPackage example in plugin.getExamples()) { final String exampleName = example.displayName; + // TODO(stuartmorgan): Always check for RunnerTests, and make it fatal if + // no examples have it. See + // https://github.com/flutter/flutter/issues/85469 if (testTarget != null) { final Directory project = example.directory .childDirectory(platform.toLowerCase()) @@ -387,6 +403,71 @@ this command. return _PlatformResult(overallResult); } + Future<_PlatformResult> _testWindows( + RepositoryPackage plugin, _TestMode mode) async { + if (mode.integrationOnly) { + return _PlatformResult(RunState.skipped); + } + + bool isTestBinary(File file) { + return file.basename.endsWith('_test.exe') || + file.basename.endsWith('_tests.exe'); + } + + return _runGoogleTestTests(plugin, + buildDirectoryName: 'windows', isTestBinary: isTestBinary); + } + + /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s + /// build directory for which [isTestBinary] is true, and runs all of them, + /// returning the overall result. + /// + /// The binaries are assumed to be Google Test test binaries, thus returning + /// zero for success and non-zero for failure. + Future<_PlatformResult> _runGoogleTestTests( + RepositoryPackage plugin, { + required String buildDirectoryName, + required bool Function(File) isTestBinary, + }) async { + final List testBinaries = []; + for (final RepositoryPackage example in plugin.getExamples()) { + final Directory buildDir = example.directory + .childDirectory('build') + .childDirectory(buildDirectoryName); + if (!buildDir.existsSync()) { + continue; + } + testBinaries.addAll(buildDir + .listSync(recursive: true) + .whereType() + .where(isTestBinary) + .where((File file) { + // Only run the debug build of the unit tests, to avoid running the + // same tests multiple times. + final List components = path.split(file.path); + return components.contains('debug') || components.contains('Debug'); + })); + } + + if (testBinaries.isEmpty) { + final String binaryExtension = platform.isWindows ? '.exe' : ''; + printError( + 'No test binaries found. At least one *_test(s)$binaryExtension ' + 'binary should be built by the example(s)'); + return _PlatformResult(RunState.failed, + error: 'No $buildDirectoryName unit tests found'); + } + + bool passing = true; + for (final File test in testBinaries) { + print('Running ${test.basename}...'); + final int exitCode = + await processRunner.runAndStream(test.path, []); + passing &= exitCode == 0; + } + return _PlatformResult(passing ? RunState.succeeded : RunState.failed); + } + /// Prints a standard format message indicating that [platform] tests for /// [plugin]'s [example] are about to be run. void _printRunningExampleTestsMessage( diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index aafe7868d8d..e210152ecf0 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -18,6 +18,7 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; +import 'common/file_utils.dart'; import 'common/git_version_finder.dart'; import 'common/plugin_command.dart'; import 'common/process_runner.dart'; @@ -154,13 +155,10 @@ class PublishPluginCommand extends PluginCommand { await gitVersionFinder.getChangedPubSpecs(); for (final String pubspecPath in changedPubspecs) { - // Convert git's Posix-style paths to a path that matches the current - // filesystem. - final String localStylePubspecPath = - path.joinAll(p.posix.split(pubspecPath)); - final File pubspecFile = packagesDir.fileSystem - .directory((await gitDir).path) - .childFile(localStylePubspecPath); + // git outputs a relativa, Posix-style path. + final File pubspecFile = childFileWithSubcomponents( + packagesDir.fileSystem.directory((await gitDir).path), + p.posix.split(pubspecPath)); yield PackageEnumerationEntry(RepositoryPackage(pubspecFile.parent), excluded: false); } diff --git a/script/tool/test/common/file_utils_test.dart b/script/tool/test/common/file_utils_test.dart new file mode 100644 index 00000000000..e3986842a96 --- /dev/null +++ b/script/tool/test/common/file_utils_test.dart @@ -0,0 +1,32 @@ +// 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/file_utils.dart'; +import 'package:test/test.dart'; + +void main() { + test('works on Posix', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.posix); + + final Directory base = fileSystem.directory('/').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + + expect(file.absolute.path, '/base/foo/bar/baz.txt'); + }); + + test('works on Windows', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.windows); + + final Directory base = fileSystem.directory(r'C:\').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + + expect(file.absolute.path, r'C:\base\foo\bar\baz.txt'); + }); +} diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index 2e08f725eb4..ac619e2622e 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -273,4 +273,72 @@ void main() { isTrue); }); }); + + group('pluginHasNativeCodeForPlatform', () { + test('returns false for web', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformWeb, plugin), isFalse); + }); + + test('returns false for a native-only plugin', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + }); + + test('returns true for a native+Dart plugin', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: true, hasDartCode: true), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: true, hasDartCode: true), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: true, hasDartCode: true), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + }); + + test('returns false for a Dart-only plugin', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: false, hasDartCode: true), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: false, hasDartCode: true), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: false, hasDartCode: true), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isFalse); + }); + }); } diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 7b2a3d3ba39..3613a808d9b 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -9,6 +9,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/native_test_command.dart'; import 'package:test/test.dart'; @@ -57,7 +58,7 @@ final Map _kDeviceListMap = { void main() { const String _kDestination = '--ios-destination'; - group('test native_test_command', () { + group('test native_test_command on Posix', () { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; @@ -164,7 +165,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for iOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -181,7 +182,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for iOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -291,7 +292,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for macOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -309,7 +310,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for macOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -707,7 +708,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for Android.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ]), ); }); @@ -1173,6 +1174,7 @@ void main() { '--android', '--ios', '--macos', + '--windows', _kDestination, 'foo_destination', ]); @@ -1183,7 +1185,38 @@ void main() { contains('No implementation for Android.'), contains('No implementation for iOS.'), contains('No implementation for macOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skips Dart-only plugins', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + hasDartCode: true, hasNativeCode: false), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + hasDartCode: true, hasNativeCode: false), + }, + ); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--windows', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No native code for macOS.'), + contains('No native code for Windows.'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); @@ -1295,4 +1328,165 @@ void main() { }); }); }); + + group('test native_test_command on Windows', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockPlatform = MockPlatform(isWindows: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final NativeTestCommand command = NativeTestCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'native_test_command', 'Test for native_test_command'); + runner.addCommand(command); + }); + + group('Windows', () { + test('runs unit tests', () async { + const String testBinaryRelativePath = + 'build/windows/foo/Debug/bar/plugin_test.exe'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + + test('only runs debug unit tests', () async { + const String debugTestBinaryRelativePath = + 'build/windows/foo/Debug/bar/plugin_test.exe'; + const String releaseTestBinaryRelativePath = + 'build/windows/foo/Release/bar/plugin_test.exe'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$debugTestBinaryRelativePath', + 'example/$releaseTestBinaryRelativePath' + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final File debugTestBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...debugTestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + // Only the debug version should be run. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(debugTestBinary.path, const [], null), + ])); + }); + + test('fails if there are no unit tests', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('fails if a unit test fails', () async { + const String testBinaryRelativePath = + 'build/windows/foo/Debug/bar/plugin_test.exe'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + processRunner.mockProcessesForExecutable[testBinary.path] = + [MockProcess(exitCode: 1)]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + }); + }); } diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 74c03648923..e053100172c 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -10,6 +10,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.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'; @@ -47,6 +48,8 @@ class PlatformDetails { const PlatformDetails( this.type, { this.variants = const [], + this.hasNativeCode = true, + this.hasDartCode = false, }); /// The type of support for the platform. @@ -54,6 +57,16 @@ class PlatformDetails { /// Any 'supportVariants' to list in the pubspec. final List variants; + + /// Whether or not the plugin includes native code. + /// + /// Ignored for web, which does not have native code. + final bool hasNativeCode; + + /// Whether or not the plugin includes Dart code. + /// + /// Ignored for web, which always has native code. + final bool hasDartCode; } /// Creates a plugin package with the given [name] in [packagesDirectory]. @@ -130,15 +143,10 @@ Directory createFakePackage( } } - final FileSystem fileSystem = packageDirectory.fileSystem; final p.Context posixContext = p.posix; for (final String file in extraFiles) { - final List newFilePath = [ - packageDirectory.path, - ...posixContext.split(file) - ]; - final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath)); - newFile.createSync(recursive: true); + childFileWithSubcomponents(packageDirectory, posixContext.split(file)) + .createSync(recursive: true); } return packageDirectory; @@ -210,49 +218,38 @@ String _pluginPlatformSection( default_package: ${packageName}_$platform '''; } else { + final List lines = [ + ' $platform:', + ]; switch (platform) { case kPlatformAndroid: - entry = ''' - android: - package: io.flutter.plugins.fake - pluginClass: FakePlugin -'''; - break; + lines.add(' package: io.flutter.plugins.fake'); + continue nativeByDefault; + nativeByDefault: case kPlatformIos: - entry = ''' - ios: - pluginClass: FLTFakePlugin -'''; - break; case kPlatformLinux: - entry = ''' - linux: - pluginClass: FakePlugin -'''; - break; case kPlatformMacos: - entry = ''' - macos: - pluginClass: FakePlugin -'''; + case kPlatformWindows: + if (support.hasNativeCode) { + final String className = + platform == kPlatformIos ? 'FLTFakePlugin' : 'FakePlugin'; + lines.add(' pluginClass: $className'); + } + if (support.hasDartCode) { + lines.add(' dartPluginClass: FakeDartPlugin'); + } break; case kPlatformWeb: - entry = ''' - web: - pluginClass: FakePlugin - fileName: ${packageName}_web.dart -'''; - break; - case kPlatformWindows: - entry = ''' - windows: - pluginClass: FakePlugin -'''; + lines.addAll([ + ' pluginClass: FakePlugin', + ' fileName: ${packageName}_web.dart', + ]); break; default: assert(false, 'Unrecognized platform: $platform'); break; } + entry = lines.join('\n') + '\n'; } // Add any variants. From 6919432e622f959fae23a387e0c6bd1c30c7fde1 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 31 Aug 2021 10:54:41 -0400 Subject: [PATCH 124/249] [flutter_plugin_tool] Move branch-switching logic from tool_runner.sh to tool (#4268) Eliminates the remaining logic from tool_runner.sh, completing the goal of migrating repository tooling off of bash (both to make maintenance easier, and to better support Windows both locally and in CI). Its branch-based logic is now part of the tool itself, via a new `--packages-for-branch` flag (which is hidden in help since it's only useful for CI). Part of https://github.com/flutter/flutter/issues/86113 --- script/tool/CHANGELOG.md | 4 +- .../tool/lib/src/common/plugin_command.dart | 90 +++++-- script/tool/pubspec.yaml | 2 +- .../tool/test/common/plugin_command_test.dart | 247 +++++++++++++----- 4 files changed, 260 insertions(+), 83 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 0edb106f099..2ef34c184b1 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 0.6.0 - Added Android native integration test support to `native-test`. - Added a new `android-lint` command to lint Android plugin native code. @@ -10,6 +10,8 @@ `--no-push-flags`. Releases now always tag and push. - **Breaking change**: `publish`'s `--package` flag has been replaced with the `--packages` flag used by most other packages. +- **Breaking change** Passing both `--run-on-changed-packages` and `--packages` + is now an error; previously it the former would be ignored. ## 0.5.0 diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index ec51261ab61..514a90b85cc 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -2,6 +2,7 @@ // 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 'dart:math'; import 'package:args/command_runner.dart'; @@ -72,11 +73,18 @@ abstract class PluginCommand extends Command { ); argParser.addFlag(_runOnChangedPackagesArg, help: 'Run the command on changed packages/plugins.\n' - 'If the $_packagesArg 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.'); + 'See $_kBaseSha if a custom base is needed to determine the diff.\n\n' + 'Cannot be combined with $_packagesArg.\n'); + argParser.addFlag(_packagesForBranchArg, + help: + 'This runs on all packages (equivalent to no package selection flag)\n' + 'on master, and behaves like --run-on-changed-packages on any other branch.\n\n' + 'Cannot be combined with $_packagesArg.\n\n' + 'This is intended for use in CI.\n', + hide: true); argParser.addOption(_kBaseSha, help: 'The base sha used to determine git diff. \n' 'This is useful when $_runOnChangedPackagesArg is specified.\n' @@ -89,6 +97,7 @@ abstract class PluginCommand extends Command { static const String _shardCountArg = 'shardCount'; static const String _excludeArg = 'exclude'; static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _packagesForBranchArg = 'packages-for-branch'; static const String _kBaseSha = 'base-sha'; /// The directory containing the plugin packages. @@ -266,15 +275,50 @@ abstract class PluginCommand extends Command { /// is a sibling of the packages directory. This is used for a small number /// of packages in the flutter/packages repository. Stream _getAllPackages() async* { + final Set packageSelectionFlags = { + _packagesArg, + _runOnChangedPackagesArg, + _packagesForBranchArg, + }; + if (packageSelectionFlags + .where((String flag) => argResults!.wasParsed(flag)) + .length > + 1) { + printError('Only one of --$_packagesArg, --$_runOnChangedPackagesArg, or ' + '--$_packagesForBranchArg can be provided.'); + throw ToolExit(exitInvalidArguments); + } + Set plugins = Set.from(getStringListArg(_packagesArg)); + final bool runOnChangedPackages; + if (getBoolArg(_runOnChangedPackagesArg)) { + runOnChangedPackages = true; + } else if (getBoolArg(_packagesForBranchArg)) { + final String? branch = await _getBranch(); + if (branch == null) { + printError('Unabled to determine branch; --$_packagesForBranchArg can ' + 'only be used in a git repository.'); + throw ToolExit(exitInvalidArguments); + } else { + runOnChangedPackages = branch != 'master'; + // Log the mode for auditing what was intended to run. + print('--$_packagesForBranchArg: running on ' + '${runOnChangedPackages ? 'changed' : 'all'} packages'); + } + } else { + runOnChangedPackages = false; + } + final Set excludedPluginNames = getExcludedPackageNames(); - final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); - if (plugins.isEmpty && - runOnChangedPackages && - !(await _changesRequireFullTest())) { - plugins = await _getChangedPackages(); + if (runOnChangedPackages) { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final List changedFiles = + await gitVersionFinder.getChangedFiles(); + if (!_changesRequireFullTest(changedFiles)) { + plugins = _getChangedPackages(changedFiles); + } } final Directory thirdPartyPackagesDirectory = packagesDir.parent @@ -374,15 +418,13 @@ abstract class PluginCommand extends Command { 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(); + // Returns packages that have been changed given a list of changed files. + // + // The paths must use POSIX separators (e.g., as provided by git output). + Set _getChangedPackages(List changedFiles) { final Set packages = {}; - for (final String path in allChangedFiles) { - final List pathComponents = path.split('/'); + for (final String path in changedFiles) { + final List pathComponents = p.posix.split(path); final int packagesIndex = pathComponents.indexWhere((String element) => element == 'packages'); if (packagesIndex != -1) { @@ -398,11 +440,19 @@ abstract class PluginCommand extends Command { return packages; } + Future _getBranch() async { + final io.ProcessResult branchResult = await (await gitDir).runCommand( + ['rev-parse', '--abbrev-ref', 'HEAD'], + throwOnError: false); + if (branchResult.exitCode != 0) { + return null; + } + return (branchResult.stdout as String).trim(); + } + // 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(); - + bool _changesRequireFullTest(List changedFiles) { const List specialFiles = [ '.ci.yaml', // LUCI config. '.cirrus.yml', // Cirrus config. @@ -417,9 +467,7 @@ abstract class PluginCommand extends Command { // 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) => + return changedFiles.any((String path) => specialFiles.contains(path) || specialDirectories.any((String dir) => path.startsWith(dir))); } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 02b3ca624b9..7c2bb0b3e3c 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.5.0 +version: 0.6.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 10bdff4e9c5..3ef0d3b3c00 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -7,6 +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/core.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'; @@ -28,8 +29,6 @@ void main() { late MockPlatform mockPlatform; late Directory packagesDir; late Directory thirdPartyPackagesDir; - late List?> gitDirCommands; - late String gitDiffResponse; setUp(() { fileSystem = MemoryFileSystem(); @@ -39,18 +38,15 @@ void main() { .childDirectory('third_party') .childDirectory('packages'); - gitDirCommands = ?>[]; - gitDiffResponse = ''; final MockGitDir 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); - } - return Future.value(mockProcessResult); + final List arguments = + invocation.positionalArguments[0]! as List; + // Attach the first argument to the command to make targeting the mock + // results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); }); processRunner = RecordingProcessRunner(); command = SamplePluginCommand( @@ -184,6 +180,68 @@ void main() { expect(command.plugins, unorderedEquals([])); }); + group('conflicting package selection', () { + test('does not allow --packages with --run-on-changed-packages', + () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--run-on-changed-packages', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of --packages, --run-on-changed-packages, or ' + '--packages-for-branch can be provided.') + ])); + }); + + test('does not allow --packages with --packages-for-branch', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--packages-for-branch', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of --packages, --run-on-changed-packages, or ' + '--packages-for-branch can be provided.') + ])); + }); + + test( + 'does not allow --run-on-changed-packages with --packages-for-branch', + () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--packages-for-branch', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of --packages, --run-on-changed-packages, or ' + '--packages-for-branch can be provided.') + ])); + }); + }); + group('test run-on-changed-packages', () { test('all plugins should be tested if there are no changes.', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); @@ -201,7 +259,9 @@ void main() { test( 'all plugins should be tested if there are no plugin related changes.', () async { - gitDiffResponse = 'AUTHORS'; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'AUTHORS'), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -215,10 +275,12 @@ void main() { }); test('all plugins should be tested if .cirrus.yml changes.', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' .cirrus.yml packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -232,10 +294,12 @@ packages/plugin1/CHANGELOG }); test('all plugins should be tested if .ci.yaml changes', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' .ci.yaml packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -250,10 +314,12 @@ packages/plugin1/CHANGELOG test('all plugins should be tested if anything in .ci/ changes', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' .ci/Dockerfile packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -268,10 +334,12 @@ packages/plugin1/CHANGELOG test('all plugins should be tested if anything in script changes.', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' script/tool_runner.sh packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -286,10 +354,12 @@ packages/plugin1/CHANGELOG test('all plugins should be tested if the root analysis options change.', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' analysis_options.yaml packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -304,10 +374,12 @@ packages/plugin1/CHANGELOG test('all plugins should be tested if formatting options change.', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' .clang-format packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -321,7 +393,9 @@ packages/plugin1/CHANGELOG }); test('Only changed plugin should be tested.', () async { - gitDiffResponse = 'packages/plugin1/plugin1.dart'; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -335,10 +409,12 @@ packages/plugin1/CHANGELOG test('multiple files in one plugin should also test the plugin', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1.dart packages/plugin1/ios/plugin1.m -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -352,10 +428,12 @@ packages/plugin1/ios/plugin1.m test('multiple plugins changed should test all the changed plugins', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); @@ -372,11 +450,13 @@ packages/plugin2/ios/plugin2.m test( 'multiple plugins inside the same plugin group changed should output the plugin group name', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1/plugin1.dart packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart packages/plugin1/plugin1_web/plugin1_web.dart -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); @@ -393,9 +473,11 @@ packages/plugin1/plugin1_web/plugin1_web.dart test( 'changing one plugin in a federated group should include all plugins in the group', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1/plugin1.dart -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); final Directory plugin2 = createFakePlugin('plugin1_platform_interface', @@ -414,35 +496,14 @@ packages/plugin1/plugin1/plugin1.dart [plugin1.path, plugin2.path, plugin3.path])); }); - test( - '--packages flag overrides the behavior of --run-on-changed-packages', - () async { - gitDiffResponse = ''' -packages/plugin1/plugin1.dart -packages/plugin2/ios/plugin2.m -packages/plugin3/plugin3.dart -'''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--packages=plugin1,plugin2', - '--base-sha=master', - '--run-on-changed-packages' - ]); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - test('--exclude flag works with --run-on-changed-packages', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); @@ -459,6 +520,74 @@ packages/plugin3/plugin3.dart }); }); + group('--packages-for-branch', () { + test('only tests changed packages on a branch', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'a-branch'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, unorderedEquals([plugin1.path])); + expect( + output, + containsAllInOrder([ + contains('--packages-for-branch: running on changed packages'), + ])); + }); + + test('tests all packages on master', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'master'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('--packages-for-branch: running on all packages'), + ])); + }); + + test('throws if getting the branch fails', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unabled to determine branch'), + ])); + }); + }); + group('sharding', () { test('distributes evenly when evenly divisible', () async { final List> expectedShards = >[ @@ -625,5 +754,3 @@ class SamplePluginCommand extends PluginCommand { } } } - -class MockProcessResult extends Mock implements ProcessResult {} From da676376b33929cbba6bc8f6db4f7b23c92f2028 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 31 Aug 2021 13:55:01 -0400 Subject: [PATCH 125/249] [flutter_plugin_tool] Migrate 'publish' to new base command (#4290) Moves `publish` to PackageLoopingCommand, giving it the same standardized output and summary that is used by most other commands in the tool. Adds minor new functionality to the base command to allow it to handle the specific needs of publish: - Allows fully customizing the set of packages to loop over, to support --all-changed - Allows customization of a few more strings so the output better matches the functionality (e.g., using 'published' instead of 'ran' in the summary lines). Fixes https://github.com/flutter/flutter/issues/83413 --- .../src/common/package_looping_command.dart | 22 +- .../tool/lib/src/publish_plugin_command.dart | 239 ++++++------------ .../test/publish_plugin_command_test.dart | 140 +++++----- 3 files changed, 163 insertions(+), 238 deletions(-) diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 00caeb30ef4..96dd881bfe0 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -92,6 +92,18 @@ abstract class PackageLoopingCommand extends PluginCommand { /// arguments are invalid), and to set up any run-level state. Future initializeRun() async {} + /// Returns the packages to process. By default, this returns the packages + /// defined by the standard tooling flags and the [inculdeSubpackages] option, + /// but can be overridden for custom package enumeration. + /// + /// Note: Consistent behavior across commands whenever possibel is a goal for + /// this tool, so this should be overridden only in rare cases. + Stream getPackagesToProcess() async* { + yield* includeSubpackages + ? getTargetPackagesAndSubpackages(filterExcluded: false) + : getTargetPackages(filterExcluded: false); + } + /// Runs the command for [package], returning a list of errors. /// /// Errors may either be an empty string if there is no context that should @@ -138,6 +150,9 @@ abstract class PackageLoopingCommand extends PluginCommand { /// context. String get failureListFooter => 'See above for full details.'; + /// The summary string used for a successful run in the final overview output. + String get successSummaryMessage => 'ran'; + /// If true, all printing (including the summary) will be redirected to a /// buffer, and provided in a call to [handleCapturedOutput] at the end of /// the run. @@ -206,9 +221,8 @@ abstract class PackageLoopingCommand extends PluginCommand { await initializeRun(); - final List targetPackages = includeSubpackages - ? await getTargetPackagesAndSubpackages(filterExcluded: false).toList() - : await getTargetPackages(filterExcluded: false).toList(); + final List targetPackages = + await getPackagesToProcess().toList(); final Map results = {}; @@ -346,7 +360,7 @@ abstract class PackageLoopingCommand extends PluginCommand { summary = 'skipped'; style = hadWarning ? Styles.LIGHT_YELLOW : Styles.DARK_GRAY; } else { - summary = 'ran'; + summary = successSummaryMessage; style = hadWarning ? Styles.YELLOW : Styles.GREEN; } if (hadWarning) { diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index e210152ecf0..6da51706ef1 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -7,7 +7,6 @@ import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -20,9 +19,11 @@ import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/file_utils.dart'; import 'common/git_version_finder.dart'; +import 'common/package_looping_command.dart'; import 'common/plugin_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; @immutable class _RemoteInfo { @@ -46,7 +47,7 @@ class _RemoteInfo { /// usage information. /// /// [processRunner], [print], and [stdin] can be overriden for easier testing. -class PublishPluginCommand extends PluginCommand { +class PublishPluginCommand extends PackageLoopingCommand { /// Creates an instance of the publish command. PublishPluginCommand( Directory packagesDir, { @@ -117,38 +118,45 @@ class PublishPluginCommand extends PluginCommand { StreamSubscription? _stdinSubscription; final PubVersionFinder _pubVersionFinder; + // Tags that already exist in the repository. + List _existingGitTags = []; + // The remote to push tags to. + late _RemoteInfo _remote; + + @override + String get successSummaryMessage => 'published'; + + @override + String get failureListHeader => + 'The following packages had failures during publishing:'; + @override - Future run() async { + Future initializeRun() async { print('Checking local repo...'); - final GitDir repository = await gitDir; + + // Ensure that the requested remote is present. final String remoteName = getStringArg(_remoteOption); final String? remoteUrl = await _verifyRemote(remoteName); if (remoteUrl == null) { printError('Unable to find URL for remote $remoteName; cannot push tags'); throw ToolExit(1); } - final _RemoteInfo remote = _RemoteInfo(name: remoteName, url: remoteUrl); + _remote = _RemoteInfo(name: remoteName, url: remoteUrl); + + // Pre-fetch all the repository's tags, to check against when publishing. + final GitDir repository = await gitDir; + final io.ProcessResult existingTagsResult = + await repository.runCommand(['tag', '--sort=-committerdate']); + _existingGitTags = (existingTagsResult.stdout as String).split('\n') + ..removeWhere((String element) => element.isEmpty); - print('Local repo is ready!'); if (getBoolArg(_dryRunFlag)) { - print('=============== DRY RUN ==============='); + print('=============== DRY RUN ==============='); } - - final List packages = await _getPackagesToProcess() - .where((PackageEnumerationEntry entry) => !entry.excluded) - .toList(); - bool successful = true; - - successful = await _publishPackages( - packages, - baseGitDir: repository, - remoteForTagPush: remote, - ); - - await _finish(successful); } - Stream _getPackagesToProcess() async* { + @override + Stream getPackagesToProcess() async* { if (getBoolArg(_allChangedFlag)) { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final List changedPubspecs = @@ -167,92 +175,52 @@ class PublishPluginCommand extends PluginCommand { } } - Future _publishPackages( - List packages, { - required GitDir baseGitDir, - required _RemoteInfo remoteForTagPush, - }) async { - if (packages.isEmpty) { - print('No version updates in this commit.'); - return true; + @override + Future runForPackage(RepositoryPackage package) async { + final PackageResult? checkResult = await _checkNeedsRelease(package); + if (checkResult != null) { + return checkResult; } - final io.ProcessResult existingTagsResult = - await baseGitDir.runCommand(['tag', '--sort=-committerdate']); - final List existingTags = (existingTagsResult.stdout as String) - .split('\n') - ..removeWhere((String element) => element.isEmpty); - - final List packagesReleased = []; - final List packagesFailed = []; - - for (final PackageEnumerationEntry entry in packages) { - final RepositoryPackage package = entry.package; - - final _CheckNeedsReleaseResult result = await _checkNeedsRelease( - package: package, - existingTags: existingTags, - ); - switch (result) { - case _CheckNeedsReleaseResult.release: - break; - case _CheckNeedsReleaseResult.noRelease: - continue; - case _CheckNeedsReleaseResult.failure: - packagesFailed.add(package.displayName); - continue; - } - print('\n'); - if (await _publishAndTagPackage(package, - remoteForTagPush: remoteForTagPush)) { - packagesReleased.add(package.displayName); - } else { - packagesFailed.add(package.displayName); - } - print('\n'); + if (!await _checkGitStatus(package)) { + return PackageResult.fail(['uncommitted changes']); } - if (packagesReleased.isNotEmpty) { - print('Packages released: ${packagesReleased.join(', ')}'); + + if (!await _publish(package)) { + return PackageResult.fail(['publish failed']); } - if (packagesFailed.isNotEmpty) { - printError( - 'Failed to release the following packages: ${packagesFailed.join(', ')}, see above for details.'); + + if (!await _tagRelease(package)) { + return PackageResult.fail(['tagging failed']); } - return packagesFailed.isEmpty; + + print('\nPublished ${package.directory.basename} successfully!'); + return PackageResult.success(); } - // Publish the package to pub with `pub publish`, then git tag the release - // and push the tag to [remoteForTagPush]. - // Returns `true` if publishing and tagging are successful. - Future _publishAndTagPackage( - RepositoryPackage package, { - _RemoteInfo? remoteForTagPush, - }) async { - if (!await _publishPackage(package)) { - return false; - } - if (!await _tagRelease( - package, - remoteForPush: remoteForTagPush, - )) { - return false; - } - print('Published ${package.directory.basename} successfully.'); - return true; + @override + Future completeRun() async { + _pubVersionFinder.httpClient.close(); + await _stdinSubscription?.cancel(); + _stdinSubscription = null; } - // Returns a [_CheckNeedsReleaseResult] that indicates the result. - Future<_CheckNeedsReleaseResult> _checkNeedsRelease({ - required RepositoryPackage package, - required List existingTags, - }) async { + /// Checks whether [package] needs to be released, printing check status and + /// returning one of: + /// - PackageResult.fail if the check could not be completed + /// - PackageResult.skip if no release is necessary + /// - null if releasing should proceed + /// + /// In cases where a non-null result is returned, that should be returned + /// as the final result for the package, without further processing. + Future _checkNeedsRelease(RepositoryPackage package) async { final File pubspecFile = package.pubspecFile; if (!pubspecFile.existsSync()) { - print(''' + logWarning(''' The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. Safe to ignore if the package is deleted in this commit. '''); - return _CheckNeedsReleaseResult.noRelease; + return PackageResult.skip('package deleted'); } final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); @@ -261,17 +229,18 @@ Safe to ignore if the package is deleted in this commit. // Ignore flutter_plugin_tools package when running publishing through flutter_plugin_tools. // TODO(cyanglaz): Make the tool also auto publish flutter_plugin_tools package. // https://github.com/flutter/flutter/issues/85430 - return _CheckNeedsReleaseResult.noRelease; + return PackageResult.skip( + 'publishing flutter_plugin_tools via the tool is not supported'); } if (pubspec.publishTo == 'none') { - return _CheckNeedsReleaseResult.noRelease; + return PackageResult.skip('publish_to: none'); } if (pubspec.version == null) { printError( 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); - return _CheckNeedsReleaseResult.failure; + return PackageResult.fail(['no version']); } // Check if the package named `packageName` with `version` has already @@ -280,49 +249,29 @@ Safe to ignore if the package is deleted in this commit. final PubVersionFinderResponse pubVersionFinderResponse = await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); if (pubVersionFinderResponse.versions.contains(version)) { - final String tagsForPackageWithSameVersion = existingTags.firstWhere( + final String tagsForPackageWithSameVersion = _existingGitTags.firstWhere( (String tag) => tag.split('-v').first == pubspec.name && tag.split('-v').last == version.toString(), orElse: () => ''); - print( - 'The version $version of ${pubspec.name} has already been published'); if (tagsForPackageWithSameVersion.isEmpty) { printError( - 'However, the git release tag for this version (${pubspec.name}-v$version) is not found. Please manually fix the tag then run the command again.'); - return _CheckNeedsReleaseResult.failure; + '${pubspec.name} $version has already been published, however ' + 'the git release tag (${pubspec.name}-v$version) was not found. ' + 'Please manually fix the tag then run the command again.'); + return PackageResult.fail(['published but untagged']); } else { - print('skip.'); - return _CheckNeedsReleaseResult.noRelease; + print('${pubspec.name} $version has already been published.'); + return PackageResult.skip('already published'); } } - return _CheckNeedsReleaseResult.release; - } - - // Publish the package. - // - // Returns `true` if successful, `false` otherwise. - Future _publishPackage(RepositoryPackage package) async { - final bool gitStatusOK = await _checkGitStatus(package); - if (!gitStatusOK) { - return false; - } - final bool publishOK = await _publish(package); - if (!publishOK) { - return false; - } - print('Package published!'); - return true; + return null; } - // Tag the release with -v, and, if [remoteForTagPush] - // is provided, push it to that remote. + // Tag the release with -v, and push it to the remote. // // Return `true` if successful, `false` otherwise. - Future _tagRelease( - RepositoryPackage package, { - _RemoteInfo? remoteForPush, - }) async { + Future _tagRelease(RepositoryPackage package) async { final String tag = _getTag(package); print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { @@ -335,27 +284,15 @@ Safe to ignore if the package is deleted in this commit. } } - if (remoteForPush == null) { - return true; - } - - print('Pushing tag to ${remoteForPush.name}...'); - return await _pushTagToRemote( + print('Pushing tag to ${_remote.name}...'); + final bool success = await _pushTagToRemote( tag: tag, - remote: remoteForPush, + remote: _remote, ); - } - - Future _finish(bool successful) async { - _pubVersionFinder.httpClient.close(); - await _stdinSubscription?.cancel(); - _stdinSubscription = null; - if (successful) { - print('Done!'); - } else { - printError('Failed, see above for details.'); - throw ToolExit(1); + if (success) { + print('Release tagged!'); } + return success; } Future _checkGitStatus(RepositoryPackage package) async { @@ -394,6 +331,7 @@ Safe to ignore if the package is deleted in this commit. } Future _publish(RepositoryPackage package) async { + print('Publishing...'); final List publishFlags = getStringListArg(_pubFlagsOption); print('Running `pub publish ${publishFlags.join(' ')}` in ' '${package.directory.absolute.path}...\n'); @@ -421,6 +359,8 @@ Safe to ignore if the package is deleted in this commit. printError('Publishing ${package.directory.basename} failed.'); return false; } + + print('Package published!'); return true; } @@ -516,14 +456,3 @@ final String _credentialsPath = () { return p.join(cacheDir, 'credentials.json'); }(); - -enum _CheckNeedsReleaseResult { - // The package needs to be released. - release, - - // The package does not need to be released. - noRelease, - - // There's an error when trying to determine whether the package needs to be released. - failure, -} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index ae3d768fcc7..2ea4fc75346 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -108,7 +108,8 @@ void main() { '?? /packages/foo/tmp\n\n' 'If the directory should be clean, you can run `git clean -xdf && ' 'git reset --hard HEAD` to wipe all local changes.'), - contains('Failed, see above for details.'), + contains('foo:\n' + ' uncommitted changes'), ])); }); @@ -264,10 +265,13 @@ void main() { isNot(contains('git-push'))); expect( output, - containsAllInOrder([ - '=============== DRY RUN ===============', - 'Running `pub publish ` in ${pluginDir.path}...\n', - 'Done!' + containsAllInOrder([ + contains('=============== DRY RUN ==============='), + contains('Running for foo'), + contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Tagging release foo-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published foo successfully!'), ])); }); @@ -353,7 +357,8 @@ void main() { expect( output, containsAllInOrder([ - contains('Published foo successfully.'), + contains('Pushing tag to upstream...'), + contains('Published foo successfully!'), ])); }); @@ -376,7 +381,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Published foo successfully.'), + contains('Published foo successfully!'), ])); }); @@ -395,12 +400,12 @@ void main() { isNot(contains('git-push'))); expect( output, - containsAllInOrder([ - '=============== DRY RUN ===============', - 'Running `pub publish ` in ${pluginDir.path}...\n', - 'Tagging release foo-v0.0.1...', - 'Pushing tag to upstream...', - 'Done!' + containsAllInOrder([ + contains('=============== DRY RUN ==============='), + contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Tagging release foo-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published foo successfully!'), ])); }); @@ -424,7 +429,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Published foo successfully.'), + contains('Published foo successfully!'), ])); }); }); @@ -460,13 +465,11 @@ void main() { expect( output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2/plugin2', - 'Done!' + containsAllInOrder([ + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('plugin1 - \x1B[32mpublished\x1B[0m'), + contains('plugin2/plugin2 - \x1B[32mpublished\x1B[0m'), ])); expect( processRunner.recordedCalls, @@ -522,12 +525,8 @@ void main() { expect( output, containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2/plugin2', - 'Done!' ])); expect( processRunner.recordedCalls, @@ -573,18 +572,16 @@ void main() { expect( output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - '=============== DRY RUN ===============', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Tagging release plugin1-v0.0.1...', - 'Pushing tag to upstream...', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Tagging release plugin2-v0.0.1...', - 'Pushing tag to upstream...', - 'Packages released: plugin1, plugin2/plugin2', - 'Done!' + containsAllInOrder([ + contains('=============== DRY RUN ==============='), + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Tagging release plugin1-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published plugin1 successfully!'), + contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Tagging release plugin2-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published plugin2 successfully!'), ])); expect( processRunner.recordedCalls @@ -623,13 +620,11 @@ void main() { ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( output2, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2/plugin2', - 'Done!' + containsAllInOrder([ + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Published plugin1 successfully!'), + contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Published plugin2 successfully!'), ])); expect( processRunner.recordedCalls, @@ -642,7 +637,7 @@ void main() { }); test( - 'delete package will not trigger publish but exit the command successfully.', + 'delete package will not trigger publish but exit the command successfully!', () async { mockHttpResponses['plugin1'] = { 'name': 'plugin1', @@ -674,13 +669,13 @@ void main() { ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( output2, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n', - 'Packages released: plugin1', - 'Done!' + containsAllInOrder([ + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Published plugin1 successfully!'), + contains( + 'The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n'), + contains('SKIPPING: package deleted'), + contains('skipped (with warning)'), ])); expect( processRunner.recordedCalls, @@ -724,14 +719,11 @@ void main() { expect( output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'The version 0.0.2 of plugin1 has already been published', - 'skip.', - 'The version 0.0.2 of plugin2 has already been published', - 'skip.', - 'Done!' + containsAllInOrder([ + contains('plugin1 0.0.2 has already been published'), + contains('SKIPPING: already published'), + contains('plugin2 0.0.2 has already been published'), + contains('SKIPPING: already published'), ])); expect( @@ -778,12 +770,10 @@ void main() { expect( output, containsAllInOrder([ - contains('The version 0.0.2 of plugin1 has already been published'), - contains( - 'However, the git release tag for this version (plugin1-v0.0.2) is not found.'), - contains('The version 0.0.2 of plugin2 has already been published'), - contains( - 'However, the git release tag for this version (plugin2-v0.0.2) is not found.'), + contains('plugin1 0.0.2 has already been published, ' + 'however the git release tag (plugin1-v0.0.2) was not found.'), + contains('plugin2 0.0.2 has already been published, ' + 'however the git release tag (plugin2-v0.0.2) was not found.'), ])); expect( processRunner.recordedCalls @@ -807,14 +797,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - expect( - output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'No version updates in this commit.', - 'Done!' - ])); + expect(output, containsAllInOrder(['Ran for 0 package(s)'])); expect( processRunner.recordedCalls .map((ProcessCall call) => call.executable), @@ -838,14 +821,13 @@ void main() { expect( output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Done!' + containsAllInOrder([ + contains( + 'SKIPPING: publishing flutter_plugin_tools via the tool is not supported') ])); expect( output.contains( - 'Running `pub publish ` in ${flutterPluginTools.path}...\n', + 'Running `pub publish ` in ${flutterPluginTools.path}...', ), isFalse); expect( From 4a92b627b3448959ed3d2455ece299a66d78f64e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 31 Aug 2021 22:57:43 -0400 Subject: [PATCH 126/249] [flutter_plugin_tools] Fix build-examples for packages (#4248) The build-examples command was filtering what it attempted to build by plugin platform, which means it never does anything for non-plugin packages. flutter/packages has steps that run this command, which suggests it used to work and regressed at some point, but nobody noticed; this will re-enable those builds so that we are getting CI coverage that the examples in flutter/packages build. Mostly fixes https://github.com/flutter/flutter/issues/88435 (needs a flutter/packages tool pin roll to pick this up) --- script/tool/CHANGELOG.md | 4 + .../tool/lib/src/build_examples_command.dart | 66 +++++-- .../src/common/package_looping_command.dart | 9 +- script/tool/lib/src/common/plugin_utils.dart | 137 +++++++-------- script/tool/pubspec.yaml | 2 +- .../test/build_examples_command_test.dart | 162 ++++++++++++++++++ .../common/package_looping_command_test.dart | 58 ++++++- 7 files changed, 355 insertions(+), 83 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2ef34c184b1..1f1da3551ef 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0+1 + +- Fixed `build-examples` to work for non-plugin packages. + ## 0.6.0 - Added Android native integration test support to `native-test`. diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index e441f61d564..56c2f5c7dc8 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -117,39 +117,65 @@ class BuildExamplesCommand extends PackageLoopingCommand { Future runForPackage(RepositoryPackage package) async { final List errors = []; + final bool isPlugin = isFlutterPlugin(package); final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries .where( (MapEntry entry) => getBoolArg(entry.key)) .map((MapEntry entry) => entry.value); - final Set<_PlatformDetails> buildPlatforms = <_PlatformDetails>{}; - final Set<_PlatformDetails> unsupportedPlatforms = <_PlatformDetails>{}; - for (final _PlatformDetails platform in requestedPlatforms) { - if (pluginSupportsPlatform(platform.pluginPlatform, package, - variant: platform.pluginPlatformVariant)) { - buildPlatforms.add(platform); - } else { - unsupportedPlatforms.add(platform); - } + + // Platform support is checked at the package level for plugins; there is + // no package-level platform information for non-plugin packages. + final Set<_PlatformDetails> buildPlatforms = isPlugin + ? requestedPlatforms + .where((_PlatformDetails platform) => pluginSupportsPlatform( + platform.pluginPlatform, package, + variant: platform.pluginPlatformVariant)) + .toSet() + : requestedPlatforms.toSet(); + + String platformDisplayList(Iterable<_PlatformDetails> platforms) { + return platforms.map((_PlatformDetails p) => p.label).join(', '); } + if (buildPlatforms.isEmpty) { final String unsupported = requestedPlatforms.length == 1 ? '${requestedPlatforms.first.label} is not supported' - : 'None of [${requestedPlatforms.map((_PlatformDetails p) => p.label).join(',')}] are supported'; + : 'None of [${platformDisplayList(requestedPlatforms)}] are supported'; return PackageResult.skip('$unsupported by this plugin'); } - print('Building for: ' - '${buildPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}'); + print('Building for: ${platformDisplayList(buildPlatforms)}'); + + final Set<_PlatformDetails> unsupportedPlatforms = + requestedPlatforms.toSet().difference(buildPlatforms); if (unsupportedPlatforms.isNotEmpty) { + final List skippedPlatforms = unsupportedPlatforms + .map((_PlatformDetails platform) => platform.label) + .toList(); + skippedPlatforms.sort(); print('Skipping unsupported platform(s): ' - '${unsupportedPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}'); + '${skippedPlatforms.join(', ')}'); } print(''); + bool builtSomething = false; for (final RepositoryPackage example in package.getExamples()) { final String packageName = getRelativePosixPath(example.directory, from: packagesDir); for (final _PlatformDetails platform in buildPlatforms) { + // Repo policy is that a plugin must have examples configured for all + // supported platforms. For packages, just log and skip any requested + // platform that a package doesn't have set up. + if (!isPlugin && + !example.directory + .childDirectory(platform.flutterPlatformDirectory) + .existsSync()) { + print('Skipping ${platform.label} for $packageName; not supported.'); + continue; + } + + builtSomething = true; + String buildPlatform = platform.label; if (platform.label.toLowerCase() != platform.flutterBuildType) { buildPlatform += ' (${platform.flutterBuildType})'; @@ -162,6 +188,15 @@ class BuildExamplesCommand extends PackageLoopingCommand { } } + if (!builtSomething) { + if (isPlugin) { + errors.add('No examples found'); + } else { + return PackageResult.skip( + 'No examples found supporting requested platform(s).'); + } + } + return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); @@ -235,6 +270,11 @@ class _PlatformDetails { /// The `flutter build` build type. final String flutterBuildType; + /// The Flutter platform directory name. + // In practice, this is the same as the plugin platform key for all platforms. + // If that changes, this can be adjusted. + String get flutterPlatformDirectory => pluginPlatform; + /// Any extra flags to pass to `flutter build`. final List extraBuildFlags; } diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 96dd881bfe0..973ac9995cb 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -237,7 +237,14 @@ abstract class PackageLoopingCommand extends PluginCommand { continue; } - final PackageResult result = await runForPackage(entry.package); + PackageResult result; + try { + result = await runForPackage(entry.package); + } catch (e, stack) { + printError(e.toString()); + printError(stack.toString()); + result = PackageResult.fail(['Unhandled exception']); + } if (result.state == RunState.skipped) { final String message = '${indentation}SKIPPING: ${result.details.first}'; diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 06af675e71e..6cfe9928d68 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -17,6 +17,11 @@ enum PlatformSupport { federated, } +/// Returns true if [package] is a Flutter plugin. +bool isFlutterPlugin(RepositoryPackage package) { + return _readPluginPubspecSection(package) != null; +} + /// Returns true if [package] is a Flutter [platform] plugin. /// /// It checks this by looking for the following pattern in the pubspec: @@ -40,46 +45,43 @@ bool pluginSupportsPlatform( platform == kPlatformMacos || platform == kPlatformWindows || platform == kPlatformLinux); - try { - final YamlMap? platformEntry = - _readPlatformPubspecSectionForPlugin(platform, plugin); - if (platformEntry == null) { + + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); + if (platformEntry == null) { + return false; + } + + // If the platform entry is present, then it supports the platform. Check + // for required mode if specified. + if (requiredMode != null) { + final bool federated = platformEntry.containsKey('default_package'); + if (federated != (requiredMode == PlatformSupport.federated)) { return false; } + } - // If the platform entry is present, then it supports the platform. Check - // for required mode if specified. - if (requiredMode != null) { - final bool federated = platformEntry.containsKey('default_package'); - if (federated != (requiredMode == PlatformSupport.federated)) { + // If a variant is specified, check for that variant. + if (variant != null) { + const String variantsKey = 'supportedVariants'; + if (platformEntry.containsKey(variantsKey)) { + if (!(platformEntry['supportedVariants']! as YamlList) + .contains(variant)) { return false; } - } - - // If a variant is specified, check for that variant. - if (variant != null) { - const String variantsKey = 'supportedVariants'; - if (platformEntry.containsKey(variantsKey)) { - if (!(platformEntry['supportedVariants']! as YamlList) - .contains(variant)) { - return false; - } - } else { - // Platforms with variants have a default variant when unspecified for - // backward compatibility. Must match the flutter tool logic. - const Map defaultVariants = { - kPlatformWindows: platformVariantWin32, - }; - if (variant != defaultVariants[platform]) { - return false; - } + } else { + // Platforms with variants have a default variant when unspecified for + // backward compatibility. Must match the flutter tool logic. + const Map defaultVariants = { + kPlatformWindows: platformVariantWin32, + }; + if (variant != defaultVariants[platform]) { + return false; } } - - return true; - } on YamlException { - return false; } + + return true; } /// Returns true if [plugin] includes native code for [platform], as opposed to @@ -89,24 +91,18 @@ bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { // Web plugins are always Dart-only. return false; } - try { - final YamlMap? platformEntry = - _readPlatformPubspecSectionForPlugin(platform, plugin); - if (platformEntry == null) { - return false; - } - // All other platforms currently use pluginClass for indicating the native - // code in the plugin. - final String? pluginClass = platformEntry['pluginClass'] as String?; - // TODO(stuartmorgan): Remove the check for 'none' once none of the plugins - // in the repository use that workaround. See - // https://github.com/flutter/flutter/issues/57497 for context. - return pluginClass != null && pluginClass != 'none'; - } on FileSystemException { - return false; - } on YamlException { + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); + if (platformEntry == null) { return false; } + // All other platforms currently use pluginClass for indicating the native + // code in the plugin. + final String? pluginClass = platformEntry['pluginClass'] as String?; + // TODO(stuartmorgan): Remove the check for 'none' once none of the plugins + // in the repository use that workaround. See + // https://github.com/flutter/flutter/issues/57497 for context. + return pluginClass != null && pluginClass != 'none'; } /// Returns the @@ -118,26 +114,33 @@ bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { /// or the pubspec couldn't be read. YamlMap? _readPlatformPubspecSectionForPlugin( String platform, RepositoryPackage plugin) { - try { - final File pubspecFile = plugin.pubspecFile; - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; - if (flutterSection == null) { - return null; - } - final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; - if (pluginSection == null) { - return null; - } - final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; - if (platforms == null) { - return null; - } - return platforms[platform] as YamlMap?; - } on FileSystemException { + final YamlMap? pluginSection = _readPluginPubspecSection(plugin); + if (pluginSection == null) { + return null; + } + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; + if (platforms == null) { + return null; + } + return platforms[platform] as YamlMap?; +} + +/// Returns the +/// flutter: +/// plugin: +/// platforms: +/// section from [plugin]'s pubspec.yaml, or null if either it is not present, +/// or the pubspec couldn't be read. +YamlMap? _readPluginPubspecSection(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; + if (!pubspecFile.existsSync()) { return null; - } on YamlException { + } + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + if (flutterSection == null) { return null; } + return flutterSection['plugin'] as YamlMap?; } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 7c2bb0b3e3c..adf62ca35a1 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.6.0 +version: 0.6.0+1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index a17107c18e2..d9cbad246d2 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -82,6 +82,35 @@ void main() { ])); }); + test('fails if a plugin has no examples', () async { + createFakePlugin('plugin', packagesDir, + examples: [], + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + MockProcess(exitCode: 1) // flutter packages get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin:\n' + ' No examples found'), + ])); + }); + test('building for iOS when plugin is not set up for iOS results in no-op', () async { mockPlatform.isMacOS = true; @@ -517,5 +546,138 @@ void main() { pluginExampleDirectory.path), ])); }); + + test('logs skipped platforms', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + }); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--ios', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('Skipping unsupported platform(s): iOS, macOS'), + ]), + ); + }); + + group('packages', () { + test('builds when requested platform is supported by example', () async { + final Directory packageDirectory = createFakePackage( + 'package', packagesDir, isFlutter: true, extraFiles: [ + 'example/ios/Runner.xcodeproj/project.pbxproj' + ]); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('BUILDING package/example for iOS'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'build', + 'ios', + '--no-codesign', + ], + packageDirectory.childDirectory('example').path), + ])); + }); + + test('skips non-Flutter examples', () async { + createFakePackage('package', packagesDir, isFlutter: false); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('No examples found supporting requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skips when there is no example', () async { + createFakePackage('package', packagesDir, + isFlutter: true, examples: []); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('No examples found supporting requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip when example does not support requested platform', () async { + createFakePackage('package', packagesDir, + isFlutter: true, + extraFiles: ['example/linux/CMakeLists.txt']); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('Skipping iOS for package/example; not supported.'), + contains('No examples found supporting requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('logs skipped platforms when only some are supported', () async { + final Directory packageDirectory = createFakePackage( + 'package', packagesDir, + isFlutter: true, + extraFiles: ['example/linux/CMakeLists.txt']); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--linux']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('Building for: Android, Linux'), + contains('Skipping Android for package/example; not supported.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'linux'], + packageDirectory.childDirectory('example').path), + ])); + }); + }); }); } diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 721923ae9c6..7cf03960a74 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -36,6 +36,8 @@ const String _errorFile = 'errors'; const String _skipFile = 'skip'; // The filename within a package containing warnings to log during runForPackage. const String _warningFile = 'warnings'; +// The filename within a package indicating that it should throw. +const String _throwFile = 'throw'; void main() { late FileSystem fileSystem; @@ -117,7 +119,7 @@ void main() { expect(() => runCommand(command), throwsA(isA())); }); - test('does not stop looping', () async { + test('does not stop looping on error', () async { createFakePackage('package_a', packagesDir); final Directory failingPackage = createFakePlugin('package_b', packagesDir); @@ -141,6 +143,31 @@ void main() { '${_startHeadingColor}Running for package_c...$_endColor', ])); }); + + test('does not stop looping on exceptions', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + failingPackage.childFile(_throwFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + '${_startHeadingColor}Running for package_c...$_endColor', + ])); + }); }); group('package iteration', () { @@ -437,6 +464,31 @@ void main() { ])); }); + test('logs unhandled exceptions as errors', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + failingPackage.childFile(_throwFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startErrorColor}Exception: Uh-oh$_endColor', + '${_startErrorColor}The following packages had errors:$_endColor', + '$_startErrorColor package_b:\n Unhandled exception$_endColor', + ])); + }); + test('prints run summary on success', () async { final Directory warnPackage1 = createFakePackage('package_a', packagesDir); @@ -657,6 +709,10 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { if (errorFile.existsSync()) { return PackageResult.fail(errorFile.readAsLinesSync()); } + final File throwFile = package.directory.childFile(_throwFile); + if (throwFile.existsSync()) { + throw Exception('Uh-oh'); + } return PackageResult.success(); } From 0c3fb71cc78c7c1fed6e169a7cd9908ddab38ef6 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 1 Sep 2021 14:18:22 -0400 Subject: [PATCH 127/249] [flutter_plugin_tools] Add Linux support to native-test (#4294) - Adds a minimal unit test to url_launcher_linux as a proof of concept. This uses almost exactly the same CMake structure as the Windows version that was added recently. - Adds Linux support for unit tests to `native-test`, sharing almost all of the existing Windows codepath. - Fixes the fact that it it was running the debug version of the unit tests, but `build-examples` only builds release. (On other platforms we run debug unit tests, but on those platforms the test command internally builds the requested unit tests, so the mismatch doesn't matter.) - Enables the new test in CI. Also opportunistically fixes some documentation in `native_test_command.dart` that wasn't updated as more platform support was added. Linux portion of https://github.com/flutter/flutter/issues/82445 --- script/tool/CHANGELOG.md | 4 + script/tool/lib/src/native_test_command.dart | 37 ++++- .../tool/test/native_test_command_test.dart | 156 +++++++++++++++++- 3 files changed, 185 insertions(+), 12 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1f1da3551ef..9b6bbb1f71c 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +- `native-test` now supports `--linux` for unit tests. + ## 0.6.0+1 - Fixed `build-examples` to work for non-plugin packages. diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 5120ad10b87..e50878db790 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -21,7 +21,9 @@ const String _iosDestinationFlag = 'ios-destination'; const int _exitNoIosSimulators = 3; /// The command to run native tests for plugins: -/// - iOS and macOS: XCTests (XCUnitTest and XCUITest) in plugins. +/// - iOS and macOS: XCTests (XCUnitTest and XCUITest) +/// - Android: JUnit tests +/// - Windows and Linux: GoogleTest tests class NativeTestCommand extends PackageLoopingCommand { /// Creates an instance of the test command. NativeTestCommand( @@ -39,6 +41,7 @@ class NativeTestCommand extends PackageLoopingCommand { ); argParser.addFlag(kPlatformAndroid, help: 'Runs Android tests'); argParser.addFlag(kPlatformIos, help: 'Runs iOS tests'); + argParser.addFlag(kPlatformLinux, help: 'Runs Linux tests'); argParser.addFlag(kPlatformMacos, help: 'Runs macOS tests'); argParser.addFlag(kPlatformWindows, help: 'Runs Windows tests'); @@ -63,9 +66,11 @@ class NativeTestCommand extends PackageLoopingCommand { Runs native unit tests and native integration tests. Currently supported platforms: -- Android (unit tests only) +- Android - iOS: requires 'xcrun' to be in your path. +- Linux (unit tests only) - macOS: requires 'xcrun' to be in your path. +- Windows (unit tests only) The example app(s) must be built for all targeted platforms before running this command. @@ -80,6 +85,7 @@ this command. _platforms = { kPlatformAndroid: _PlatformDetails('Android', _testAndroid), kPlatformIos: _PlatformDetails('iOS', _testIos), + kPlatformLinux: _PlatformDetails('Linux', _testLinux), kPlatformMacos: _PlatformDetails('macOS', _testMacOS), kPlatformWindows: _PlatformDetails('Windows', _testWindows), }; @@ -103,6 +109,11 @@ this command. 'See https://github.com/flutter/flutter/issues/70233.'); } + if (getBoolArg(kPlatformLinux) && getBoolArg(_integrationTestFlag)) { + logWarning('This command currently only supports unit tests for Linux. ' + 'See https://github.com/flutter/flutter/issues/70235.'); + } + // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { String destination = getStringArg(_iosDestinationFlag); @@ -418,6 +429,21 @@ this command. buildDirectoryName: 'windows', isTestBinary: isTestBinary); } + Future<_PlatformResult> _testLinux( + RepositoryPackage plugin, _TestMode mode) async { + if (mode.integrationOnly) { + return _PlatformResult(RunState.skipped); + } + + bool isTestBinary(File file) { + return file.basename.endsWith('_test') || + file.basename.endsWith('_tests'); + } + + return _runGoogleTestTests(plugin, + buildDirectoryName: 'linux', isTestBinary: isTestBinary); + } + /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s /// build directory for which [isTestBinary] is true, and runs all of them, /// returning the overall result. @@ -442,10 +468,11 @@ this command. .whereType() .where(isTestBinary) .where((File file) { - // Only run the debug build of the unit tests, to avoid running the - // same tests multiple times. + // Only run the release build of the unit tests, to avoid running the + // same tests multiple times. Release is used rather than debug since + // `build-examples` builds release versions. final List components = path.split(file.path); - return components.contains('debug') || components.contains('Debug'); + return components.contains('release') || components.contains('Release'); })); } diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 3613a808d9b..d1ab11f6e50 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -736,6 +736,147 @@ void main() { }); }); + group('Linux', () { + test('runs unit tests', () async { + const String testBinaryRelativePath = + 'build/linux/foo/release/bar/plugin_test'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + + test('only runs release unit tests', () async { + const String debugTestBinaryRelativePath = + 'build/linux/foo/debug/bar/plugin_test'; + const String releaseTestBinaryRelativePath = + 'build/linux/foo/release/bar/plugin_test'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$debugTestBinaryRelativePath', + 'example/$releaseTestBinaryRelativePath' + ], platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final File releaseTestBinary = childFileWithSubcomponents( + pluginDirectory, + ['example', ...releaseTestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test...'), + contains('No issues found!'), + ]), + ); + + // Only the release version should be run. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(releaseTestBinary.path, const [], null), + ])); + }); + + test('fails if there are no unit tests', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('fails if a unit test fails', () async { + const String testBinaryRelativePath = + 'build/linux/foo/release/bar/plugin_test'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + processRunner.mockProcessesForExecutable[testBinary.path] = + [MockProcess(exitCode: 1)]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running plugin_test...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + }); + // Tests behaviors of implementation that is shared between iOS and macOS. group('iOS/macOS', () { test('fails if xcrun fails', () async { @@ -1352,7 +1493,7 @@ void main() { group('Windows', () { test('runs unit tests', () async { const String testBinaryRelativePath = - 'build/windows/foo/Debug/bar/plugin_test.exe'; + 'build/windows/foo/Release/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' @@ -1384,7 +1525,7 @@ void main() { ])); }); - test('only runs debug unit tests', () async { + test('only runs release unit tests', () async { const String debugTestBinaryRelativePath = 'build/windows/foo/Debug/bar/plugin_test.exe'; const String releaseTestBinaryRelativePath = @@ -1397,8 +1538,9 @@ void main() { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); - final File debugTestBinary = childFileWithSubcomponents(pluginDirectory, - ['example', ...debugTestBinaryRelativePath.split('/')]); + final File releaseTestBinary = childFileWithSubcomponents( + pluginDirectory, + ['example', ...releaseTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ 'native-test', @@ -1414,11 +1556,11 @@ void main() { ]), ); - // Only the debug version should be run. + // Only the release version should be run. expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(debugTestBinary.path, const [], null), + ProcessCall(releaseTestBinary.path, const [], null), ])); }); @@ -1450,7 +1592,7 @@ void main() { test('fails if a unit test fails', () async { const String testBinaryRelativePath = - 'build/windows/foo/Debug/bar/plugin_test.exe'; + 'build/windows/foo/Release/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' From e8d657cf964ae91a3be6607bf00d710cf25fb5e5 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 1 Sep 2021 13:11:03 -0700 Subject: [PATCH 128/249] Add a way to opt a file out of Dart formatting (#4292) --- script/tool/CHANGELOG.md | 4 ++- script/tool/lib/src/format_command.dart | 20 ++++++++++++ script/tool/pubspec.yaml | 2 +- script/tool/test/format_command_test.dart | 37 +++++++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 9b6bbb1f71c..098e57a8c62 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 0.7.0 - `native-test` now supports `--linux` for unit tests. +- Formatting now skips Dart files that contain a line that exactly + matches the string `// This file is hand-formatted.`. ## 0.6.0+1 diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index d09a94b1aef..f24a99436c8 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -206,7 +206,27 @@ class FormatCommand extends PluginCommand { final String fromPath = relativeTo.path; + // Dart files are allowed to have a pragma to disable auto-formatting. This + // was added because Hixie hurts when dealing with what dartfmt does to + // artisanally-formatted Dart, while Stuart gets really frustrated when + // dealing with PRs from newer contributors who don't know how to make Dart + // readable. After much discussion, it was decided that files in the plugins + // and packages repos that really benefit from hand-formatting (e.g. files + // with large blobs of hex literals) could be opted-out of the requirement + // that they be autoformatted, so long as the code's owner was willing to + // bear the cost of this during code reviews. + // In the event that code ownership moves to someone who does not hold the + // same views as the original owner, the pragma can be removed and the file + // auto-formatted. + const String handFormattedExtension = '.dart'; + const String handFormattedPragma = '// This file is hand-formatted.'; + return files + .where((File file) { + // See comment above near [handFormattedPragma]. + return path.extension(file.path) != handFormattedExtension || + !file.readAsLinesSync().contains(handFormattedPragma); + }) .map((File file) => path.relative(file.path, from: fromPath)) .where((String path) => // Ignore files in build/ directories (e.g., headers of frameworks) diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index adf62ca35a1..2569e0ede87 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.6.0+1 +version: 0.7.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index e2bf1e3e6e8..d278bb2940b 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -8,6 +8,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/format_command.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -106,6 +107,42 @@ void main() { ])); }); + test('does not format .dart files with pragma', () async { + const List formattedFiles = [ + 'lib/a.dart', + 'lib/src/b.dart', + 'lib/src/c.dart', + ]; + const String unformattedFile = 'lib/src/d.dart'; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: [ + ...formattedFiles, + unformattedFile, + ], + ); + + final p.Context posixContext = p.posix; + childFileWithSubcomponents(pluginDir, posixContext.split(unformattedFile)) + .writeAsStringSync( + '// copyright bla bla\n// This file is hand-formatted.\ncode...'); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + ..._getPackagesDirRelativePaths(pluginDir, formattedFiles) + ], + packagesDir.path), + ])); + }); + test('fails if flutter format fails', () async { const List files = [ 'lib/a.dart', From 4f63c43ce7a5858037334f055b8cee10f63fed74 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 3 Sep 2021 15:51:04 -0700 Subject: [PATCH 129/249] build-examples .pluginToolsConfig.yaml support (#4305) --- script/tool/CHANGELOG.md | 4 + .../tool/lib/src/build_examples_command.dart | 74 ++++++++++++++++++- script/tool/pubspec.yaml | 2 +- .../test/build_examples_command_test.dart | 48 +++++++++++- 4 files changed, 123 insertions(+), 5 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 098e57a8c62..aa73c65f3e8 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.1 + +- Add support for `.pluginToolsConfig.yaml` in the `build-examples` command. + ## 0.7.0 - `native-test` now supports `--linux` for unit tests. diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 56c2f5c7dc8..82ed074c462 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -16,7 +17,19 @@ import 'common/repository_package.dart'; /// Key for APK. const String _platformFlagApk = 'apk'; +const String _pluginToolsConfigFileName = '.pluginToolsConfig.yaml'; +const String _pluginToolsConfigBuildFlagsKey = 'buildFlags'; +const String _pluginToolsConfigGlobalKey = 'global'; + +const String _pluginToolsConfigExample = ''' +$_pluginToolsConfigBuildFlagsKey: + $_pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" +'''; + const int _exitNoPlatformFlags = 3; +const int _exitInvalidPluginToolsConfig = 4; // Flutter build types. These are the values passed to `flutter build `. const String _flutterBuildTypeAndroid = 'apk'; @@ -99,7 +112,13 @@ class BuildExamplesCommand extends PackageLoopingCommand { @override final String description = 'Builds all example apps (IPA for iOS and APK for Android).\n\n' - 'This command requires "flutter" to be in your path.'; + 'This command requires "flutter" to be in your path.\n\n' + 'A $_pluginToolsConfigFileName file can be placed in an example app ' + 'directory to specify additional build arguments. It should be a YAML ' + 'file with a top-level map containing a single key ' + '"$_pluginToolsConfigBuildFlagsKey" containing a map containing a ' + 'single key "$_pluginToolsConfigGlobalKey" containing a list of build ' + 'arguments.'; @override Future initializeRun() async { @@ -202,6 +221,58 @@ class BuildExamplesCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } + Iterable _readExtraBuildFlagsConfiguration( + Directory directory) sync* { + final File pluginToolsConfig = + directory.childFile(_pluginToolsConfigFileName); + if (pluginToolsConfig.existsSync()) { + final Object? configuration = + loadYaml(pluginToolsConfig.readAsStringSync()); + if (configuration is! YamlMap) { + printError('The $_pluginToolsConfigFileName file must be a YAML map.'); + printError( + 'Currently, the key "$_pluginToolsConfigBuildFlagsKey" is the only one that has an effect.'); + printError( + 'It must itself be a map. Currently, in that map only the key "$_pluginToolsConfigGlobalKey"'); + printError( + 'has any effect; it must contain a list of arguments to pass to the'); + printError('flutter tool.'); + printError(_pluginToolsConfigExample); + throw ToolExit(_exitInvalidPluginToolsConfig); + } + if (configuration.containsKey(_pluginToolsConfigBuildFlagsKey)) { + final Object? buildFlagsConfiguration = + configuration[_pluginToolsConfigBuildFlagsKey]; + if (buildFlagsConfiguration is! YamlMap) { + printError( + 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map.'); + printError( + 'Currently, in that map only the key "$_pluginToolsConfigGlobalKey" has any effect; it must '); + printError( + 'contain a list of arguments to pass to the flutter tool.'); + printError(_pluginToolsConfigExample); + throw ToolExit(_exitInvalidPluginToolsConfig); + } + if (buildFlagsConfiguration.containsKey(_pluginToolsConfigGlobalKey)) { + final Object? globalBuildFlagsConfiguration = + buildFlagsConfiguration[_pluginToolsConfigGlobalKey]; + if (globalBuildFlagsConfiguration is! YamlList) { + printError( + 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map'); + printError('whose "$_pluginToolsConfigGlobalKey" key is a list.'); + printError( + 'That list must contain a list of arguments to pass to the flutter tool.'); + printError( + 'For example, the $_pluginToolsConfigFileName file could look like:'); + printError(_pluginToolsConfigExample); + throw ToolExit(_exitInvalidPluginToolsConfig); + } + yield* globalBuildFlagsConfiguration.cast(); + } + } + } + } + Future _buildExample( RepositoryPackage example, String flutterBuildType, { @@ -231,6 +302,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { 'build', flutterBuildType, ...extraBuildFlags, + ..._readExtraBuildFlagsConfiguration(example.directory), if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 2569e0ede87..689618f0612 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.7.0 +version: 0.7.1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index d9cbad246d2..c3b0cb9d5cd 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -379,7 +379,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); @@ -407,7 +406,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, containsAll([ @@ -436,7 +434,6 @@ void main() { contains('Creating temporary winuwp folder'), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -679,5 +676,50 @@ void main() { ])); }); }); + + test('The .pluginToolsConfig.yaml file', () async { + mockPlatform.isLinux = true; + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final File pluginExampleConfigFile = + pluginExampleDirectory.childFile('.pluginToolsConfig.yaml'); + pluginExampleConfigFile + .writeAsStringSync('buildFlags:\n global:\n - "test argument"'); + + final List output = [ + ...await runCapturingPrint( + runner, ['build-examples', '--linux']), + ...await runCapturingPrint( + runner, ['build-examples', '--macos']), + ]; + + expect( + output, + containsAllInOrder([ + '\nBUILDING plugin/example for Linux', + '\nBUILDING plugin/example for macOS', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'linux', 'test argument'], + pluginExampleDirectory.path), + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'macos', 'test argument'], + pluginExampleDirectory.path), + ])); + }); }); } From 8d5bf5953eeb72bab5fcf42a116f057e700a37ff Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 3 Sep 2021 19:01:04 -0400 Subject: [PATCH 130/249] [flutter_plugin_tools] Adjust diff logging (#4312) --- .../lib/src/common/git_version_finder.dart | 21 +++++++++++-------- .../tool/lib/src/common/plugin_command.dart | 3 +++ .../tool/lib/src/publish_plugin_command.dart | 3 +++ .../tool/test/common/plugin_command_test.dart | 9 +++++++- .../test/publish_plugin_command_test.dart | 2 ++ 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index 2c9519e7a85..a0a7a32b5e0 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -11,7 +11,7 @@ import 'package:yaml/yaml.dart'; /// Finding diffs based on `baseGitDir` and `baseSha`. class GitVersionFinder { /// Constructor - GitVersionFinder(this.baseGitDir, this.baseSha); + GitVersionFinder(this.baseGitDir, String? baseSha) : _baseSha = baseSha; /// The top level directory of the git repo. /// @@ -19,7 +19,7 @@ class GitVersionFinder { final GitDir baseGitDir; /// The base sha used to get diff. - final String? baseSha; + String? _baseSha; static bool _isPubspec(String file) { return file.trim().endsWith('pubspec.yaml'); @@ -32,10 +32,9 @@ class GitVersionFinder { /// Get a list of all the changed files. Future> getChangedFiles() async { - final String baseSha = await _getBaseSha(); + 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 []; @@ -49,7 +48,7 @@ class GitVersionFinder { /// 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()); + final String ref = gitRef ?? (await getBaseSha()); io.ProcessResult gitShow; try { @@ -63,9 +62,11 @@ class GitVersionFinder { return versionString == null ? null : Version.parse(versionString); } - Future _getBaseSha() async { - if (baseSha != null && baseSha!.isNotEmpty) { - return baseSha!; + /// Returns the base used to diff against. + Future getBaseSha() async { + String? baseSha = _baseSha; + if (baseSha != null && baseSha.isNotEmpty) { + return baseSha; } io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( @@ -76,6 +77,8 @@ class GitVersionFinder { baseShaFromMergeBase = await baseGitDir .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); } - return (baseShaFromMergeBase.stdout as String).trim(); + baseSha = (baseShaFromMergeBase.stdout as String).trim(); + _baseSha = baseSha; + return baseSha; } } diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 514a90b85cc..5d5cbd9abf6 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -314,6 +314,9 @@ abstract class PluginCommand extends Command { if (runOnChangedPackages) { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print( + 'Running for all packages that have changed relative to "$baseSha"\n'); final List changedFiles = await gitVersionFinder.getChangedFiles(); if (!_changesRequireFullTest(changedFiles)) { diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 6da51706ef1..769b9e8c8f0 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -159,6 +159,9 @@ class PublishPluginCommand extends PackageLoopingCommand { Stream getPackagesToProcess() async* { if (getBoolArg(_allChangedFlag)) { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print( + 'Publishing all packages that have changed relative to "$baseSha"\n'); final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 3ef0d3b3c00..13724e26e5f 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -398,12 +398,19 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ + final List output = await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' ]); + expect( + output, + containsAllInOrder([ + contains( + 'Running for all packages that have changed relative to "master"'), + ])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 2ea4fc75346..14e99a10f36 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -466,6 +466,8 @@ void main() { expect( output, containsAllInOrder([ + contains( + 'Publishing all packages that have changed relative to "HEAD~"'), contains('Running `pub publish ` in ${pluginDir1.path}...'), contains('Running `pub publish ` in ${pluginDir2.path}...'), contains('plugin1 - \x1B[32mpublished\x1B[0m'), From 916121b9fb8f8c20716dd7db096e22e8aa662cb1 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 9 Sep 2021 09:59:18 -0400 Subject: [PATCH 131/249] [ci] Enable the new Windows targets (#4325) Now that the builders have propagated, enable all the new tests and remove the obsolete versions. --- script/tool/test/drive_examples_command_test.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 85d2326d068..a7a1652c2fc 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -2,6 +2,7 @@ // 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:args/command_runner.dart'; @@ -60,7 +61,8 @@ void main() { final String output = '''${includeBanner ? updateBanner : ''}[${devices.join(',')}]'''; - final MockProcess mockDevicesProcess = MockProcess(stdout: output); + final MockProcess mockDevicesProcess = + MockProcess(stdout: output, stdoutEncoding: utf8); processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [mockDevicesProcess]; From 4ea49f8625aae27e74de8fa1831f3f9fa88ebf4f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 10 Sep 2021 16:07:06 -0400 Subject: [PATCH 132/249] [flutter_plugin_tools] Remove an unnecessary logging message (#4320) --- script/tool/lib/src/publish_plugin_command.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 769b9e8c8f0..4fdecf603ee 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -433,7 +433,6 @@ final String _credentialsPath = () { // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 String? cacheDir; final String? pubCache = io.Platform.environment['PUB_CACHE']; - print(pubCache); if (pubCache != null) { cacheDir = pubCache; } else if (io.Platform.isWindows) { From 188d56248ad6951b2a87e3b3313a85c4aaedc0dd Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 10 Sep 2021 16:07:16 -0400 Subject: [PATCH 133/249] [flutter_plugin_tools] Make having no Java unit tests a failure (#4310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This brings the native Android unit tests in line with the policy of having tests that we expect all plugins to have—unless there's a very specific reason to opt them out—fail when missing instead of skipping when missing, to help guard against errors where we silently fail to run tests we think we are running. Adds an explicit exclusion list covering the plugins that have a reason to be opted out. Android portion of flutter/flutter#85469 --- script/tool/CHANGELOG.md | 5 + script/tool/lib/src/native_test_command.dart | 15 ++- .../tool/test/native_test_command_test.dart | 97 +++++++++++++++++-- 3 files changed, 106 insertions(+), 11 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index aa73c65f3e8..c585bee4720 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +- `native-test --android` now fails plugins that don't have unit tests, + rather than skipping them. + ## 0.7.1 - Add support for `.pluginToolsConfig.yaml` in the `build-examples` command. diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index e50878db790..78a82afc571 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -242,7 +242,8 @@ this command. final Iterable examples = plugin.getExamples(); - bool ranTests = false; + bool ranUnitTests = false; + bool ranAnyTests = false; bool failed = false; bool hasMissingBuild = false; for (final RepositoryPackage example in examples) { @@ -289,7 +290,8 @@ this command. printError('$exampleName unit tests failed.'); failed = true; } - ranTests = true; + ranUnitTests = true; + ranAnyTests = true; } if (runIntegrationTests) { @@ -311,7 +313,7 @@ this command. printError('$exampleName integration tests failed.'); failed = true; } - ranTests = true; + ranAnyTests = true; } } @@ -321,7 +323,12 @@ this command. ? 'Examples must be built before testing.' : null); } - if (!ranTests) { + if (!mode.integrationOnly && !ranUnitTests) { + printError('No unit tests ran. Plugins are required to have unit tests.'); + return _PlatformResult(RunState.failed, + error: 'No unit tests ran (use --exclude if this is intentional).'); + } + if (!ranAnyTests) { return _PlatformResult(RunState.skipped); } return _PlatformResult(RunState.succeeded); diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index d1ab11f6e50..f7b2ea5c0de 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -430,7 +430,8 @@ void main() { ], ); - await runCapturingPrint(runner, ['native-test', '--android']); + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); final Directory androidFolder = plugin.childDirectory('example').childDirectory('android'); @@ -467,7 +468,8 @@ void main() { ], ); - await runCapturingPrint(runner, ['native-test', '--android']); + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); // Nothing should run since those files are all // integration_test-specific. @@ -641,7 +643,11 @@ void main() { ); final List output = await runCapturingPrint( - runner, ['native-test', '--android']); + runner, ['native-test', '--android'], + errorHandler: (Error e) { + // Having no unit tests is fatal, but that's not the point of this + // test so just ignore the failure. + }); expect( output, @@ -654,7 +660,7 @@ void main() { ])); }); - test('fails when a test fails', () async { + test('fails when a unit test fails', () async { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, @@ -695,6 +701,84 @@ void main() { ); }); + test('fails when an integration test fails', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess(), // unit passes + MockProcess(exitCode: 1), // integration fails + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('plugin/example integration tests failed.'), + contains('The following packages had errors:'), + contains('plugin') + ]), + ); + }); + + test('fails if there are no unit tests', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('No Android unit tests found for plugin/example'), + contains( + 'No unit tests ran. Plugins are required to have unit tests.'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No unit tests ran (use --exclude if this is intentional).') + ]), + ); + }); + test('skips if Android is not supported', () async { createFakePlugin( 'plugin', @@ -713,7 +797,7 @@ void main() { ); }); - test('skips when running no tests', () async { + test('skips when running no tests in integration-only mode', () async { createFakePlugin( 'plugin', packagesDir, @@ -723,12 +807,11 @@ void main() { ); final List output = await runCapturingPrint( - runner, ['native-test', '--android']); + runner, ['native-test', '--android', '--no-unit']); expect( output, containsAllInOrder([ - contains('No Android unit tests found for plugin/example'), contains('No Android integration tests found for plugin/example'), contains('SKIPPING: No tests found.'), ]), From 2b615cad84b2659a6b094560631ca843a4699280 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Fri, 10 Sep 2021 17:36:22 -0700 Subject: [PATCH 134/249] [google_maps_flutter_web] Fix getScreenCoordinate, zIndex of Circles (#4298) This commit: * uses the zIndex attribute when converting Circle geometry objects. * ensures that the getScreenCoordinate method works as expected on the web platform. * adds tests that can use a fully-rendered Google Map (see projection_test.dart) * changes the initialization flow of the web Google Map, so the Controller is only returned to the main plugin when it's ready to work. In order to test the getScreenCoordinate method, the Controller of a fully-rendered map must be available on the test, so we can retrieve information from an actual map instance. While working on this, it was observed that the Controller was being sent to the programmer before it was truly ready (while the map was still initializing). Instead of littering the test with imprecise timeouts that may make these tests slower (and flakier) than needed, this PR also changes the initialization process of a GMap slightly so when its Controller is returned to the user of the plugin (onPlatformViewCreated method call), it is truly ready. For this: * Controller.init is immediately called after the controller is created, * The plugin waits for the first onTilesloaded event coming from the JS SDK, and then * The Controller is sent to the user This change happens within "private" sections of the plugin, so programmers using the plugin "normally" shouldn't notice any difference whatsoever (only that the GMap might load slightly faster, and the onPlatformViewCreated callback might be firing a few hundred milliseconds later). --- .../tool/lib/src/license_check_command.dart | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index e68585c44bd..8cee46b45a4 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -49,16 +49,24 @@ const Set _ignoredFullBasenameList = { // When adding license regexes here, include the copyright info to ensure that // any new additions are flagged for added scrutiny in review. final List _thirdPartyLicenseBlockRegexes = [ -// Third-party code used in url_launcher_web. + // Third-party code used in url_launcher_web. RegExp( - r'^// Copyright 2017 Workiva Inc\..*' - r'^// Licensed under the Apache License, Version 2\.0', - multiLine: true, - dotAll: true), + r'^// Copyright 2017 Workiva Inc\..*' + r'^// Licensed under the Apache License, Version 2\.0', + multiLine: true, + dotAll: true, + ), + // Third-party code used in google_maps_flutter_web. + RegExp( + r'^// The MIT License [^C]+ Copyright \(c\) 2008 Krasimir Tsonev', + multiLine: true, + ), // bsdiff in flutter/packages. - RegExp(r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' - r'// Use of this source code is governed by a BSD-style license that can be\n' - r'// found in the LICENSE file\.\n'), + RegExp( + r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' + r'// Use of this source code is governed by a BSD-style license that can be\n' + r'// found in the LICENSE file\.\n', + ), ]; // The exact format of the BSD license that our license files should contain. From ae15095e0544a42f1b060aae96d834972ed74231 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 13 Sep 2021 19:37:12 -0400 Subject: [PATCH 135/249] [flutter_plugin_tools] Make no unit tests fatal for iOS/macOS (#4341) Brings iOS and macOS into alignment with the other platforms, where having unit tests set up is required. - For deprecated plugins with no tests, `--exclude`s them, as on other platforms. - For `quick_actions` and `share`, which have integration tests but no unit tests, sets up the unit test scaffolding. (This is done for `share` even though it's deprecated since unlike other platforms, iOS/macOS runs both native tests in the same command, and setting up a special way to exclude just units tests for that one case would be much more effort.) Fixes flutter/flutter#85469 --- script/tool/CHANGELOG.md | 4 +- script/tool/lib/src/native_test_command.dart | 50 ++- .../tool/test/native_test_command_test.dart | 397 ++++++++---------- 3 files changed, 213 insertions(+), 238 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index c585bee4720..7df6913db7d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,7 +1,7 @@ ## NEXT -- `native-test --android` now fails plugins that don't have unit tests, - rather than skipping them. +- `native-test --android`, `--ios`, and `--macos` now fail plugins that don't + have unit tests, rather than skipping them. ## 0.7.1 diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 78a82afc571..4911b4aeb15 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -251,8 +251,6 @@ this command. final bool hasIntegrationTests = exampleHasNativeIntegrationTests(example); - // TODO(stuartmorgan): Make !hasUnitTests fatal. See - // https://github.com/flutter/flutter/issues/85469 if (mode.unit && !hasUnitTests) { _printNoExampleTestsMessage(example, 'Android unit'); } @@ -355,33 +353,40 @@ this command. List extraFlags = const [], }) async { String? testTarget; + const String unitTestTarget = 'RunnerTests'; if (mode.unitOnly) { - testTarget = 'RunnerTests'; + testTarget = unitTestTarget; } else if (mode.integrationOnly) { testTarget = 'RunnerUITests'; } + bool ranUnitTests = false; // Assume skipped until at least one test has run. RunState overallResult = RunState.skipped; for (final RepositoryPackage example in plugin.getExamples()) { final String exampleName = example.displayName; - // TODO(stuartmorgan): Always check for RunnerTests, and make it fatal if - // no examples have it. See - // https://github.com/flutter/flutter/issues/85469 - if (testTarget != null) { - final Directory project = example.directory - .childDirectory(platform.toLowerCase()) - .childDirectory('Runner.xcodeproj'); + // If running a specific target, check that. Otherwise, check if there + // are unit tests, since having no unit tests for a plugin is fatal + // (by repo policy) even if there are integration tests. + bool exampleHasUnitTests = false; + final String? targetToCheck = + testTarget ?? (mode.unit ? unitTestTarget : null); + final Directory xcodeProject = example.directory + .childDirectory(platform.toLowerCase()) + .childDirectory('Runner.xcodeproj'); + if (targetToCheck != null) { final bool? hasTarget = - await _xcode.projectHasTarget(project, testTarget); + await _xcode.projectHasTarget(xcodeProject, targetToCheck); if (hasTarget == null) { printError('Unable to check targets for $exampleName.'); overallResult = RunState.failed; continue; } else if (!hasTarget) { - print('No "$testTarget" target in $exampleName; skipping.'); + print('No "$targetToCheck" target in $exampleName; skipping.'); continue; + } else if (targetToCheck == unitTestTarget) { + exampleHasUnitTests = true; } } @@ -404,20 +409,39 @@ this command. switch (exitCode) { case _xcodebuildNoTestExitCode: _printNoExampleTestsMessage(example, platform); - continue; + break; case 0: printSuccess('Successfully ran $platform xctest for $exampleName'); // If this is the first test, assume success until something fails. if (overallResult == RunState.skipped) { overallResult = RunState.succeeded; } + if (exampleHasUnitTests) { + ranUnitTests = true; + } break; default: // Any failure means a failure overall. overallResult = RunState.failed; + // If unit tests ran, note that even if they failed. + if (exampleHasUnitTests) { + ranUnitTests = true; + } break; } } + + if (!mode.integrationOnly && !ranUnitTests) { + printError('No unit tests ran. Plugins are required to have unit tests.'); + // Only return a specific summary error message about the missing unit + // tests if there weren't also failures, to avoid having a misleadingly + // specific message. + if (overallResult != RunState.failed) { + return _PlatformResult(RunState.failed, + error: 'No unit tests ran (use --exclude if this is intentional).'); + } + } + return _PlatformResult(overallResult); } diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index f7b2ea5c0de..ba93efcb3ac 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -78,6 +78,61 @@ void main() { runner.addCommand(command); }); + // Returns a MockProcess to provide for "xcrun xcodebuild -list" for a + // project that contains [targets]. + MockProcess _getMockXcodebuildListProcess(List targets) { + final Map projects = { + 'project': { + 'targets': targets, + } + }; + return MockProcess(stdout: jsonEncode(projects)); + } + + // Returns the ProcessCall to expect for checking the targets present in + // the [package]'s [platform]/Runner.xcodeproj. + ProcessCall _getTargetCheckCall(Directory package, String platform) { + return ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + package + .childDirectory(platform) + .childDirectory('Runner.xcodeproj') + .path, + ], + null); + } + + // Returns the ProcessCall to expect for running the tests in the + // workspace [platform]/Runner.xcworkspace, with the given extra flags. + ProcessCall _getRunTestCall( + Directory package, + String platform, { + String? destination, + List extraFlags = const [], + }) { + return ProcessCall( + 'xcrun', + [ + 'xcodebuild', + 'test', + '-workspace', + '$platform/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + if (destination != null) ...['-destination', destination], + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + package.path); + } + test('fails if no platforms are provided', () async { Error? commandError; final List output = await runCapturingPrint( @@ -124,31 +179,26 @@ void main() { pluginDirectory1.childDirectory('example'); processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), // Exit code 66 from testing indicates no tests. MockProcess(exitCode: 66), ]; - final List output = - await runCapturingPrint(runner, ['native-test', '--macos']); + final List output = await runCapturingPrint( + runner, ['native-test', '--macos', '--no-unit']); - expect(output, contains(contains('No tests found.'))); + expect( + output, + containsAllInOrder([ + contains('No tests found.'), + contains('Skipped 1 package(s)'), + ])); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos', + extraFlags: ['-only-testing:RunnerUITests']), ])); }); @@ -196,6 +246,11 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--ios', @@ -213,22 +268,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'foo_destination'), ])); }); @@ -243,6 +285,8 @@ void main() { processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), ]; await runCapturingPrint(runner, ['native-test', '--ios']); @@ -261,22 +305,9 @@ void main() { '--json', ], null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A'), ])); }); }); @@ -325,6 +356,11 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--macos', @@ -338,20 +374,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); }); @@ -999,13 +1023,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - const Map projects = { - 'project': { - 'targets': ['RunnerTests', 'RunnerUITests'] - } - }; processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), ]; final List output = await runCapturingPrint(runner, [ @@ -1023,34 +1043,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-only-testing:RunnerTests', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos', + extraFlags: ['-only-testing:RunnerTests']), ])); }); @@ -1064,13 +1059,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - const Map projects = { - 'project': { - 'targets': ['RunnerTests', 'RunnerUITests'] - } - }; processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), ]; final List output = await runCapturingPrint(runner, [ @@ -1088,34 +1079,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-only-testing:RunnerUITests', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos', + extraFlags: ['-only-testing:RunnerUITests']), ])); }); @@ -1130,13 +1096,8 @@ void main() { pluginDirectory1.childDirectory('example'); // Simulate a project with unit tests but no integration tests... - const Map projects = { - 'project': { - 'targets': ['RunnerTests'] - } - }; processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + _getMockXcodebuildListProcess(['RunnerTests']), ]; // ... then try to run only integration tests. @@ -1156,19 +1117,47 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + ])); + }); + + test('fails if there are no unit tests', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess(['RunnerUITests']), + ]; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['native-test', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No "RunnerTests" target in plugin/example; skipping.'), + contains( + 'No unit tests ran. Plugins are required to have unit tests.'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No unit tests ran (use --exclude if this is intentional).'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1206,19 +1195,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); }); @@ -1244,6 +1221,15 @@ void main() { final Directory androidFolder = pluginExampleDirectory.childDirectory('android'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), // iOS list + MockProcess(), // iOS run + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), // macOS list + MockProcess(), // macOS run + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--android', @@ -1266,36 +1252,11 @@ void main() { orderedEquals([ ProcessCall(androidFolder.childFile('gradlew').path, const ['testDebugUnitTest'], androidFolder.path), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'foo_destination'), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1309,6 +1270,11 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--ios', @@ -1327,20 +1293,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1353,6 +1307,11 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--ios', @@ -1371,22 +1330,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'foo_destination'), ])); }); @@ -1460,6 +1406,11 @@ void main() { ], ); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + // Simulate failing Android, but not iOS. final String gradlewPath = pluginDir .childDirectory('example') From 927e0ab19744dfe923239f5872ec9fac7ab7c56b Mon Sep 17 00:00:00 2001 From: keyonghan <54558023+keyonghan@users.noreply.github.com> Date: Tue, 14 Sep 2021 22:02:03 -0700 Subject: [PATCH 136/249] Run firebase test in flutter-cirrus (#4332) --- script/tool/lib/src/firebase_test_lab_command.dart | 2 +- script/tool/test/firebase_test_lab_command_test.dart | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 4fc47c0da70..941cba3a694 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -27,7 +27,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addOption( 'project', - defaultsTo: 'flutter-infra', + defaultsTo: 'flutter-cirrus', help: 'The Firebase project name.', ); final String? homeDir = io.Platform.environment['HOME']; diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 7716990b323..e39ccf30b13 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -130,7 +130,7 @@ void main() { .split(' '), null), ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), ProcessCall( '/packages/plugin1/example/android/gradlew', 'app:assembleAndroidTest -Pverbose=true'.split(' '), @@ -207,7 +207,7 @@ void main() { .split(' '), null), ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), ProcessCall( '/packages/plugin/example/android/gradlew', 'app:assembleAndroidTest -Pverbose=true'.split(' '), @@ -433,7 +433,7 @@ void main() { .split(' '), null), ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), ProcessCall( '/packages/plugin/example/android/gradlew', 'app:assembleAndroidTest -Pverbose=true'.split(' '), @@ -588,7 +588,7 @@ void main() { .split(' '), null), ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), ProcessCall( '/packages/plugin/example/android/gradlew', 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' From 1b95825c76f004e5a806b60cda2984d3c0655b2f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 20 Sep 2021 13:54:02 -0400 Subject: [PATCH 137/249] [flutter_plugin_tools] Add a federated PR safety check (#4329) Creates a new command to validate that PRs don't change platform interface packages and implementations at the same time, to try to prevent ecosystem-breaking changes. See https://github.com/flutter/flutter/issues/89518 for context. Per the explanation in the issue, this has carve-outs for: - Changes to platform interfaces that aren't published (allowing for past uses cases such as making a substantive change to an implementation, and making minor adjustments to comments in the PI package based on those changes). - Things that look like bulk changes (e.g., a mass change to account for a new lint rule) Fixes https://github.com/flutter/flutter/issues/89518 --- script/tool/CHANGELOG.md | 3 + .../lib/src/common/git_version_finder.dart | 3 + .../lib/src/common/repository_package.dart | 6 + .../src/federation_safety_check_command.dart | 188 +++++++++++ script/tool/lib/src/main.dart | 2 + .../tool/lib/src/pubspec_check_command.dart | 10 +- .../federation_safety_check_command_test.dart | 314 ++++++++++++++++++ 7 files changed, 518 insertions(+), 8 deletions(-) create mode 100644 script/tool/lib/src/federation_safety_check_command.dart create mode 100644 script/tool/test/federation_safety_check_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7df6913db7d..3be9173a505 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,9 @@ - `native-test --android`, `--ios`, and `--macos` now fail plugins that don't have unit tests, rather than skipping them. +- Added a new `federation-safety-check` command to help catch changes to + federated packages that have been done in such a way that they will pass in + CI, but fail once the change is landed and published. ## 0.7.1 diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index a0a7a32b5e0..1cdd2fcc409 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -58,6 +58,9 @@ class GitVersionFinder { return null; } final String fileContent = gitShow.stdout as String; + if (fileContent.trim().isEmpty) { + return null; + } final String? versionString = loadYaml(fileContent)['version'] as String?; return versionString == null ? null : Version.parse(versionString); } diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index f6601d39b79..feece7c1cdf 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -47,6 +47,12 @@ class RepositoryPackage { /// The package's top-level pubspec.yaml. File get pubspecFile => directory.childFile('pubspec.yaml'); + /// True if this appears to be a federated plugin package, according to + /// repository conventions. + bool get isFederated => + directory.parent.basename != 'packages' && + directory.basename.startsWith(directory.parent.basename); + /// Returns the Flutter example packages contained in the package, if any. Iterable getExamples() { final Directory exampleDirectory = directory.childDirectory('example'); diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart new file mode 100644 index 00000000000..cb0da162e60 --- /dev/null +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -0,0 +1,188 @@ +// 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:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common/core.dart'; +import 'common/file_utils.dart'; +import 'common/git_version_finder.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to check that PRs don't violate repository best practices that +/// have been established to avoid breakages that building and testing won't +/// catch. +class FederationSafetyCheckCommand extends PackageLoopingCommand { + /// Creates an instance of the safety check command. + FederationSafetyCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ); + + // A map of package name (as defined by the directory name of the package) + // to a list of changed Dart files in that package, as Posix paths relative to + // the package root. + // + // This only considers top-level packages, not subpackages such as example/. + final Map> _changedDartFiles = >{}; + + // The set of *_platform_interface packages that will have public code changes + // published. + final Set _modifiedAndPublishedPlatformInterfacePackages = {}; + + // The set of conceptual plugins (not packages) that have changes. + final Set _changedPlugins = {}; + + static const String _platformInterfaceSuffix = '_platform_interface'; + + @override + final String name = 'federation-safety-check'; + + @override + final String description = + 'Checks that the change does not violate repository rules around changes ' + 'to federated plugin packages.'; + + @override + bool get hasLongOutput => false; + + @override + Future initializeRun() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print('Validating changes relative to "$baseSha"\n'); + for (final String path in await gitVersionFinder.getChangedFiles()) { + // Git output always uses Posix paths. + final List allComponents = p.posix.split(path); + final int packageIndex = allComponents.indexOf('packages'); + if (packageIndex == -1) { + continue; + } + final List relativeComponents = + allComponents.sublist(packageIndex + 1); + // The package name is either the directory directly under packages/, or + // the directory under that in the case of a federated plugin. + String packageName = relativeComponents.removeAt(0); + // Count the top-level plugin as changed. + _changedPlugins.add(packageName); + if (relativeComponents[0] == packageName || + relativeComponents[0].startsWith('${packageName}_')) { + packageName = relativeComponents.removeAt(0); + } + + if (relativeComponents.last.endsWith('.dart')) { + _changedDartFiles[packageName] ??= []; + _changedDartFiles[packageName]! + .add(p.posix.joinAll(relativeComponents)); + } + + if (packageName.endsWith(_platformInterfaceSuffix) && + relativeComponents.first == 'pubspec.yaml' && + await _packageWillBePublished(path)) { + _modifiedAndPublishedPlatformInterfacePackages.add(packageName); + } + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + if (!isFlutterPlugin(package)) { + return PackageResult.skip('Not a plugin.'); + } + + if (!package.isFederated) { + return PackageResult.skip('Not a federated plugin.'); + } + + if (package.directory.basename.endsWith('_platform_interface')) { + // As the leaf nodes in the graph, a published package interface change is + // assumed to be correct, and other changes are validated against that. + return PackageResult.skip( + 'Platform interface changes are not validated.'); + } + + // Uses basename to match _changedPackageFiles. + final String basePackageName = package.directory.parent.basename; + final String platformInterfacePackageName = + '$basePackageName$_platformInterfaceSuffix'; + final List changedPlatformInterfaceFiles = + _changedDartFiles[platformInterfacePackageName] ?? []; + + if (!_modifiedAndPublishedPlatformInterfacePackages + .contains(platformInterfacePackageName)) { + print('No published changes for $platformInterfacePackageName.'); + return PackageResult.success(); + } + + if (!changedPlatformInterfaceFiles + .any((String path) => path.startsWith('lib/'))) { + print('No public code changes for $platformInterfacePackageName.'); + return PackageResult.success(); + } + + // If the change would be flagged, but it appears to be a mass change + // rather than a plugin-specific change, allow it with a warning. + // + // This is a tradeoff between safety and convenience; forcing mass changes + // to be split apart is not ideal, and the assumption is that reviewers are + // unlikely to accidentally approve a PR that is supposed to be changing a + // single plugin, but touches other plugins (vs accidentally approving a + // PR that changes multiple parts of a single plugin, which is a relatively + // easy mistake to make). + // + // 3 is chosen to minimize the chances of accidentally letting something + // through (vs 2, which could be a single-plugin change with one stray + // change to another file accidentally included), while not setting too + // high a bar for detecting mass changes. This can be tuned if there are + // issues with false positives or false negatives. + const int massChangePluginThreshold = 3; + if (_changedPlugins.length >= massChangePluginThreshold) { + logWarning('Ignoring potentially dangerous change, as this appears ' + 'to be a mass change.'); + return PackageResult.success(); + } + + printError('Dart changes are not allowed to other packages in ' + '$basePackageName in the same PR as changes to public Dart code in ' + '$platformInterfacePackageName, as this can cause accidental breaking ' + 'changes to be missed by automated checks. Please split the changes to ' + 'these two packages into separate PRs.\n\n' + 'If you believe that this is a false positive, please file a bug.'); + return PackageResult.fail( + ['$platformInterfacePackageName changed.']); + } + + Future _packageWillBePublished( + String pubspecRepoRelativePosixPath) async { + final File pubspecFile = childFileWithSubcomponents( + packagesDir.parent, p.posix.split(pubspecRepoRelativePosixPath)); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + return false; + } + + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final Version? previousVersion = + await gitVersionFinder.getPackageVersion(pubspecRepoRelativePosixPath); + if (previousVersion == null) { + // The plugin is new, so it will be published. + return true; + } + return pubspec.version != previousVersion; + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index e70cba24cc5..70a6ab51603 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -13,6 +13,7 @@ import 'build_examples_command.dart'; import 'common/core.dart'; import 'create_all_plugins_app_command.dart'; import 'drive_examples_command.dart'; +import 'federation_safety_check_command.dart'; import 'firebase_test_lab_command.dart'; import 'format_command.dart'; import 'license_check_command.dart'; @@ -49,6 +50,7 @@ void main(List args) { ..addCommand(BuildExamplesCommand(packagesDir)) ..addCommand(CreateAllPluginsAppCommand(packagesDir)) ..addCommand(DriveExamplesCommand(packagesDir)) + ..addCommand(FederationSafetyCheckCommand(packagesDir)) ..addCommand(FirebaseTestLabCommand(packagesDir)) ..addCommand(FormatCommand(packagesDir)) ..addCommand(LicenseCheckCommand(packagesDir)) diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 29f9ea733a0..605a8aa83a3 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -210,17 +210,11 @@ class PubspecCheckCommand extends PackageLoopingCommand { // Returns true if [packageName] appears to be an implementation package // according to repository conventions. bool _isImplementationPackage(RepositoryPackage package) { - // An implementation package should be in a group folder... - final Directory parentDir = package.directory.parent; - if (parentDir.path == packagesDir.path) { + if (!package.isFederated) { return false; } final String packageName = package.directory.basename; - final String parentName = parentDir.basename; - // ... whose name is a prefix of the package name. - if (!packageName.startsWith(parentName)) { - return false; - } + final String parentName = package.directory.parent.basename; // A few known package names are not implementation packages; assume // anything else is. (This is done instead of listing known implementation // suffixes to allow for non-standard suffixes; e.g., to put several diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart new file mode 100644 index 00000000000..4ae3ec5c76d --- /dev/null +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -0,0 +1,314 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:flutter_plugin_tools/src/federation_safety_check_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through the process runner, to make mock output + // consistent with other processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); + }); + + processRunner = RecordingProcessRunner(); + final FederationSafetyCheckCommand command = FederationSafetyCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: gitDir); + + runner = CommandRunner('federation_safety_check_command', + 'Test for $FederationSafetyCheckCommand'); + runner.addCommand(command); + }); + + test('skips non-plugin packages', () async { + final Directory package = createFakePackage('foo', packagesDir); + + final String changedFileOutput = [ + package.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo...'), + contains('Not a plugin'), + contains('Skipped 1 package(s)'), + ]), + ); + }); + + test('skips unfederated plugins', () async { + final Directory package = createFakePlugin('foo', packagesDir); + + final String changedFileOutput = [ + package.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo...'), + contains('Not a federated plugin'), + contains('Skipped 1 package(s)'), + ]), + ); + }); + + test('skips interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + platformInterface.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo_platform_interface...'), + contains('Platform interface changes are not validated.'), + contains('Skipped 1 package(s)'), + ]), + ); + }); + + test('allows changes to multiple non-interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No published changes for foo_platform_interface.'), + contains('Running for foo_bar...'), + contains('No published changes for foo_platform_interface.'), + ]), + ); + }); + + test( + 'fails on changes to interface and non-interface packages in the same plugin', + () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['federation-safety-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('Dart changes are not allowed to other packages in foo in the ' + 'same PR as changes to public Dart code in foo_platform_interface, ' + 'as this can cause accidental breaking changes to be missed by ' + 'automated checks. Please split the changes to these two packages ' + 'into separate PRs.'), + contains('Running for foo_bar...'), + contains('Dart changes are not allowed to other packages in foo'), + contains('The following packages had errors:'), + contains('foo/foo:\n' + ' foo_platform_interface changed.'), + contains('foo_bar:\n' + ' foo_platform_interface changed.'), + ]), + ); + }); + + test('ignores test-only changes to interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('test').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No public code changes for foo_platform_interface.'), + contains('Running for foo_bar...'), + contains('No public code changes for foo_platform_interface.'), + ]), + ); + }); + + test('ignores unpublished changes to interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess( + stdout: RepositoryPackage(platformInterface) + .pubspecFile + .readAsStringSync()), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No published changes for foo_platform_interface.'), + contains('Running for foo_bar...'), + contains('No published changes for foo_platform_interface.'), + ]), + ); + }); + + test('allows things that look like mass changes, with warning', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final Directory otherPlugin1 = createFakePlugin('bar', packagesDir); + final Directory otherPlugin2 = createFakePlugin('baz', packagesDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('lib').childFile('foo.dart'), + otherPlugin1.childFile('bar.dart'), + otherPlugin2.childFile('baz.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains( + 'Ignoring potentially dangerous change, as this appears to be a mass change.'), + contains('Running for foo_bar...'), + contains( + 'Ignoring potentially dangerous change, as this appears to be a mass change.'), + contains('Ran for 2 package(s) (2 with warnings)'), + ]), + ); + }); +} From 3e7ec17a0eafe3b170239d16b91f96877a1be272 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 21 Sep 2021 15:03:58 -0400 Subject: [PATCH 138/249] [flutter_plugin_tools] Fix federated safety check (#4368) The new safety check doesn't allow simple platform-interface-only changes because it doesn't actually check that a non-interface package is actually modified before failing it for a modified platform interface. This fixes that, and adds a test case covering it. --- .../src/federation_safety_check_command.dart | 7 ++++ .../federation_safety_check_command_test.dart | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index cb0da162e60..fd53d6cbaa6 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -135,6 +135,13 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { return PackageResult.success(); } + final List changedPackageFiles = + _changedDartFiles[package.directory.basename] ?? []; + if (changedPackageFiles.isEmpty) { + print('No Dart changes.'); + return PackageResult.success(); + } + // If the change would be flagged, but it appears to be a mass change // rather than a plugin-specific change, allow it with a warning. // diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart index 4ae3ec5c76d..e23485fbc8b 100644 --- a/script/tool/test/federation_safety_check_command_test.dart +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -125,6 +125,47 @@ void main() { ); }); + test('allows changes to just an interface package', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + createFakePlugin('foo', pluginGroupDir); + createFakePlugin('foo_ios', pluginGroupDir); + createFakePlugin('foo_android', pluginGroupDir); + + final String changedFileOutput = [ + platformInterface.childDirectory('lib').childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No Dart changes.'), + contains('Running for foo_android...'), + contains('No Dart changes.'), + contains('Running for foo_ios...'), + contains('No Dart changes.'), + contains('Running for foo_platform_interface...'), + contains('Ran for 3 package(s)'), + contains('Skipped 1 package(s)'), + ]), + ); + expect( + output, + isNot(contains([ + contains('No published changes for foo_platform_interface'), + ])), + ); + }); + test('allows changes to multiple non-interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); final Directory appFacing = createFakePlugin('foo', pluginGroupDir); From 211a21792e9ccd23f4c0a46520661fd92297493d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 21 Sep 2021 15:04:34 -0400 Subject: [PATCH 139/249] Require authors file (#4367) Adds a check to `publish-check` that there is an AUTHORS file present, since our license refers to "The Flutter Authors", so we want to have a file distributed with each package that says who the AUTHORS are (vs. just having a top-level repo AUTHORS file, which is not part of package distribution). Adds AUTHORS files to packages that have been created since the earlier one-time fix that added them, but didn't add a check to prevent future issues. Also updates the publish-check failure tests to include checks for specific output so that we know that they are failing for the reasons the test is expecting, bringing them up to current repo standards for failure tests. Fixes https://github.com/flutter/flutter/issues/89680 --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/publish_check_command.dart | 21 ++++- script/tool/test/list_command_test.dart | 3 + .../tool/test/publish_check_command_test.dart | 89 +++++++++++++++++-- script/tool/test/util.dart | 7 ++ 5 files changed, 111 insertions(+), 10 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 3be9173a505..7e9cd3bec93 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,7 @@ - Added a new `federation-safety-check` command to help catch changes to federated packages that have been done in such a way that they will pass in CI, but fail once the change is landed and published. +- `publish-check` now validates that there is an `AUTHORS` file. ## 0.7.1 diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index ab9f5f14749..563e0904552 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -77,10 +77,17 @@ class PublishCheckCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - final _PublishCheckResult? result = await _passesPublishCheck(package); + _PublishCheckResult? result = await _passesPublishCheck(package); if (result == null) { return PackageResult.skip('Package is marked as unpublishable.'); } + if (!_passesAuthorsCheck(package)) { + _printImportantStatusMessage( + 'No AUTHORS file found. Packages must include an AUTHORS file.', + isError: true); + result = _PublishCheckResult.error; + } + if (result.index > _overallResult.index) { _overallResult = result; } @@ -189,7 +196,7 @@ class PublishCheckCommand extends PackageLoopingCommand { final String packageName = package.directory.basename; final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { - print('no pubspec'); + print('No valid pubspec found.'); return _PublishCheckResult.error; } else if (pubspec.publishTo == 'none') { return null; @@ -239,6 +246,16 @@ HTTP response: ${pubVersionFinderResponse.httpResponse.body} } } + bool _passesAuthorsCheck(RepositoryPackage package) { + final List pathComponents = + package.directory.fileSystem.path.split(package.directory.path); + if (pathComponents.contains('third_party')) { + // Third-party packages aren't required to have an AUTHORS file. + return true; + } + return package.directory.childFile('AUTHORS').existsSync(); + } + void _printImportantStatusMessage(String message, {required bool isError}) { final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; if (getBoolArg(_machineFlag)) { diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index 488fc9bcb1e..fcdf9fafdb6 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -99,13 +99,16 @@ void main() { examples, unorderedEquals([ '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/AUTHORS', '/packages/plugin1/CHANGELOG.md', '/packages/plugin1/example/pubspec.yaml', '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/AUTHORS', '/packages/plugin2/CHANGELOG.md', '/packages/plugin2/example/example1/pubspec.yaml', '/packages/plugin2/example/example2/pubspec.yaml', '/packages/plugin3/pubspec.yaml', + '/packages/plugin3/AUTHORS', '/packages/plugin3/CHANGELOG.md', ]), ); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index e1ab0e224e4..c5527af2173 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -69,12 +69,22 @@ void main() { createFakePlugin('plugin_tools_test_package_a', packagesDir); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) + MockProcess(exitCode: 1, stdout: 'Some error from pub') ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( - () => runCapturingPrint(runner, ['publish-check']), - throwsA(isA()), + output, + containsAllInOrder([ + contains('Some error from pub'), + contains('Unable to publish plugin_tools_test_package_a'), + ]), ); }); @@ -82,8 +92,58 @@ void main() { final Directory dir = createFakePlugin('c', packagesDir); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - expect(() => runCapturingPrint(runner, ['publish-check']), - throwsA(isA())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No valid pubspec found.'), + ]), + ); + }); + + test('fails if AUTHORS is missing', () async { + final Directory package = createFakePackage('a_package', packagesDir); + package.childFile('AUTHORS').delete(); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'No AUTHORS file found. Packages must include an AUTHORS file.'), + ]), + ); + }); + + test('does not require AUTHORS for third-party', () async { + final Directory package = createFakePackage( + 'a_package', + packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages')); + package.childFile('AUTHORS').delete(); + + final List output = + await runCapturingPrint(runner, ['publish-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + ]), + ); }); test('pass on prerelease if --allow-pre-release flag is on', () async { @@ -116,8 +176,21 @@ void main() { process, ]; - expect(runCapturingPrint(runner, ['publish-check']), - throwsA(isA())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Packages with an SDK constraint on a pre-release of the Dart SDK'), + contains('Unable to publish d'), + ]), + ); }); test('Success message on stderr is not printed as an error', () async { @@ -324,7 +397,7 @@ void main() { // aren't controlled by this package, so asserting its exact format would // make the test fragile to irrelevant changes in those details. expect(output.first, contains(r''' - "no pubspec", + "No valid pubspec found.", "\n============================================================\n|| Running for no_publish_b\n============================================================\n", "url https://pub.dev/packages/no_publish_b.json", "no_publish_b.json", diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index e053100172c..9abb34bef35 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -126,6 +126,7 @@ Directory createFakePackage( ## $version * Some changes. '''); + createFakeAuthors(packageDirectory); if (examples.length == 1) { final Directory exampleDir = packageDirectory.childDirectory(examples.first) @@ -208,6 +209,12 @@ publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being parent.childFile('pubspec.yaml').writeAsStringSync(yaml); } +void createFakeAuthors(Directory parent) { + final File authorsFile = parent.childFile('AUTHORS'); + authorsFile.createSync(); + authorsFile.writeAsStringSync('Google Inc.'); +} + String _pluginPlatformSection( String platform, PlatformDetails support, String packageName) { String entry = ''; From 91ef4b1a008eaaa3b5bb392f872e9322ea48cf35 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 22 Sep 2021 13:58:07 -0400 Subject: [PATCH 140/249] Add false secret lists, and enforce ordering (#4372) --- .../tool/lib/src/pubspec_check_command.dart | 2 + .../tool/test/pubspec_check_command_test.dart | 39 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 605a8aa83a3..fec0dcef9ac 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -39,6 +39,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { 'flutter:', 'dependencies:', 'dev_dependencies:', + 'false_secrets:', ]; static const List _majorPackageSections = [ @@ -46,6 +47,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { 'dependencies:', 'dev_dependencies:', 'flutter:', + 'false_secrets:', ]; static const String _expectedIssueLinkFormat = diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index c5d36013c40..948136993d1 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -113,6 +113,13 @@ dev_dependencies: '''; } + String falseSecretsSection() { + return ''' +false_secrets: + - /lib/main.dart +'''; + } + test('passes for a plugin following conventions', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); @@ -122,6 +129,7 @@ ${environmentSection()} ${flutterSection(isPlugin: true)} ${dependenciesSection()} ${devDependenciesSection()} +${falseSecretsSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -147,6 +155,7 @@ ${environmentSection()} ${dependenciesSection()} ${devDependenciesSection()} ${flutterSection()} +${falseSecretsSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -399,7 +408,7 @@ ${dependenciesSection()} ); }); - test('fails when devDependencies section is out of order', () async { + test('fails when dev_dependencies section is out of order', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' @@ -426,6 +435,34 @@ ${dependenciesSection()} ); }); + test('fails when false_secrets section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${falseSecretsSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); + test('fails when an implemenation package is missing "implements"', () async { final Directory pluginDirectory = createFakePlugin( From c0aca80c15746630692757870c16409457c9e985 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 23 Sep 2021 13:23:06 -0400 Subject: [PATCH 141/249] [flutter_plugin_tools] Allow overriding breaking change check (#4369) --- script/tool/CHANGELOG.md | 2 + .../lib/src/common/repository_package.dart | 5 + .../tool/lib/src/drive_examples_command.dart | 2 +- .../src/federation_safety_check_command.dart | 2 +- .../tool/lib/src/version_check_command.dart | 83 +++++++++++++++-- .../tool/test/version_check_command_test.dart | 91 ++++++++++++++++++- 6 files changed, 173 insertions(+), 12 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7e9cd3bec93..2bc7a901a9a 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -6,6 +6,8 @@ federated packages that have been done in such a way that they will pass in CI, but fail once the change is landed and published. - `publish-check` now validates that there is an `AUTHORS` file. +- Added flags to `version-check` to allow overriding the platform interface + major version change restriction. ## 0.7.1 diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index feece7c1cdf..cb586afb4df 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -53,6 +53,11 @@ class RepositoryPackage { directory.parent.basename != 'packages' && directory.basename.startsWith(directory.parent.basename); + /// True if this appears to be a platform interface package, according to + /// repository conventions. + bool get isPlatformInterface => + directory.basename.endsWith('_platform_interface'); + /// Returns the Flutter example packages contained in the package, if any. Iterable getExamples() { final Directory exampleDirectory = directory.childDirectory('example'); diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index b3434b0659f..593e557fa39 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -133,7 +133,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - if (package.directory.basename.endsWith('_platform_interface') && + if (package.isPlatformInterface && !package.getSingleExampleDeprecated().directory.existsSync()) { // Platform interface packages generally aren't intended to have // examples, and don't need integration tests, so skip rather than fail. diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index fd53d6cbaa6..200f9c3f48c 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -109,7 +109,7 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { return PackageResult.skip('Not a federated plugin.'); } - if (package.directory.basename.endsWith('_platform_interface')) { + if (package.isPlatformInterface) { // As the leaf nodes in the graph, a published package interface change is // assumed to be correct, and other changes are validated against that. return PackageResult.skip( diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 6b49c40d66b..528251fbf80 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -18,6 +18,8 @@ import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; import 'common/repository_package.dart'; +const int _exitMissingChangeDescriptionFile = 3; + /// Categories of version change types. enum NextVersionType { /// A breaking change. @@ -108,13 +110,36 @@ class VersionCheckCommand extends PackageLoopingCommand { argParser.addFlag( _againstPubFlag, help: 'Whether the version check should run against the version on pub.\n' - 'Defaults to false, which means the version check only run against the previous version in code.', + 'Defaults to false, which means the version check only run against ' + 'the previous version in code.', defaultsTo: false, negatable: true, ); + argParser.addOption(_changeDescriptionFile, + help: 'The path to a file containing the description of the change ' + '(e.g., PR description or commit message).\n\n' + 'If supplied, this is used to allow overrides to some version ' + 'checks.'); + argParser.addFlag(_ignorePlatformInterfaceBreaks, + help: 'Bypasses the check that platform interfaces do not contain ' + 'breaking changes.\n\n' + 'This is only intended for use in post-submit CI checks, to ' + 'prevent the possibility of post-submit breakage if a change ' + 'description justification is not transferred into the commit ' + 'message. Pre-submit checks should always use ' + '--$_changeDescriptionFile instead.', + hide: true); } static const String _againstPubFlag = 'against-pub'; + static const String _changeDescriptionFile = 'change-description-file'; + static const String _ignorePlatformInterfaceBreaks = + 'ignore-platform-interface-breaks'; + + /// The string that must be in [_changeDescriptionFile] to allow a breaking + /// change to a platform interface. + static const String _breakingChangeJustificationMarker = + '## Breaking change justification'; final PubVersionFinder _pubVersionFinder; @@ -292,16 +317,17 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} return _CurrentVersionState.invalidChange; } - final bool isPlatformInterface = - pubspec.name.endsWith('_platform_interface'); - // TODO(stuartmorgan): Relax this check. See - // https://github.com/flutter/flutter/issues/85391 - if (isPlatformInterface && - allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR) { + if (allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR && + !_validateBreakingChange(package)) { printError('${indentation}Breaking change detected.\n' - '${indentation}Breaking changes to platform interfaces are strongly discouraged.\n'); + '${indentation}Breaking changes to platform interfaces are not ' + 'allowed without explicit justification.\n' + '${indentation}See ' + 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' + 'for more information.'); return _CurrentVersionState.invalidChange; } + return _CurrentVersionState.validChange; } @@ -398,4 +424,45 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return null; } } + + /// Checks whether the current breaking change to [package] should be allowed, + /// logging extra information for auditing when allowing unusual cases. + bool _validateBreakingChange(RepositoryPackage package) { + // Only platform interfaces have breaking change restrictions. + if (!package.isPlatformInterface) { + return true; + } + + if (getBoolArg(_ignorePlatformInterfaceBreaks)) { + logWarning( + '${indentation}Allowing breaking change to ${package.displayName} ' + 'due to --$_ignorePlatformInterfaceBreaks'); + return true; + } + + if (_getChangeDescription().contains(_breakingChangeJustificationMarker)) { + logWarning( + '${indentation}Allowing breaking change to ${package.displayName} ' + 'due to "$_breakingChangeJustificationMarker" in the change ' + 'description.'); + return true; + } + + return false; + } + + /// Returns the contents of the file pointed to by [_changeDescriptionFile], + /// or an empty string if that flag is not provided. + String _getChangeDescription() { + final String path = getStringArg(_changeDescriptionFile); + if (path.isEmpty) { + return ''; + } + final File file = packagesDir.fileSystem.file(path); + if (!file.existsSync()) { + printError('${indentation}No such file: $path'); + throw ToolExit(_exitMissingChangeDescriptionFile); + } + return file.readAsStringSync(); + } } diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 9ab7c57089a..7d59dbb3ee7 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -46,7 +46,7 @@ class MockProcessResult extends Mock implements io.ProcessResult {} void main() { const String indentation = ' '; group('$VersionCheckCommand', () { - FileSystem fileSystem; + late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; @@ -252,7 +252,8 @@ void main() { ])); }); - test('disallows breaking changes to platform interfaces', () async { + test('disallows breaking changes to platform interfaces by default', + () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -276,6 +277,92 @@ void main() { ])); }); + test('allows breaking changes to platform interfaces with explanation', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile.writeAsStringSync(''' +Some general PR description + +## Breaking change justification + +This is necessary because of X, Y, and Z + +## Another section'''); + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--change-description-file=${changeDescriptionFile.path}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Allowing breaking change to plugin_platform_interface ' + 'due to "## Breaking change justification" in the change ' + 'description.'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + + test('throws if a nonexistent change description file is specified', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--change-description-file=a_missing_file.txt' + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No such file: a_missing_file.txt'), + ]), + ); + }); + + test('allows breaking changes to platform interfaces with bypass flag', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--ignore-platform-interface-breaks' + ]); + + expect( + output, + containsAllInOrder([ + contains('Allowing breaking change to plugin_platform_interface due ' + 'to --ignore-platform-interface-breaks'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + test('Allow empty lines in front of the first version in CHANGELOG', () async { const String version = '1.0.1'; From 3d4782ae6293be36962297e48155d2304c31aa05 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 23 Sep 2021 15:40:04 -0400 Subject: [PATCH 142/249] [flutter_plugin_tools] Improve version check error handling (#4376) Catches invalid CHANGELOG formats and logs useful error messages for them, rather than throwing FormatExceptions. --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/version_check_command.dart | 23 +++++-- .../tool/test/version_check_command_test.dart | 67 +++++++++++++++++++ 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2bc7a901a9a..a5263ba0396 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -8,6 +8,7 @@ - `publish-check` now validates that there is an `AUTHORS` file. - Added flags to `version-check` to allow overriding the platform interface major version change restriction. +- Improved error handling and error messages in CHANGELOG version checks. ## 0.7.1 diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 528251fbf80..90ba0668002 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -374,8 +374,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} print( '${indentation}Found NEXT; validating next version in the CHANGELOG.'); // Ensure that the version in pubspec hasn't changed without updating - // CHANGELOG. That means the next version entry in the CHANGELOG pass the - // normal validation. + // CHANGELOG. That means the next version entry in the CHANGELOG should + // pass the normal validation. + versionString = null; while (iterator.moveNext()) { if (iterator.current.trim().startsWith('## ')) { versionString = iterator.current.trim().split(' ').last; @@ -384,11 +385,19 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } } - final Version? fromChangeLog = - versionString == null ? null : Version.parse(versionString); - if (fromChangeLog == null) { - printError( - '${indentation}Cannot find version on the first line CHANGELOG.md'); + if (versionString == null) { + printError('${indentation}Unable to find a version in CHANGELOG.md'); + print('${indentation}The current version should be on a line starting ' + 'with "## ", either on the first non-empty line or after a "## NEXT" ' + 'section.'); + return false; + } + + final Version fromChangeLog; + try { + fromChangeLog = Version.parse(versionString); + } on FormatException { + printError('"$versionString" could not be parsed as a version.'); return false; } diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 7d59dbb3ee7..39132212d66 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -601,6 +601,73 @@ This is necessary because of X, Y, and Z ); }); + test( + 'fails gracefully if the version headers are not found due to using the wrong style', + () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## NEXT +* Some changes for a later release. +# 1.0.0 +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to find a version in CHANGELOG.md'), + contains('The current version should be on a line starting with ' + '"## ", either on the first non-empty line or after a "## NEXT" ' + 'section.'), + ]), + ); + }); + + test('fails gracefully if the version is unparseable', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## Alpha +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"Alpha" could not be parsed as a version.'), + ]), + ); + }); + test('allows valid against pub', () async { mockHttpResponse = { 'name': 'some_package', From 769043d35ae228e404bb65c496207c17926912b7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 28 Sep 2021 09:20:20 -0400 Subject: [PATCH 143/249] [flutter_plugin_tools] Check licenses in Kotlin (#4373) The license check overlooked Kotlin, since it's not currently widely used in our repositories. Also adds the missing license to one Kotlin file, from an example that was (likely accidentally) re-generated using Kotlin instead of Java. --- script/tool/CHANGELOG.md | 1 + script/tool/lib/src/license_check_command.dart | 1 + script/tool/test/license_check_command_test.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a5263ba0396..6119545260a 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -9,6 +9,7 @@ - Added flags to `version-check` to allow overriding the platform interface major version change restriction. - Improved error handling and error messages in CHANGELOG version checks. +- `license-check` now validates Kotlin files. ## 0.7.1 diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 8cee46b45a4..7165e985c05 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -16,6 +16,7 @@ const Set _codeFileExtensions = { '.h', '.html', '.java', + '.kt', '.m', '.mm', '.swift', diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 288cf4696a5..5a8a90e9a67 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -66,6 +66,7 @@ void main() { 'html': true, 'java': true, 'json': false, + 'kt': true, 'm': true, 'md': false, 'mm': true, From a731d6e131267b8cd1019639b17238eefa3c2e5a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 30 Sep 2021 13:30:26 -0400 Subject: [PATCH 144/249] [flutter_plugin_tools] Validate pubspec description (#4396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pub.dev deducts points for having a pubspec.yaml `description` that is too short or too long; several of our plugins are losing points on this. To ensure that we are following—and modeling—best practices, this adds a check that our `description` fields meet pub.dev expectations. Fixes our existing violations. Two are not published even though this only takes effect once published: - camera: We change this plugin pretty frequently, so this should go out soon without adding a release just for this trivial issue. - wifi_info_flutter: This is deprecated, so we don't plan to release it. It has to be fixed to allow the tool change to land though. --- script/tool/CHANGELOG.md | 2 + .../lib/src/common/repository_package.dart | 9 ++ .../tool/lib/src/pubspec_check_command.dart | 33 +++++++ .../test/common/repository_package_test.dart | 35 +++++++ .../tool/test/pubspec_check_command_test.dart | 93 +++++++++++++++++++ 5 files changed, 172 insertions(+) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 6119545260a..2e6404e2cee 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -10,6 +10,8 @@ major version change restriction. - Improved error handling and error messages in CHANGELOG version checks. - `license-check` now validates Kotlin files. +- `pubspec-check` now checks that the description is of the pub-recommended + length. ## 0.7.1 diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index cb586afb4df..3b4417ac818 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -58,6 +58,15 @@ class RepositoryPackage { bool get isPlatformInterface => directory.basename.endsWith('_platform_interface'); + /// True if this appears to be a platform implementation package, according to + /// repository conventions. + bool get isPlatformImplementation => + // Any part of a federated plugin that isn't the platform interface and + // isn't the app-facing package should be an implementation package. + isFederated && + !isPlatformInterface && + directory.basename != directory.parent.basename; + /// Returns the Flutter example packages contained in the package, if any. Iterable getExamples() { final Directory exampleDirectory = directory.childDirectory('example'); diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index fec0dcef9ac..b99f5af68c4 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -126,6 +126,18 @@ class PubspecCheckCommand extends PackageLoopingCommand { '${indentation * 2}$_expectedIssueLinkFormat'); passing = false; } + + // Don't check descriptions for federated package components other than + // the app-facing package, since they are unlisted, and are expected to + // have short descriptions. + if (!package.isPlatformInterface && !package.isPlatformImplementation) { + final String? descriptionError = + _checkDescription(pubspec, package: package); + if (descriptionError != null) { + printError('$indentation$descriptionError'); + passing = false; + } + } } return passing; @@ -180,6 +192,27 @@ class PubspecCheckCommand extends PackageLoopingCommand { return errorMessages; } + // Validates the "description" field for a package, returning an error + // string if there are any issues. + String? _checkDescription( + Pubspec pubspec, { + required RepositoryPackage package, + }) { + final String? description = pubspec.description; + if (description == null) { + return 'Missing "description"'; + } + + if (description.length < 60) { + return '"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'; + } + if (description.length > 180) { + return '"description" is too long. pub.dev recommends package ' + 'descriptions of 60-180 characters.'; + } + } + bool _checkIssueLink(Pubspec pubspec) { return pubspec.issueTracker ?.toString() diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index 5c5624312f5..4c20389ae4b 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -120,4 +120,39 @@ void main() { plugin.childDirectory('example').childDirectory('example2').path); }); }); + + group('federated plugin queries', () { + test('all return false for a simple plugin', () { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + expect(RepositoryPackage(plugin).isFederated, false); + expect(RepositoryPackage(plugin).isPlatformInterface, false); + expect(RepositoryPackage(plugin).isFederated, false); + }); + + test('handle app-facing packages', () { + final Directory plugin = + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isPlatformInterface, false); + expect(RepositoryPackage(plugin).isPlatformImplementation, false); + }); + + test('handle platform interface packages', () { + final Directory plugin = createFakePlugin('a_plugin_platform_interface', + packagesDir.childDirectory('a_plugin')); + expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isPlatformInterface, true); + expect(RepositoryPackage(plugin).isPlatformImplementation, false); + }); + + test('handle platform implementation packages', () { + // A platform interface can end with anything, not just one of the known + // platform names, because of cases like webview_flutter_wkwebview. + final Directory plugin = createFakePlugin( + 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); + expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isPlatformInterface, false); + expect(RepositoryPackage(plugin).isPlatformImplementation, true); + }); + }); } diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 948136993d1..d09dcebce4a 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -56,6 +56,7 @@ void main() { bool includeHomepage = false, bool includeIssueTracker = true, bool publishable = true, + String? description, }) { final String repositoryPath = repositoryPackagesDirRelativePath ?? name; final String repoLink = 'https://github.com/flutter/' @@ -64,8 +65,11 @@ void main() { final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; + description ??= 'A test package for validating that the pubspec.yaml ' + 'follows repo best practices.'; return ''' name: $name +description: $description ${includeRepository ? 'repository: $repoLink' : ''} ${includeHomepage ? 'homepage: $repoLink' : ''} ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} @@ -327,6 +331,95 @@ ${devDependenciesSection()} ); }); + test('fails when description is too short', () async { + final Directory pluginDirectory = + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, description: 'Too short')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + + test( + 'allows short descriptions for non-app-facing parts of federated plugins', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, description: 'Too short')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + + test('fails when description is too long', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + const String description = 'This description is too long. It just goes ' + 'on and on and on and on and on. pub.dev will down-score it because ' + 'there is just too much here. Someone shoul really cut this down to just ' + 'the core description so that search results are more useful and the ' + 'package does not lose pub points.'; + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, description: description)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too long. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + test('fails when environment section is out of order', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); From b7568184dee6085a61b3408372f6e1cc7d3da44c Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 15 Oct 2021 18:18:02 -0700 Subject: [PATCH 145/249] Bump compileSdkVersion to 31 (#4432) --- script/tool/lib/src/create_all_plugins_app_command.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 6dbebf2f5c7..5d9b4ed9c72 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -93,10 +93,13 @@ class CreateAllPluginsAppCommand extends PluginCommand { final StringBuffer newGradle = StringBuffer(); for (final String line in gradleFile.readAsLinesSync()) { - if (line.contains('minSdkVersion 16')) { - // Android SDK 20 is required by Google maps. - // Android SDK 19 is required by WebView. + if (line.contains('minSdkVersion')) { + // minSdkVersion 20 is required by Google maps. + // minSdkVersion 19 is required by WebView. newGradle.writeln('minSdkVersion 20'); + } else if (line.contains('compileSdkVersion')) { + // compileSdkVersion 31 is required by Camera. + newGradle.writeln('compileSdkVersion 31'); } else { newGradle.writeln(line); } From c2717e76992b0f9161d40e0b639990df3c5847f8 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Tue, 19 Oct 2021 10:48:42 -0700 Subject: [PATCH 146/249] [ci] Replace Firebase Test Lab deprecated Pixel 4 device with Pixel 5 (#4436) --- script/tool/CHANGELOG.md | 1 + .../lib/src/firebase_test_lab_command.dart | 2 +- .../test/firebase_test_lab_command_test.dart | 34 +++++++++---------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2e6404e2cee..3b4ff8f226d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +- Update Firebase Testlab deprecated test device. (Pixel 4 API 29 -> Pixel 5 API 30). - `native-test --android`, `--ios`, and `--macos` now fail plugins that don't have unit tests, rather than skipping them. - Added a new `federation-safety-check` command to help catch changes to diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 941cba3a694..28afc638203 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -54,7 +54,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { splitCommas: false, defaultsTo: [ 'model=walleye,version=26', - 'model=flame,version=29' + 'model=redfin,version=30' ], help: 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index e39ccf30b13..268210d0042 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -101,7 +101,7 @@ void main() { final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -142,7 +142,7 @@ void main() { '/packages/plugin1/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin1/example'), ProcessCall( @@ -156,7 +156,7 @@ void main() { '/packages/plugin2/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin2/example'), ]), @@ -176,7 +176,7 @@ void main() { final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -219,7 +219,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -229,7 +229,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -257,7 +257,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -294,7 +294,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -332,7 +332,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -366,7 +366,7 @@ void main() { final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -400,7 +400,7 @@ void main() { final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -445,7 +445,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -468,7 +468,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -505,7 +505,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -543,7 +543,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -571,7 +571,7 @@ void main() { await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--test-run-id', 'testRunId', '--build-id', @@ -601,7 +601,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), From 5bd3ae6a0ae5eae0aeeea37a432b57e244fbcbee Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 20 Oct 2021 11:03:04 -0400 Subject: [PATCH 147/249] [flutter_plugin_tools] Fix license-check on Windows (#4425) --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/license_check_command.dart | 10 ++++- .../tool/test/license_check_command_test.dart | 39 ++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 3b4ff8f226d..bb4ca2390d7 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -13,6 +13,7 @@ - `license-check` now validates Kotlin files. - `pubspec-check` now checks that the description is of the pub-recommended length. +- Fix `license-check` when run on Windows with line ending conversion enabled. ## 0.7.1 diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 7165e985c05..d2c129ff7b4 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -206,7 +206,10 @@ class LicenseCheckCommand extends PluginCommand { for (final File file in codeFiles) { print('Checking ${file.path}'); - final String content = await file.readAsString(); + // On Windows, git may auto-convert line endings on checkout; this should + // still pass since they will be converted back on commit. + final String content = + (await file.readAsString()).replaceAll('\r\n', '\n'); final String firstParyLicense = firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? @@ -244,7 +247,10 @@ class LicenseCheckCommand extends PluginCommand { for (final File file in files) { print('Checking ${file.path}'); - if (!file.readAsStringSync().contains(_fullBsdLicenseText)) { + // On Windows, git may auto-convert line endings on checkout; this should + // still pass since they will be converted back on commit. + final String contents = file.readAsStringSync().replaceAll('\r\n', '\n'); + if (!contents.contains(_fullBsdLicenseText)) { incorrectLicenseFiles.add(file); } } diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 5a8a90e9a67..e97274afd09 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -48,12 +48,14 @@ void main() { 'Use of this source code is governed by a BSD-style license that can be', 'found in the LICENSE file.', ], + bool useCrlf = false, }) { final List lines = ['$prefix$comment$copyright']; for (final String line in license) { lines.add('$comment$line'); } - file.writeAsStringSync(lines.join('\n') + suffix + '\n'); + final String newline = useCrlf ? '\r\n' : '\n'; + file.writeAsStringSync(lines.join(newline) + suffix + newline); } test('looks at only expected extensions', () async { @@ -140,6 +142,23 @@ void main() { ])); }); + test('passes correct license blocks on Windows', () async { + final File checked = root.childFile('checked.cc'); + checked.createSync(); + _writeLicense(checked, useCrlf: true); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check a file. + expect( + output, + containsAllInOrder([ + contains('Checking checked.cc'), + contains('All files passed validation!'), + ])); + }); + test('handles the comment styles for all supported languages', () async { final File fileA = root.childFile('file_a.cc'); fileA.createSync(); @@ -406,6 +425,24 @@ void main() { ])); }); + test('passes correct LICENSE files on Windows', () async { + final File license = root.childFile('LICENSE'); + license.createSync(); + license + .writeAsStringSync(_correctLicenseFileText.replaceAll('\n', '\r\n')); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check the file. + expect( + output, + containsAllInOrder([ + contains('Checking LICENSE'), + contains('All files passed validation!'), + ])); + }); + test('fails if any first-party LICENSE files are incorrectly formatted', () async { final File license = root.childFile('LICENSE'); From 78395e5adf5db8e72a6e90046e4efd6ad5a82349 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 25 Oct 2021 11:29:07 -0400 Subject: [PATCH 148/249] [flutter_plugin_tools] Fix pubspec-check on Windows (#4428) The repository check always failed when run on Windows, because the relative path generated to check the end of the URL was using local filesystem style for separators, but URLs always use POSIX separators. --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/pubspec_check_command.dart | 2 +- .../tool/test/pubspec_check_command_test.dart | 437 ++++++++++-------- 3 files changed, 247 insertions(+), 193 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index bb4ca2390d7..89bc7759b96 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -14,6 +14,7 @@ - `pubspec-check` now checks that the description is of the pub-recommended length. - Fix `license-check` when run on Windows with line ending conversion enabled. +- Fixed `pubspec-check` on Windows. ## 0.7.1 diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index b99f5af68c4..04c3848a7de 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -177,7 +177,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { errorMessages.add('Missing "repository"'); } else { final String relativePackagePath = - path.relative(package.path, from: packagesDir.parent.path); + getRelativePosixPath(package.directory, from: packagesDir.parent); if (!pubspec.repository!.path.endsWith(relativePackagePath)) { errorMessages .add('The "repository" link should end with the package path.'); diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index d09dcebce4a..ba943903b6c 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -12,62 +12,43 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; -void main() { - group('test pubspec_check_command', () { - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - createPackagesDirectory(parentDir: packagesDir.parent); - processRunner = RecordingProcessRunner(); - final PubspecCheckCommand command = PubspecCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'pubspec_check_command', 'Test for pubspec_check_command'); - runner.addCommand(command); - }); - - /// Returns the top section of a pubspec.yaml for a package named [name], - /// for either a flutter/packages or flutter/plugins package depending on - /// the values of [isPlugin]. - /// - /// By default it will create a header that includes all of the expected - /// values, elements can be changed via arguments to create incorrect - /// entries. - /// - /// If [includeRepository] is true, by default the path in the link will - /// be "packages/[name]"; a different "packages"-relative path can be - /// provided with [repositoryPackagesDirRelativePath]. - String headerSection( - String name, { - bool isPlugin = false, - bool includeRepository = true, - String? repositoryPackagesDirRelativePath, - bool includeHomepage = false, - bool includeIssueTracker = true, - bool publishable = true, - String? description, - }) { - final String repositoryPath = repositoryPackagesDirRelativePath ?? name; - final String repoLink = 'https://github.com/flutter/' - '${isPlugin ? 'plugins' : 'packages'}/tree/master/' - 'packages/$repositoryPath'; - final String issueTrackerLink = - 'https://github.com/flutter/flutter/issues?' - 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; - description ??= 'A test package for validating that the pubspec.yaml ' - 'follows repo best practices.'; - return ''' +/// Returns the top section of a pubspec.yaml for a package named [name], +/// for either a flutter/packages or flutter/plugins package depending on +/// the values of [isPlugin]. +/// +/// By default it will create a header that includes all of the expected +/// values, elements can be changed via arguments to create incorrect +/// entries. +/// +/// If [includeRepository] is true, by default the path in the link will +/// be "packages/[name]"; a different "packages"-relative path can be +/// provided with [repositoryPackagesDirRelativePath]. +String _headerSection( + String name, { + bool isPlugin = false, + bool includeRepository = true, + String? repositoryPackagesDirRelativePath, + bool includeHomepage = false, + bool includeIssueTracker = true, + bool publishable = true, + String? description, +}) { + final String repositoryPath = repositoryPackagesDirRelativePath ?? name; + final List repoLinkPathComponents = [ + 'flutter', + if (isPlugin) 'plugins' else 'packages', + 'tree', + 'master', + 'packages', + repositoryPath, + ]; + final String repoLink = + 'https://github.com/' + repoLinkPathComponents.join('/'); + final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' + 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; + description ??= 'A test package for validating that the pubspec.yaml ' + 'follows repo best practices.'; + return ''' name: $name description: $description ${includeRepository ? 'repository: $repoLink' : ''} @@ -76,64 +57,89 @@ ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} version: 1.0.0 ${publishable ? '' : 'publish_to: \'none\''} '''; - } +} - String environmentSection() { - return ''' +String _environmentSection() { + return ''' environment: sdk: ">=2.12.0 <3.0.0" flutter: ">=2.0.0" '''; - } +} - String flutterSection({ - bool isPlugin = false, - String? implementedPackage, - }) { - final String pluginEntry = ''' +String _flutterSection({ + bool isPlugin = false, + String? implementedPackage, +}) { + final String pluginEntry = ''' plugin: ${implementedPackage == null ? '' : ' implements: $implementedPackage'} platforms: '''; - return ''' + return ''' flutter: ${isPlugin ? pluginEntry : ''} '''; - } +} - String dependenciesSection() { - return ''' +String _dependenciesSection() { + return ''' dependencies: flutter: sdk: flutter '''; - } +} - String devDependenciesSection() { - return ''' +String _devDependenciesSection() { + return ''' dev_dependencies: flutter_test: sdk: flutter '''; - } +} - String falseSecretsSection() { - return ''' +String _falseSecretsSection() { + return ''' false_secrets: - /lib/main.dart '''; - } +} + +void main() { + group('test pubspec_check_command', () { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final PubspecCheckCommand command = PubspecCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'pubspec_check_command', 'Test for pubspec_check_command'); + runner.addCommand(command); + }); test('passes for a plugin following conventions', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} -${falseSecretsSection()} +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_falseSecretsSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -154,12 +160,12 @@ ${falseSecretsSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin')} -${environmentSection()} -${dependenciesSection()} -${devDependenciesSection()} -${flutterSection()} -${falseSecretsSection()} +${_headerSection('plugin')} +${_environmentSection()} +${_dependenciesSection()} +${_devDependenciesSection()} +${_flutterSection()} +${_falseSecretsSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -181,9 +187,9 @@ ${falseSecretsSection()} packageDirectory.createSync(recursive: true); packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('package')} -${environmentSection()} -${dependenciesSection()} +${_headerSection('package')} +${_environmentSection()} +${_dependenciesSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -203,11 +209,11 @@ ${dependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeHomepage: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, includeHomepage: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -230,11 +236,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeRepository: false)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, includeRepository: false)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -256,11 +262,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -283,11 +289,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -309,11 +315,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeIssueTracker: false)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, includeIssueTracker: false)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -336,11 +342,11 @@ ${devDependenciesSection()} createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, description: 'Too short')} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, description: 'Too short')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -365,11 +371,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, description: 'Too short')} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, description: 'Too short')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -397,11 +403,11 @@ ${devDependenciesSection()} 'the core description so that search results are more useful and the ' 'package does not lose pub points.'; pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, description: description)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, description: description)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -424,11 +430,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} -${environmentSection()} +${_headerSection('plugin', isPlugin: true)} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_environmentSection()} '''); Error? commandError; @@ -451,11 +457,11 @@ ${environmentSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${flutterSection(isPlugin: true)} -${environmentSection()} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true)} +${_flutterSection(isPlugin: true)} +${_environmentSection()} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -478,11 +484,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${devDependenciesSection()} -${dependenciesSection()} +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_devDependenciesSection()} +${_dependenciesSection()} '''); Error? commandError; @@ -505,11 +511,11 @@ ${dependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${devDependenciesSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_devDependenciesSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} '''); Error? commandError; @@ -532,12 +538,12 @@ ${dependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${falseSecretsSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_falseSecretsSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -562,11 +568,11 @@ ${devDependenciesSection()} 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin_a_foo', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -590,11 +596,11 @@ ${devDependenciesSection()} 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin_a_foo', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -618,15 +624,15 @@ ${devDependenciesSection()} 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection( +${_headerSection( 'plugin_a_foo', isPlugin: true, repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo', )} -${environmentSection()} -${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -646,15 +652,15 @@ ${devDependenciesSection()} createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection( +${_headerSection( 'plugin_a', isPlugin: true, repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', )} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -676,16 +682,16 @@ ${devDependenciesSection()} packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection( +${_headerSection( 'plugin_a_platform_interface', isPlugin: true, repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_platform_interface', )} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -707,11 +713,11 @@ ${devDependenciesSection()} // Environment section is in the wrong location. // Missing 'implements'. pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true, publishable: false)} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} -${environmentSection()} +${_headerSection('plugin_a_foo', isPlugin: true, publishable: false)} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_environmentSection()} '''); Error? commandError; @@ -737,17 +743,17 @@ ${environmentSection()} // Missing metadata that is only useful for published packages, such as // repository and issue tracker. pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection( +${_headerSection( 'plugin', isPlugin: true, publishable: false, includeRepository: false, includeIssueTracker: false, )} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -762,4 +768,51 @@ ${devDependenciesSection()} ); }); }); + + group('test pubspec_check_command on Windows', () { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockPlatform = MockPlatform(isWindows: true); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final PubspecCheckCommand command = PubspecCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'pubspec_check_command', 'Test for pubspec_check_command'); + runner.addCommand(command); + }); + + test('repository check works', () async { + final Directory packageDirectory = + createFakePackage('package', packagesDir); + + packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${_headerSection('package')} +${_environmentSection()} +${_dependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for package...'), + contains('No issues found!'), + ]), + ); + }); + }); } From 5d15fe9626cfd6b97a2baa9c9f0aa7fac4fc81d8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 10 Nov 2021 14:46:35 -0500 Subject: [PATCH 149/249] [flutter_plugin_tools] Add 'main' support (#4474) Treat `main` the same as `master` for branch-based switching, in preparation for switching the branch names in Flutter repositories. Also updates all of the tests that used `master` as the explicit base to use `main` instead; what the tests use is arbitrary, so they can be switched now even though the repo itself hasn't switched. Part of https://github.com/flutter/flutter/issues/90476 --- script/tool/CHANGELOG.md | 4 +- .../tool/lib/src/common/plugin_command.dart | 5 +- script/tool/lib/src/test_command.dart | 2 +- .../tool/lib/src/version_check_command.dart | 6 +- script/tool/pubspec.yaml | 2 +- script/tool/test/analyze_command_test.dart | 2 +- .../tool/test/common/plugin_command_test.dart | 117 +++++++--------- .../tool/test/pubspec_check_command_test.dart | 2 +- .../tool/test/version_check_command_test.dart | 130 ++++++++---------- 9 files changed, 122 insertions(+), 148 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 89bc7759b96..aa931391a93 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 0.7.2 - Update Firebase Testlab deprecated test device. (Pixel 4 API 29 -> Pixel 5 API 30). - `native-test --android`, `--ios`, and `--macos` now fail plugins that don't @@ -15,6 +15,8 @@ length. - Fix `license-check` when run on Windows with line ending conversion enabled. - Fixed `pubspec-check` on Windows. +- Add support for `main` as a primary branch. `master` continues to work for + compatibility. ## 0.7.1 diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 5d5cbd9abf6..6708f910113 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -81,7 +81,8 @@ abstract class PluginCommand extends Command { argParser.addFlag(_packagesForBranchArg, help: 'This runs on all packages (equivalent to no package selection flag)\n' - 'on master, and behaves like --run-on-changed-packages on any other branch.\n\n' + 'on main (or master), and behaves like --run-on-changed-packages on ' + 'any other branch.\n\n' 'Cannot be combined with $_packagesArg.\n\n' 'This is intended for use in CI.\n', hide: true); @@ -301,7 +302,7 @@ abstract class PluginCommand extends Command { 'only be used in a git repository.'); throw ToolExit(exitInvalidArguments); } else { - runOnChangedPackages = branch != 'master'; + runOnChangedPackages = branch != 'master' && branch != 'main'; // Log the mode for auditing what was intended to run. print('--$_packagesForBranchArg: running on ' '${runOnChangedPackages ? 'changed' : 'all'} packages'); diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 5a0b43d3b22..ee3540d90d3 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -24,7 +24,7 @@ class TestCommand extends PackageLoopingCommand { defaultsTo: '', help: 'Runs Dart unit tests in Dart VM with the given experiments enabled. ' - 'See https://github.com/dart-lang/sdk/blob/master/docs/process/experimental-flags.md ' + 'See https://github.com/dart-lang/sdk/blob/main/docs/process/experimental-flags.md ' 'for details.', ); } diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 90ba0668002..dc664e53d7f 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -262,7 +262,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // This method isn't called unless `version` is non-null. final Version currentVersion = pubspec.version!; Version? previousVersion; + String previousVersionSource; if (getBoolArg(_againstPubFlag)) { + previousVersionSource = 'pub'; previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); if (previousVersion == null) { return _CurrentVersionState.unknown; @@ -273,6 +275,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } } else { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + previousVersionSource = await gitVersionFinder.getBaseSha(); previousVersion = await _getPreviousVersionFromGit(package, gitVersionFinder: gitVersionFinder) ?? Version.none; @@ -310,9 +313,8 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} if (allowedNextVersions.containsKey(currentVersion)) { print('$indentation$previousVersion -> $currentVersion'); } else { - final String source = (getBoolArg(_againstPubFlag)) ? 'pub' : 'master'; printError('${indentation}Incorrectly updated version.\n' - '${indentation}HEAD: $currentVersion, $source: $previousVersion.\n' + '${indentation}HEAD: $currentVersion, $previousVersionSource: $previousVersion.\n' '${indentation}Allowed versions: $allowedNextVersions'); return _CurrentVersionState.invalidChange; } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 689618f0612..8cde4dd46d8 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.7.1 +version: 0.7.2 dependencies: args: ^2.1.0 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 502fa9a0634..878facd83c0 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -253,7 +253,7 @@ void main() { }); // Ensure that the command used to analyze flutter/plugins in the Dart repo: - // https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh + // https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh // continues to work. // // DO NOT remove or modify this test without a coordination plan in place to diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 13724e26e5f..6d586e416b7 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -246,11 +246,8 @@ void main() { test('all plugins should be tested if there are no changes.', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -264,11 +261,8 @@ void main() { ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -283,11 +277,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -302,11 +293,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -322,11 +310,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -342,11 +327,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -362,11 +344,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -382,11 +361,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -398,17 +374,14 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final List output = await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect( output, containsAllInOrder([ contains( - 'Running for all packages that have changed relative to "master"'), + 'Running for all packages that have changed relative to "main"'), ])); expect(command.plugins, unorderedEquals([plugin1.path])); @@ -424,11 +397,8 @@ packages/plugin1/ios/plugin1.m ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path])); }); @@ -444,11 +414,8 @@ packages/plugin2/ios/plugin2.m final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -468,11 +435,8 @@ packages/plugin1/plugin1_web/plugin1_web.dart createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path])); }); @@ -491,11 +455,8 @@ packages/plugin1/plugin1/plugin1.dart packagesDir.childDirectory('plugin1')); final Directory plugin3 = createFakePlugin( 'plugin1_web', packagesDir.childDirectory('plugin1')); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect( command.plugins, @@ -518,7 +479,7 @@ packages/plugin3/plugin3.dart await runCapturingPrint(runner, [ 'sample', '--exclude=plugin2,plugin3', - '--base-sha=master', + '--base-sha=main', '--run-on-changed-packages' ]); @@ -549,6 +510,28 @@ packages/plugin3/plugin3.dart ])); }); + test('tests all packages on main', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'main'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('--packages-for-branch: running on all packages'), + ])); + }); + test('tests all packages on master', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'packages/plugin1/plugin1.dart'), diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index ba943903b6c..9ad1eaa620c 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -38,7 +38,7 @@ String _headerSection( 'flutter', if (isPlugin) 'plugins' else 'packages', 'tree', - 'master', + 'main', 'packages', repositoryPath, ]; diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 39132212d66..30b8855b5a3 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -22,15 +22,15 @@ import 'mocks.dart'; import 'util.dart'; void testAllowedVersion( - String masterVersion, + String mainVersion, String headVersion, { bool allowed = true, NextVersionType? nextVersionType, }) { - final Version master = Version.parse(masterVersion); + final Version main = Version.parse(mainVersion); final Version head = Version.parse(headVersion); final Map allowedVersions = - getAllowedNextVersions(master, newVersion: head); + getAllowedNextVersions(main, newVersion: head); if (allowed) { expect(allowedVersions, contains(head)); if (nextVersionType != null) { @@ -109,10 +109,10 @@ void main() { test('allows valid version', () async { createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, @@ -125,17 +125,17 @@ void main() { expect( gitDirCommands, containsAll([ - equals(['show', 'master:packages/plugin/pubspec.yaml']), + equals(['show', 'main:packages/plugin/pubspec.yaml']), ])); }); test('denies invalid version', () async { createFakePlugin('plugin', packagesDir, version: '0.2.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'main:packages/plugin/pubspec.yaml': 'version: 0.0.1', }; final Future> result = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); await expectLater( result, @@ -145,7 +145,7 @@ void main() { expect( gitDirCommands, containsAll([ - equals(['show', 'master:packages/plugin/pubspec.yaml']), + equals(['show', 'main:packages/plugin/pubspec.yaml']), ])); }); @@ -229,11 +229,11 @@ void main() { createFakePlugin('plugin_platform_interface', packagesDir, version: '1.1.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, containsAllInOrder([ @@ -247,7 +247,7 @@ void main() { containsAll([ equals([ 'show', - 'master:packages/plugin_platform_interface/pubspec.yaml' + 'main:packages/plugin_platform_interface/pubspec.yaml' ]), ])); }); @@ -257,11 +257,11 @@ void main() { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; final Future> output = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); await expectLater( output, throwsA(isA()), @@ -272,7 +272,7 @@ void main() { containsAll([ equals([ 'show', - 'master:packages/plugin_platform_interface/pubspec.yaml' + 'main:packages/plugin_platform_interface/pubspec.yaml' ]), ])); }); @@ -282,7 +282,7 @@ void main() { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; final File changeDescriptionFile = @@ -297,7 +297,7 @@ This is necessary because of X, Y, and Z ## Another section'''); final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', '--change-description-file=${changeDescriptionFile.path}' ]); @@ -317,14 +317,14 @@ This is necessary because of X, Y, and Z createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; Error? commandError; final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', '--change-description-file=a_missing_file.txt' ], errorHandler: (Error e) { commandError = e; @@ -344,12 +344,12 @@ This is necessary because of X, Y, and Z createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', '--ignore-platform-interface-breaks' ]); @@ -375,7 +375,7 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, containsAllInOrder([ @@ -393,11 +393,9 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -422,7 +420,7 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, containsAllInOrder([ @@ -445,11 +443,9 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -477,11 +473,11 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); await expectLater( output, containsAllInOrder([ @@ -506,11 +502,9 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -541,15 +535,13 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -578,15 +570,13 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -615,13 +605,13 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; Error? commandError; final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', ], errorHandler: (Error e) { commandError = e; }); @@ -648,13 +638,13 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; Error? commandError; final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', ], errorHandler: (Error e) { commandError = e; }); @@ -680,10 +670,10 @@ This is necessary because of X, Y, and Z createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint(runner, - ['version-check', '--base-sha=master', '--against-pub']); + ['version-check', '--base-sha=main', '--against-pub']); expect( output, @@ -704,15 +694,13 @@ This is necessary because of X, Y, and Z createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; bool hasError = false; - final List result = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List result = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -736,14 +724,12 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; bool hasError = false; - final List result = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List result = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -767,10 +753,10 @@ ${indentation}HTTP response: null createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; final List result = await runCapturingPrint(runner, - ['version-check', '--base-sha=master', '--against-pub']); + ['version-check', '--base-sha=main', '--against-pub']); expect( result, From d7f8fc267faa12a651e2569eaaf841c5ced9a013 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sun, 14 Nov 2021 20:21:32 -0500 Subject: [PATCH 150/249] [flutter_plugin_tools] Add optional timing info (#4500) Adds a `--log-timing` flag that can be passed to cause package-looping commands to log relative start time and per-package elapsed time, to help with future efforts to improve CI times (e.g., finding unusually slow packages, or designing changes to sharding). Adds this flag to the CI scripts to enable timing there. --- script/tool/CHANGELOG.md | 5 ++ .../src/common/package_looping_command.dart | 58 ++++++++++++++----- .../tool/lib/src/common/plugin_command.dart | 30 ++++++---- .../common/package_looping_command_test.dart | 41 +++++++++++++ 4 files changed, 111 insertions(+), 23 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index aa931391a93..5a037c89c61 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +- Added `--log-timing` to add timing information to package headers in looping + commands. + ## 0.7.2 - Update Firebase Testlab deprecated test device. (Pixel 4 API 29 -> Pixel 5 API 30). diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 973ac9995cb..bfee71a0f4c 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -170,7 +170,7 @@ abstract class PackageLoopingCommand extends PluginCommand { /// messages. DO NOT RELY on someone noticing a warning; instead, use it for /// things that might be useful to someone debugging an unexpected result. void logWarning(String warningMessage) { - print(Colorize(warningMessage)..yellow()); + _printColorized(warningMessage, Styles.YELLOW); if (_currentPackageEntry != null) { _packagesWithWarnings.add(_currentPackageEntry!); } else { @@ -219,6 +219,8 @@ abstract class PackageLoopingCommand extends PluginCommand { _otherWarningCount = 0; _currentPackageEntry = null; + final DateTime runStart = DateTime.now(); + await initializeRun(); final List targetPackages = @@ -227,8 +229,9 @@ abstract class PackageLoopingCommand extends PluginCommand { final Map results = {}; for (final PackageEnumerationEntry entry in targetPackages) { + final DateTime packageStart = DateTime.now(); _currentPackageEntry = entry; - _printPackageHeading(entry); + _printPackageHeading(entry, startTime: runStart); // Command implementations should never see excluded packages; they are // included at this level only for logging. @@ -246,11 +249,20 @@ abstract class PackageLoopingCommand extends PluginCommand { result = PackageResult.fail(['Unhandled exception']); } if (result.state == RunState.skipped) { - final String message = - '${indentation}SKIPPING: ${result.details.first}'; - captureOutput ? print(message) : print(Colorize(message)..darkGray()); + _printColorized('${indentation}SKIPPING: ${result.details.first}', + Styles.DARK_GRAY); } results[entry] = result; + + // Only log an elapsed time for long output; for short output, comparing + // the relative timestamps of successive entries should be trivial. + if (shouldLogTiming && hasLongOutput) { + final Duration elapsedTime = DateTime.now().difference(packageStart); + _printColorized( + '\n[${entry.package.displayName} completed in ' + '${elapsedTime.inMinutes}m ${elapsedTime.inSeconds % 60}s]', + Styles.DARK_GRAY); + } } _currentPackageEntry = null; @@ -287,11 +299,20 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Something is always printed to make it easier to distinguish between /// a command running for a package and producing no output, and a command /// not having been run for a package. - void _printPackageHeading(PackageEnumerationEntry entry) { + void _printPackageHeading(PackageEnumerationEntry entry, + {required DateTime startTime}) { final String packageDisplayName = entry.package.displayName; String heading = entry.excluded ? 'Not running for $packageDisplayName; excluded' : 'Running for $packageDisplayName'; + + if (shouldLogTiming) { + final Duration relativeTime = DateTime.now().difference(startTime); + final String timeString = _formatDurationAsRelativeTime(relativeTime); + heading = + hasLongOutput ? '$heading [@$timeString]' : '[$timeString] $heading'; + } + if (hasLongOutput) { heading = ''' @@ -302,13 +323,7 @@ abstract class PackageLoopingCommand extends PluginCommand { } else if (!entry.excluded) { heading = '$heading...'; } - if (captureOutput) { - print(heading); - } else { - final Colorize colorizeHeading = Colorize(heading); - print( - entry.excluded ? colorizeHeading.darkGray() : colorizeHeading.cyan()); - } + _printColorized(heading, entry.excluded ? Styles.DARK_GRAY : Styles.CYAN); } /// Prints a summary of packges run, packages skipped, and warnings. @@ -401,4 +416,21 @@ abstract class PackageLoopingCommand extends PluginCommand { } _printError(failureListFooter); } + + /// Prints [message] in [color] unless [captureOutput] is set, in which case + /// it is printed without color. + void _printColorized(String message, Styles color) { + if (captureOutput) { + print(message); + } else { + print(Colorize(message)..apply(color)); + } + } + + /// Returns a duration [d] formatted as minutes:seconds. Does not use hours, + /// since time logging is primarily intended for CI, where durations should + /// always be less than an hour. + String _formatDurationAsRelativeTime(Duration d) { + return '${d.inMinutes}:${(d.inSeconds % 60).toString().padLeft(2, '0')}'; + } } diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 6708f910113..f40a102dfbc 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -76,7 +76,7 @@ abstract class PluginCommand extends Command { '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.\n\n' + 'See $_baseShaArg if a custom base is needed to determine the diff.\n\n' 'Cannot be combined with $_packagesArg.\n'); argParser.addFlag(_packagesForBranchArg, help: @@ -86,20 +86,25 @@ abstract class PluginCommand extends Command { 'Cannot be combined with $_packagesArg.\n\n' 'This is intended for use in CI.\n', hide: true); - argParser.addOption(_kBaseSha, + argParser.addOption(_baseShaArg, 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.'); + argParser.addFlag(_logTimingArg, + help: 'Logs timing information.\n\n' + 'Currently only logs per-package timing for multi-package commands, ' + 'but more information may be added in the future.'); } - static const String _pluginsArg = 'plugins'; - static const String _packagesArg = 'packages'; - static const String _shardIndexArg = 'shardIndex'; - static const String _shardCountArg = 'shardCount'; + static const String _baseShaArg = 'base-sha'; static const String _excludeArg = 'exclude'; - static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _logTimingArg = 'log-timing'; + static const String _packagesArg = 'packages'; static const String _packagesForBranchArg = 'packages-for-branch'; - static const String _kBaseSha = 'base-sha'; + static const String _pluginsArg = 'plugins'; + static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _shardCountArg = 'shardCount'; + static const String _shardIndexArg = 'shardIndex'; /// The directory containing the plugin packages. final Directory packagesDir; @@ -183,6 +188,11 @@ abstract class PluginCommand extends Command { return (argResults![key] as List?) ?? []; } + /// If true, commands should log timing information that might be useful in + /// analyzing their runtime (e.g., the per-package time for multi-package + /// commands). + bool get shouldLogTiming => getBoolArg(_logTimingArg); + void _checkSharding() { final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); @@ -411,11 +421,11 @@ abstract class PluginCommand extends Command { return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); } - /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. + /// Retrieve an instance of [GitVersionFinder] based on `_baseShaArg` and [gitDir]. /// /// Throws tool exit if [gitDir] nor root directory is a git directory. Future retrieveVersionFinder() async { - final String baseSha = getStringArg(_kBaseSha); + final String baseSha = getStringArg(_baseShaArg); final GitVersionFinder gitVersionFinder = GitVersionFinder(await gitDir, baseSha); diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 7cf03960a74..6e46a3330cc 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -22,6 +22,7 @@ import '../util.dart'; import 'plugin_command_test.mocks.dart'; // Constants for colorized output start and end. +const String _startElapsedTimeColor = '\x1B[90m'; const String _startErrorColor = '\x1B[31m'; const String _startHeadingColor = '\x1B[36m'; const String _startSkipColor = '\x1B[90m'; @@ -272,6 +273,46 @@ void main() { ])); }); + test('prints timing info in long-form output when requested', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = + await runCommand(command, arguments: ['--log-timing']); + + const String separator = + '============================================================'; + expect( + output, + containsAllInOrder([ + '$_startHeadingColor\n$separator\n|| Running for package_a [@0:00]\n$separator\n$_endColor', + '$_startElapsedTimeColor\n[package_a completed in 0m 0s]$_endColor', + '$_startHeadingColor\n$separator\n|| Running for package_b [@0:00]\n$separator\n$_endColor', + '$_startElapsedTimeColor\n[package_b completed in 0m 0s]$_endColor', + ])); + }); + + test('prints timing info in short-form output when requested', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = + await runCommand(command, arguments: ['--log-timing']); + + expect( + output, + containsAllInOrder([ + '$_startHeadingColor[0:00] Running for package_a...$_endColor', + '$_startHeadingColor[0:00] Running for package_b...$_endColor', + ])); + // Short-form output should not include elapsed time. + expect(output, isNot(contains('[package_a completed in 0m 0s]'))); + }); + test('shows the success message when nothing fails', () async { createFakePackage('package_a', packagesDir); createFakePackage('package_b', packagesDir); From f4546c0791413348e539f62330c99dab16eee2e7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sun, 14 Nov 2021 21:26:05 -0500 Subject: [PATCH 151/249] [flutter_plugin_tools] Build gtest unit tests (#4492) --- script/tool/CHANGELOG.md | 2 + script/tool/lib/src/common/cmake.dart | 118 ++++++++++++ script/tool/lib/src/common/gradle.dart | 3 - script/tool/lib/src/native_test_command.dart | 69 +++++-- .../tool/test/native_test_command_test.dart | 172 +++++++++++++++--- 5 files changed, 324 insertions(+), 40 deletions(-) create mode 100644 script/tool/lib/src/common/cmake.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 5a037c89c61..31efc28aa3e 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,7 @@ ## NEXT +- `native-test` now builds unit tests before running them on Windows and Linux, + matching the behavior of other platforms. - Added `--log-timing` to add timing information to package headers in looping commands. diff --git a/script/tool/lib/src/common/cmake.dart b/script/tool/lib/src/common/cmake.dart new file mode 100644 index 00000000000..04ad880292b --- /dev/null +++ b/script/tool/lib/src/common/cmake.dart @@ -0,0 +1,118 @@ +// 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:flutter_plugin_tools/src/common/core.dart'; +import 'package:platform/platform.dart'; + +import 'process_runner.dart'; + +const String _cacheCommandKey = 'CMAKE_COMMAND:INTERNAL'; + +/// A utility class for interacting with CMake projects. +class CMakeProject { + /// Creates an instance that runs commands for [project] with the given + /// [processRunner]. + CMakeProject( + this.flutterProject, { + required this.buildMode, + this.processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + }); + + /// The directory of a Flutter project to run Gradle commands in. + final Directory flutterProject; + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// The platform that commands are being run on. + final Platform platform; + + /// The build mode (e.g., Debug, Release). + /// + /// This is a constructor paramater because on Linux many properties depend + /// on the build mode since it uses a single-configuration generator. + final String buildMode; + + late final String _cmakeCommand = _determineCmakeCommand(); + + /// The project's platform directory name. + String get _platformDirName => platform.isWindows ? 'windows' : 'linux'; + + /// The project's 'example' build directory for this instance's platform. + Directory get buildDirectory { + Directory buildDir = + flutterProject.childDirectory('build').childDirectory(_platformDirName); + if (platform.isLinux) { + buildDir = buildDir + // TODO(stuartmorgan): Support arm64 if that ever becomes a supported + // CI configuration for the repository. + .childDirectory('x64') + // Linux uses a single-config generator, so the base build directory + // includes the configuration. + .childDirectory(buildMode.toLowerCase()); + } + return buildDir; + } + + File get _cacheFile => buildDirectory.childFile('CMakeCache.txt'); + + /// Returns the CMake command to run build commands for this project. + /// + /// Assumes the project has been built at least once, such that the CMake + /// generation step has run. + String getCmakeCommand() { + return _cmakeCommand; + } + + /// Returns the CMake command to run build commands for this project. This is + /// used to initialize _cmakeCommand, and should not be called directly. + /// + /// Assumes the project has been built at least once, such that the CMake + /// generation step has run. + String _determineCmakeCommand() { + // On Linux 'cmake' is expected to be in the path, so doesn't need to + // be lookup up and cached. + if (platform.isLinux) { + return 'cmake'; + } + final File cacheFile = _cacheFile; + String? command; + for (String line in cacheFile.readAsLinesSync()) { + line = line.trim(); + if (line.startsWith(_cacheCommandKey)) { + command = line.substring(line.indexOf('=') + 1).trim(); + break; + } + } + if (command == null) { + printError('Unable to find CMake command in ${cacheFile.path}'); + throw ToolExit(100); + } + return command; + } + + /// Whether or not the project is ready to have CMake commands run on it + /// (i.e., whether the `flutter` tool has generated the necessary files). + bool isConfigured() => _cacheFile.existsSync(); + + /// Runs a `cmake` command with the given parameters. + Future runBuild( + String target, { + List arguments = const [], + }) { + return processRunner.runAndStream( + getCmakeCommand(), + [ + '--build', + buildDirectory.path, + '--target', + target, + if (platform.isWindows) ...['--config', buildMode], + ...arguments, + ], + ); + } +} diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart index e7214bf2971..9da4e89811e 100644 --- a/script/tool/lib/src/common/gradle.dart +++ b/script/tool/lib/src/common/gradle.dart @@ -14,9 +14,6 @@ const String _gradleWrapperNonWindows = 'gradlew'; class GradleProject { /// Creates an instance that runs commands for [project] with the given /// [processRunner]. - /// - /// If [log] is true, commands run by this instance will long various status - /// messages. GradleProject( this.flutterProject, { this.processRunner = const ProcessRunner(), diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 4911b4aeb15..0b0dd26ba22 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -5,6 +5,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; +import 'common/cmake.dart'; import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; @@ -456,8 +457,8 @@ this command. file.basename.endsWith('_tests.exe'); } - return _runGoogleTestTests(plugin, - buildDirectoryName: 'windows', isTestBinary: isTestBinary); + return _runGoogleTestTests(plugin, 'Windows', 'Debug', + isTestBinary: isTestBinary); } Future<_PlatformResult> _testLinux( @@ -471,8 +472,16 @@ this command. file.basename.endsWith('_tests'); } - return _runGoogleTestTests(plugin, - buildDirectoryName: 'linux', isTestBinary: isTestBinary); + // Since Linux uses a single-config generator, building-examples only + // generates the build files for release, so the tests have to be run in + // release mode as well. + // + // TODO(stuartmorgan): Consider adding a command to `flutter` that would + // generate build files without doing a build, and using that instead of + // relying on running build-examples. See + // https://github.com/flutter/flutter/issues/93407. + return _runGoogleTestTests(plugin, 'Linux', 'Release', + isTestBinary: isTestBinary); } /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s @@ -482,38 +491,66 @@ this command. /// The binaries are assumed to be Google Test test binaries, thus returning /// zero for success and non-zero for failure. Future<_PlatformResult> _runGoogleTestTests( - RepositoryPackage plugin, { - required String buildDirectoryName, + RepositoryPackage plugin, + String platformName, + String buildMode, { required bool Function(File) isTestBinary, }) async { final List testBinaries = []; + bool hasMissingBuild = false; + bool buildFailed = false; for (final RepositoryPackage example in plugin.getExamples()) { - final Directory buildDir = example.directory - .childDirectory('build') - .childDirectory(buildDirectoryName); - if (!buildDir.existsSync()) { + final CMakeProject project = CMakeProject(example.directory, + buildMode: buildMode, + processRunner: processRunner, + platform: platform); + if (!project.isConfigured()) { + printError('ERROR: Run "flutter build" on ${example.displayName}, ' + 'or run this tool\'s "build-examples" command, for the target ' + 'platform before executing tests.'); + hasMissingBuild = true; continue; } - testBinaries.addAll(buildDir + + // By repository convention, example projects create an aggregate target + // called 'unit_tests' that builds all unit tests (usually just an alias + // for a specific test target). + final int exitCode = await project.runBuild('unit_tests'); + if (exitCode != 0) { + printError('${example.displayName} unit tests failed to build.'); + buildFailed = true; + } + + testBinaries.addAll(project.buildDirectory .listSync(recursive: true) .whereType() .where(isTestBinary) .where((File file) { - // Only run the release build of the unit tests, to avoid running the - // same tests multiple times. Release is used rather than debug since - // `build-examples` builds release versions. + // Only run the `buildMode` build of the unit tests, to avoid running + // the same tests multiple times. final List components = path.split(file.path); - return components.contains('release') || components.contains('Release'); + return components.contains(buildMode) || + components.contains(buildMode.toLowerCase()); })); } + if (hasMissingBuild) { + return _PlatformResult(RunState.failed, + error: 'Examples must be built before testing.'); + } + + if (buildFailed) { + return _PlatformResult(RunState.failed, + error: 'Failed to build $platformName unit tests.'); + } + if (testBinaries.isEmpty) { final String binaryExtension = platform.isWindows ? '.exe' : ''; printError( 'No test binaries found. At least one *_test(s)$binaryExtension ' 'binary should be built by the example(s)'); return _PlatformResult(RunState.failed, - error: 'No $buildDirectoryName unit tests found'); + error: 'No $platformName unit tests found'); } bool passing = true; diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index ba93efcb3ac..697cbd4b84d 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -8,10 +8,12 @@ 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/cmake.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/native_test_command.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -53,6 +55,16 @@ final Map _kDeviceListMap = { } }; +const String _fakeCmakeCommand = 'path/to/cmake'; + +void _createFakeCMakeCache(Directory pluginDir, Platform platform) { + final CMakeProject project = CMakeProject(pluginDir.childDirectory('example'), + platform: platform, buildMode: 'Release'); + final File cache = project.buildDirectory.childFile('CMakeCache.txt'); + cache.createSync(recursive: true); + cache.writeAsStringSync('CMAKE_COMMAND:INTERNAL=$_fakeCmakeCommand'); +} + // TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of // doing all the process mocking and validation. void main() { @@ -67,7 +79,10 @@ void main() { setUp(() { fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(isMacOS: true); + // iOS and macOS tests expect macOS, Linux tests expect Linux; nothing + // needs to distinguish between Linux and macOS, so set both to true to + // allow them to share a setup group. + mockPlatform = MockPlatform(isMacOS: true, isLinux: true); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); final NativeTestCommand command = NativeTestCommand(packagesDir, @@ -133,6 +148,26 @@ void main() { package.path); } + // Returns the ProcessCall to expect for build the Linux unit tests for the + // given plugin. + ProcessCall _getLinuxBuildCall(Directory pluginDir) { + return ProcessCall( + 'cmake', + [ + '--build', + pluginDir + .childDirectory('example') + .childDirectory('build') + .childDirectory('linux') + .childDirectory('x64') + .childDirectory('release') + .path, + '--target', + 'unit_tests' + ], + null); + } + test('fails if no platforms are provided', () async { Error? commandError; final List output = await runCapturingPrint( @@ -844,15 +879,16 @@ void main() { }); group('Linux', () { - test('runs unit tests', () async { + test('builds and runs unit tests', () async { const String testBinaryRelativePath = - 'build/linux/foo/release/bar/plugin_test'; + 'build/linux/x64/release/bar/plugin_test'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File testBinary = childFileWithSubcomponents(pluginDirectory, ['example', ...testBinaryRelativePath.split('/')]); @@ -874,15 +910,16 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getLinuxBuildCall(pluginDirectory), ProcessCall(testBinary.path, const [], null), ])); }); test('only runs release unit tests', () async { const String debugTestBinaryRelativePath = - 'build/linux/foo/debug/bar/plugin_test'; + 'build/linux/x64/debug/bar/plugin_test'; const String releaseTestBinaryRelativePath = - 'build/linux/foo/release/bar/plugin_test'; + 'build/linux/x64/release/bar/plugin_test'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', @@ -890,6 +927,7 @@ void main() { ], platformSupport: { kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File releaseTestBinary = childFileWithSubcomponents( pluginDirectory, @@ -909,15 +947,15 @@ void main() { ]), ); - // Only the release version should be run. expect( processRunner.recordedCalls, orderedEquals([ + _getLinuxBuildCall(pluginDirectory), ProcessCall(releaseTestBinary.path, const [], null), ])); }); - test('fails if there are no unit tests', () async { + test('fails if CMake has not been configured', () async { createFakePlugin('plugin', packagesDir, platformSupport: { kPlatformLinux: const PlatformDetails(PlatformSupport.inline), @@ -936,22 +974,56 @@ void main() { expect( output, containsAllInOrder([ - contains('No test binaries found.'), + contains('plugin:\n' + ' Examples must be built before testing.') ]), ); expect(processRunner.recordedCalls, orderedEquals([])); }); + test('fails if there are no unit tests', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getLinuxBuildCall(pluginDirectory), + ])); + }); + test('fails if a unit test fails', () async { const String testBinaryRelativePath = - 'build/linux/foo/release/bar/plugin_test'; + 'build/linux/x64/release/bar/plugin_test'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File testBinary = childFileWithSubcomponents(pluginDirectory, ['example', ...testBinaryRelativePath.split('/')]); @@ -979,6 +1051,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getLinuxBuildCall(pluginDirectory), ProcessCall(testBinary.path, const [], null), ])); }); @@ -1524,16 +1597,37 @@ void main() { runner.addCommand(command); }); + // Returns the ProcessCall to expect for build the Windows unit tests for + // the given plugin. + ProcessCall _getWindowsBuildCall(Directory pluginDir) { + return ProcessCall( + _fakeCmakeCommand, + [ + '--build', + pluginDir + .childDirectory('example') + .childDirectory('build') + .childDirectory('windows') + .path, + '--target', + 'unit_tests', + '--config', + 'Debug' + ], + null); + } + group('Windows', () { test('runs unit tests', () async { const String testBinaryRelativePath = - 'build/windows/foo/Release/bar/plugin_test.exe'; + 'build/windows/Debug/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File testBinary = childFileWithSubcomponents(pluginDirectory, ['example', ...testBinaryRelativePath.split('/')]); @@ -1555,15 +1649,16 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getWindowsBuildCall(pluginDirectory), ProcessCall(testBinary.path, const [], null), ])); }); - test('only runs release unit tests', () async { + test('only runs debug unit tests', () async { const String debugTestBinaryRelativePath = - 'build/windows/foo/Debug/bar/plugin_test.exe'; + 'build/windows/Debug/bar/plugin_test.exe'; const String releaseTestBinaryRelativePath = - 'build/windows/foo/Release/bar/plugin_test.exe'; + 'build/windows/Release/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', @@ -1571,10 +1666,10 @@ void main() { ], platformSupport: { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); - final File releaseTestBinary = childFileWithSubcomponents( - pluginDirectory, - ['example', ...releaseTestBinaryRelativePath.split('/')]); + final File debugTestBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...debugTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ 'native-test', @@ -1590,15 +1685,15 @@ void main() { ]), ); - // Only the release version should be run. expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(releaseTestBinary.path, const [], null), + _getWindowsBuildCall(pluginDirectory), + ProcessCall(debugTestBinary.path, const [], null), ])); }); - test('fails if there are no unit tests', () async { + test('fails if CMake has not been configured', () async { createFakePlugin('plugin', packagesDir, platformSupport: { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), @@ -1617,22 +1712,56 @@ void main() { expect( output, containsAllInOrder([ - contains('No test binaries found.'), + contains('plugin:\n' + ' Examples must be built before testing.') ]), ); expect(processRunner.recordedCalls, orderedEquals([])); }); + test('fails if there are no unit tests', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getWindowsBuildCall(pluginDirectory), + ])); + }); + test('fails if a unit test fails', () async { const String testBinaryRelativePath = - 'build/windows/foo/Release/bar/plugin_test.exe'; + 'build/windows/Debug/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File testBinary = childFileWithSubcomponents(pluginDirectory, ['example', ...testBinaryRelativePath.split('/')]); @@ -1660,6 +1789,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getWindowsBuildCall(pluginDirectory), ProcessCall(testBinary.path, const [], null), ])); }); From 7b6ac13139de8ed12cbcefac7cb9f4dfc248908a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 23 Nov 2021 11:34:18 -0500 Subject: [PATCH 152/249] [flutter_plugin_tools] Check for missing version and CHANGELOG updates (#4530) The currently documented repository policy is to: - require version updates for packages changes that don't meet specific exemptions, and - require CHANGELOG changes for essentially all changes. This adds tooling that enforces that policy, with a mechanism for overriding it via PR descriptions, to avoid cases where they are accidentally omitted without reviewers catching it. In order to facilitate testing (which require mocking another `git` command), this also updates the existing `version-check` tests: - Replaces the custom git result injection/validating with the newer bind-to-process-mocks approach that is now used in the rest of the tool tests. - Fixes some tests that were only checking for `ToolExit` to also check the error output, in order to ensure that failure tests are not accidentally passing for the wrong reason (as is being done in general as tests in the tooling are updated). Fixes https://github.com/flutter/flutter/issues/93790 --- script/tool/CHANGELOG.md | 7 +- .../lib/src/common/git_version_finder.dart | 5 +- .../tool/lib/src/version_check_command.dart | 151 +++- .../test/common/git_version_finder_test.dart | 4 +- .../tool/test/version_check_command_test.dart | 676 ++++++++++++++---- 5 files changed, 680 insertions(+), 163 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 31efc28aa3e..ee23428e66c 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,9 +1,12 @@ -## NEXT +## 0.7.3 - `native-test` now builds unit tests before running them on Windows and Linux, matching the behavior of other platforms. -- Added `--log-timing` to add timing information to package headers in looping +- Adds `--log-timing` to add timing information to package headers in looping commands. +- Adds a `--check-for-missing-changes` flag to `version-check` that requires + version updates (except for recognized exemptions) and CHANGELOG changes when + modifying packages, unless the PR description explains why it's not needed. ## 0.7.2 diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index 1cdd2fcc409..331187335f5 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -75,8 +75,9 @@ class GitVersionFinder { io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], throwOnError: false); - if (baseShaFromMergeBase.stderr != null || - baseShaFromMergeBase.stdout == null) { + final String stdout = (baseShaFromMergeBase.stdout as String? ?? '').trim(); + final String stderr = (baseShaFromMergeBase.stdout as String? ?? '').trim(); + if (stderr.isNotEmpty || stdout.isEmpty) { baseShaFromMergeBase = await baseGitDir .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); } diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index dc664e53d7f..1ec5dc4f249 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -120,6 +120,14 @@ class VersionCheckCommand extends PackageLoopingCommand { '(e.g., PR description or commit message).\n\n' 'If supplied, this is used to allow overrides to some version ' 'checks.'); + argParser.addFlag(_checkForMissingChanges, + help: 'Validates that changes to packages include CHANGELOG and ' + 'version changes unless they meet an established exemption.\n\n' + 'If used with --$_changeDescriptionFile, this is should only be ' + 'used in pre-submit CI checks, to prevent the possibility of ' + 'post-submit breakage if an override justification is not ' + 'transferred into the commit message.', + hide: true); argParser.addFlag(_ignorePlatformInterfaceBreaks, help: 'Bypasses the check that platform interfaces do not contain ' 'breaking changes.\n\n' @@ -133,6 +141,7 @@ class VersionCheckCommand extends PackageLoopingCommand { static const String _againstPubFlag = 'against-pub'; static const String _changeDescriptionFile = 'change-description-file'; + static const String _checkForMissingChanges = 'check-for-missing-changes'; static const String _ignorePlatformInterfaceBreaks = 'ignore-platform-interface-breaks'; @@ -141,8 +150,26 @@ class VersionCheckCommand extends PackageLoopingCommand { static const String _breakingChangeJustificationMarker = '## Breaking change justification'; + /// The string that must be at the start of a line in [_changeDescriptionFile] + /// to allow skipping a version change for a PR that would normally require + /// one. + static const String _missingVersionChangeJustificationMarker = + 'No version change:'; + + /// The string that must be at the start of a line in [_changeDescriptionFile] + /// to allow skipping a CHANGELOG change for a PR that would normally require + /// one. + static const String _missingChangelogChangeJustificationMarker = + 'No CHANGELOG change:'; + final PubVersionFinder _pubVersionFinder; + late final GitVersionFinder _gitVersionFinder; + late final String _mergeBase; + late final List _changedFiles; + + late final String _changeDescription = _loadChangeDescription(); + @override final String name = 'version-check'; @@ -156,7 +183,11 @@ class VersionCheckCommand extends PackageLoopingCommand { bool get hasLongOutput => false; @override - Future initializeRun() async {} + Future initializeRun() async { + _gitVersionFinder = await retrieveVersionFinder(); + _mergeBase = await _gitVersionFinder.getBaseSha(); + _changedFiles = await _gitVersionFinder.getChangedFiles(); + } @override Future runForPackage(RepositoryPackage package) async { @@ -206,6 +237,17 @@ class VersionCheckCommand extends PackageLoopingCommand { errors.add('CHANGELOG.md failed validation.'); } + // If there are no other issues, make sure that there isn't a missing + // change to the version and/or CHANGELOG. + if (getBoolArg(_checkForMissingChanges) && + !versionChanged && + errors.isEmpty) { + final String? error = await _checkForMissingChangeError(package); + if (error != null) { + errors.add(error); + } + } + return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); @@ -239,10 +281,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } /// Returns the version of [package] from git at the base comparison hash. - Future _getPreviousVersionFromGit( - RepositoryPackage package, { - required GitVersionFinder gitVersionFinder, - }) async { + Future _getPreviousVersionFromGit(RepositoryPackage package) async { final File pubspecFile = package.pubspecFile; final String relativePath = path.relative(pubspecFile.absolute.path, from: (await gitDir).path); @@ -250,7 +289,8 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final String gitPath = path.style == p.Style.windows ? p.posix.joinAll(path.split(relativePath)) : relativePath; - return await gitVersionFinder.getPackageVersion(gitPath); + return await _gitVersionFinder.getPackageVersion(gitPath, + gitRef: _mergeBase); } /// Returns the state of the verison of [package] relative to the comparison @@ -274,11 +314,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} '$indentation${pubspec.name}: Current largest version on pub: $previousVersion'); } } else { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - previousVersionSource = await gitVersionFinder.getBaseSha(); - previousVersion = await _getPreviousVersionFromGit(package, - gitVersionFinder: gitVersionFinder) ?? - Version.none; + previousVersionSource = _mergeBase; + previousVersion = + await _getPreviousVersionFromGit(package) ?? Version.none; } if (previousVersion == Version.none) { print('${indentation}Unable to find previous version ' @@ -462,9 +500,11 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return false; } + String _getChangeDescription() => _changeDescription; + /// Returns the contents of the file pointed to by [_changeDescriptionFile], /// or an empty string if that flag is not provided. - String _getChangeDescription() { + String _loadChangeDescription() { final String path = getStringArg(_changeDescriptionFile); if (path.isEmpty) { return ''; @@ -476,4 +516,91 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. } return file.readAsStringSync(); } + + /// Returns an error string if the changes to this package should have + /// resulted in a version change, or shoud have resulted in a CHANGELOG change + /// but didn't. + /// + /// This should only be called if the version did not change. + Future _checkForMissingChangeError(RepositoryPackage package) async { + // Find the relative path to the current package, as it would appear at the + // beginning of a path reported by getChangedFiles() (which always uses + // Posix paths). + final Directory gitRoot = + packagesDir.fileSystem.directory((await gitDir).path); + final String relativePackagePath = + getRelativePosixPath(package.directory, from: gitRoot) + '/'; + bool hasChanges = false; + bool needsVersionChange = false; + bool hasChangelogChange = false; + for (final String path in _changedFiles) { + // Only consider files within the package. + if (!path.startsWith(relativePackagePath)) { + continue; + } + hasChanges = true; + + final List components = p.posix.split(path); + final bool isChangelog = components.last == 'CHANGELOG.md'; + if (isChangelog) { + hasChangelogChange = true; + } + + if (!needsVersionChange && + !isChangelog && + // The example's main.dart is shown on pub.dev, but for anything else + // in the example publishing has no purpose. + !(components.contains('example') && components.last != 'main.dart') && + // Changes to tests don't need to be published. + !components.contains('test') && + !components.contains('androidTest') && + !components.contains('RunnerTests') && + !components.contains('RunnerUITests') && + // Ignoring lints doesn't affect clients. + !components.contains('lint-baseline.xml')) { + needsVersionChange = true; + } + } + + if (!hasChanges) { + return null; + } + + if (needsVersionChange) { + if (_getChangeDescription().split('\n').any((String line) => + line.startsWith(_missingVersionChangeJustificationMarker))) { + logWarning('Ignoring lack of version change due to ' + '"$_missingVersionChangeJustificationMarker" in the ' + 'change description.'); + } else { + printError( + 'No version change found, but the change to this package could ' + 'not be verified to be exempt from version changes according to ' + 'repository policy. If this is a false positive, please ' + 'add a line starting with\n' + '$_missingVersionChangeJustificationMarker\n' + 'to your PR description with an explanation of why it is exempt.'); + return 'Missing version change'; + } + } + + if (!hasChangelogChange) { + if (_getChangeDescription().split('\n').any((String line) => + line.startsWith(_missingChangelogChangeJustificationMarker))) { + logWarning('Ignoring lack of CHANGELOG update due to ' + '"$_missingChangelogChangeJustificationMarker" in the ' + 'change description.'); + } else { + printError( + 'No CHANGELOG change found. If this PR needs an exemption from' + 'the standard policy of listing all changes in the CHANGELOG, ' + 'please add a line starting with\n' + '$_missingChangelogChangeJustificationMarker\n' + 'to your PR description with an explanation of why.'); + return 'Missing CHANGELOG change'; + } + } + + return null; + } } diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart index f1f40b5e003..fa8b1c410dd 100644 --- a/script/tool/test/common/git_version_finder_test.dart +++ b/script/tool/test/common/git_version_finder_test.dart @@ -14,7 +14,7 @@ void main() { late List?> gitDirCommands; late String gitDiffResponse; late MockGitDir gitDir; - String? mergeBaseResponse; + String mergeBaseResponse = ''; setUp(() { gitDirCommands = ?>[]; @@ -74,7 +74,7 @@ file2/file2.cc final GitVersionFinder finder = GitVersionFinder(gitDir, null); await finder.getChangedFiles(); verify(gitDir.runCommand( - ['diff', '--name-only', mergeBaseResponse!, 'HEAD'])); + ['diff', '--name-only', mergeBaseResponse, 'HEAD'])); }); test('use correct base sha if specified', () async { diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 30b8855b5a3..5a5a0a108a3 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.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; @@ -51,8 +50,6 @@ void main() { late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; - late List> gitDirCommands; - Map gitShowResponses; late MockGitDir gitDir; // Ignored if mockHttpResponse is set. int mockHttpStatus; @@ -63,27 +60,17 @@ void main() { mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - gitDirCommands = >[]; - gitShowResponses = {}; gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); 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] == 'show') { - final String? response = - gitShowResponses[invocation.positionalArguments[0][1]]; - if (response == null) { - throw const io.ProcessException('git', ['show']); - } - when(mockProcessResult.stdout as String?) - .thenReturn(response); - } else if (invocation.positionalArguments[0][0] == 'merge-base') { - when(mockProcessResult.stdout as String?) - .thenReturn('abc123'); - } - return Future.value(mockProcessResult); + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through the process runner, to make mock output + // consistent with other processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); }); // Default to simulating the plugin never having been published. @@ -108,9 +95,9 @@ void main() { test('allows valid version', () async { createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main']); @@ -121,39 +108,49 @@ void main() { contains('1.0.0 -> 2.0.0'), ]), ); - expect(gitDirCommands.length, equals(1)); expect( - gitDirCommands, - containsAll([ - equals(['show', 'main:packages/plugin/pubspec.yaml']), + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', ['main:packages/plugin/pubspec.yaml'], null) ])); }); test('denies invalid version', () async { createFakePlugin('plugin', packagesDir, version: '0.2.0'); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 0.0.1', - }; - final Future> result = runCapturingPrint( - runner, ['version-check', '--base-sha=main']); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 0.0.1'), + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main'], + errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), - ); - expect(gitDirCommands.length, equals(1)); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Incorrectly updated version.'), + ])); expect( - gitDirCommands, - containsAll([ - equals(['show', 'main:packages/plugin/pubspec.yaml']), + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', ['main:packages/plugin/pubspec.yaml'], null) ])); }); - test('allows valid version without explicit base-sha', () async { + test('uses merge-base without explicit base-sha', () async { createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'abc123:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-merge-base'] = [ + MockProcess(stdout: 'abc123'), + MockProcess(stdout: 'abc123'), + ]; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List output = await runCapturingPrint(runner, ['version-check']); @@ -164,6 +161,14 @@ void main() { contains('1.0.0 -> 2.0.0'), ]), ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall('git-merge-base', + ['--fork-point', 'FETCH_HEAD', 'HEAD'], null), + ProcessCall('git-show', + ['abc123:packages/plugin/pubspec.yaml'], null), + ])); }); test('allows valid version for new package.', () async { @@ -182,11 +187,11 @@ void main() { test('allows likely reverts.', () async { createFakePlugin('plugin', packagesDir, version: '0.6.1'); - gitShowResponses = { - 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', - }; - final List output = - await runCapturingPrint(runner, ['version-check']); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 0.6.2'), + ]; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main']); expect( output, @@ -195,43 +200,47 @@ void main() { 'This is assumed to be a revert.'), ]), ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', ['main:packages/plugin/pubspec.yaml'], null) + ])); }); test('denies lower version that could not be a simple revert', () async { createFakePlugin('plugin', packagesDir, version: '0.5.1'); - gitShowResponses = { - 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', - }; - final Future> result = - runCapturingPrint(runner, ['version-check']); - - await expectLater( - result, - throwsA(isA()), - ); - }); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 0.6.2'), + ]; - test('denies invalid version without explicit base-sha', () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0'); - gitShowResponses = { - 'abc123:packages/plugin/pubspec.yaml': 'version: 0.0.1', - }; - final Future> result = - runCapturingPrint(runner, ['version-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main'], + errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), - ); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Incorrectly updated version.'), + ])); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', ['main:packages/plugin/pubspec.yaml'], null) + ])); }); test('allows minor changes to platform interfaces', () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '1.1.0'); - gitShowResponses = { - 'main:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main']); expect( @@ -241,14 +250,15 @@ void main() { contains('1.0.0 -> 1.1.0'), ]), ); - expect(gitDirCommands.length, equals(1)); - expect( - gitDirCommands, - containsAll([ - equals([ - 'show', - 'main:packages/plugin_platform_interface/pubspec.yaml' - ]), + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) ])); }); @@ -256,24 +266,35 @@ void main() { () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'main:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - }; - final Future> output = runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - await expectLater( - output, - throwsA(isA()), - ); - expect(gitDirCommands.length, equals(1)); - expect( - gitDirCommands, - containsAll([ - equals([ - 'show', - 'main:packages/plugin_platform_interface/pubspec.yaml' - ]), + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + ' Breaking changes to platform interfaces are not allowed ' + 'without explicit justification.\n' + ' See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' + 'for more information.'), + ])); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) ])); }); @@ -281,10 +302,9 @@ void main() { () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'main:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final File changeDescriptionFile = fileSystem.file('change_description.txt'); changeDescriptionFile.writeAsStringSync(''' @@ -310,16 +330,25 @@ This is necessary because of X, Y, and Z contains('Ran for 1 package(s) (1 with warnings)'), ]), ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) + ])); }); test('throws if a nonexistent change description file is specified', () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'main:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -337,16 +366,25 @@ This is necessary because of X, Y, and Z contains('No such file: a_missing_file.txt'), ]), ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) + ])); }); test('allows breaking changes to platform interfaces with bypass flag', () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'main:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', @@ -361,6 +399,16 @@ This is necessary because of X, Y, and Z contains('Ran for 1 package(s) (1 with warnings)'), ]), ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) + ])); }); test('Allow empty lines in front of the first version in CHANGELOG', @@ -392,15 +440,14 @@ This is necessary because of X, Y, and Z * Some changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - bool hasError = false; + Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], errorHandler: (Error e) { - expect(e, isA()); - hasError = true; + commandError = e; }); - expect(hasError, isTrue); + expect(commandError, isA()); expect( output, containsAllInOrder([ @@ -472,13 +519,13 @@ This is necessary because of X, Y, and Z * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main']); - await expectLater( + expect( output, containsAllInOrder([ contains('Running for plugin'), @@ -534,9 +581,6 @@ This is necessary because of X, Y, and Z * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; bool hasError = false; final List output = await runCapturingPrint( @@ -569,9 +613,6 @@ This is necessary because of X, Y, and Z * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; bool hasError = false; final List output = await runCapturingPrint( @@ -604,9 +645,9 @@ This is necessary because of X, Y, and Z * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -637,9 +678,9 @@ This is necessary because of X, Y, and Z * Some changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -658,6 +699,360 @@ This is necessary because of X, Y, and Z ); }); + group('missing change detection', () { + Future> _runWithMissingChangeDetection( + List extraArgs, + {void Function(Error error)? errorHandler}) async { + return runCapturingPrint( + runner, + [ + 'version-check', + '--base-sha=main', + '--check-for-missing-changes', + ...extraArgs, + ], + errorHandler: errorHandler); + } + + test('passes for unchanged packages', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test( + 'fails if a version change is missing from a change that does not ' + 'pass the exemption check', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/lib/plugin.dart +'''), + ]; + + Error? commandError; + final List output = await _runWithMissingChangeDetection( + [], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No version change found'), + contains('plugin:\n' + ' Missing version change'), + ]), + ); + }); + + test('passes version change requirement when version changes', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.1'); + + const String changelog = ''' +## 1.0.1 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/lib/plugin.dart +packages/plugin/CHANGELOG.md +packages/plugin/pubspec.yaml +'''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test('version change check ignores files outside the package', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin_a/lib/plugin.dart +tool/plugin/lib/plugin.dart +'''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test('allows missing version change for exempt changes', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/android/lint-baseline.xml +packages/plugin/example/android/src/androidTest/foo/bar/FooTest.java +packages/plugin/example/ios/RunnerTests/Foo.m +packages/plugin/example/ios/RunnerUITests/info.plist +packages/plugin/CHANGELOG.md +'''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test('allows missing version change with justification', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/lib/plugin.dart +packages/plugin/CHANGELOG.md +packages/plugin/pubspec.yaml +'''), + ]; + + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile.writeAsStringSync(''' +Some general PR description + +No version change: Code change is only to implementation comments. +'''); + final List output = + await _runWithMissingChangeDetection([ + '--change-description-file=${changeDescriptionFile.path}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Ignoring lack of version change due to ' + '"No version change:" in the change description.'), + ]), + ); + }); + + test('fails if a CHANGELOG change is missing', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/lib/foo.dart +'''), + ]; + + Error? commandError; + final List output = await _runWithMissingChangeDetection( + [], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No CHANGELOG change found'), + contains('plugin:\n' + ' Missing CHANGELOG change'), + ]), + ); + }); + + test('passes CHANGELOG check when the CHANGELOG is changed', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/lib/foo.dart +packages/plugin/CHANGELOG.md +'''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test('fails CHANGELOG check if only another package CHANGELOG chages', + () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/lib/foo.dart +packages/another_plugin/CHANGELOG.md +'''), + ]; + + Error? commandError; + final List output = await _runWithMissingChangeDetection( + [], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No CHANGELOG change found'), + ]), + ); + }); + + test('allows missing CHANGELOG change with justification', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/lib/foo.dart +'''), + ]; + + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile.writeAsStringSync(''' +Some general PR description + +No CHANGELOG change: Code change is only to implementation comments. +'''); + final List output = + await _runWithMissingChangeDetection([ + '--change-description-file=${changeDescriptionFile.path}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Ignoring lack of CHANGELOG update due to ' + '"No CHANGELOG change:" in the change description.'), + ]), + ); + }); + }); + test('allows valid against pub', () async { mockHttpResponse = { 'name': 'some_package', @@ -669,9 +1064,6 @@ This is necessary because of X, Y, and Z }; createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; final List output = await runCapturingPrint(runner, ['version-check', '--base-sha=main', '--against-pub']); @@ -693,9 +1085,6 @@ This is necessary because of X, Y, and Z }; createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; bool hasError = false; final List result = await runCapturingPrint( @@ -723,9 +1112,6 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N mockHttpStatus = 400; createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; bool hasError = false; final List result = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -752,9 +1138,9 @@ ${indentation}HTTP response: null mockHttpStatus = 404; createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List result = await runCapturingPrint(runner, ['version-check', '--base-sha=main', '--against-pub']); From b25b31427aad6802a96b315b3e6dc6dc44992e04 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 23 Nov 2021 21:52:01 -0500 Subject: [PATCH 153/249] [flutter_plugin_tools] Check for FlutterTestRunner in FTL tests (#4531) When running via Firebase Test Lab, ensure that there is a test using `FlutterTestRunner`. This ensures that a plugin can't silently not run any of the Dart integration test on Android by having some other native integration test. Fixes https://github.com/flutter/flutter/issues/93952 --- script/tool/CHANGELOG.md | 4 + .../lib/src/firebase_test_lab_command.dart | 29 ++- .../test/firebase_test_lab_command_test.dart | 175 +++++++++++++----- 3 files changed, 156 insertions(+), 52 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index ee23428e66c..a2a2ed82429 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +- Ensures that `firebase-test-lab` runs include an `integration_test` runner. + ## 0.7.3 - `native-test` now builds unit tests before running them on Windows and Linux, diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 28afc638203..4e53ee8fbac 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -127,16 +127,24 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { '${example.displayName} does not support Android.'); } - if (!androidDirectory + final Directory uiTestDirectory = androidDirectory .childDirectory('app') .childDirectory('src') - .childDirectory('androidTest') - .existsSync()) { + .childDirectory('androidTest'); + if (!uiTestDirectory.existsSync()) { printError('No androidTest directory found.'); return PackageResult.fail( ['No tests ran (use --exclude if this is intentional).']); } + // Ensure that the Dart integration tests will be run, not just native UI + // tests. + if (!await _testsContainDartIntegrationTestRunner(uiTestDirectory)) { + printError('No integration_test runner found. ' + 'See the integration_test package README for setup instructions.'); + return PackageResult.fail(['No integration_test runner.']); + } + // Ensures that gradle wrapper exists final GradleProject project = GradleProject(example.directory, processRunner: processRunner, platform: platform); @@ -280,4 +288,19 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { file is File && file.basename.endsWith('_test.dart')) .cast(); } + + /// Returns true if any of the test files in [uiTestDirectory] contain the + /// annotation that means that the test will reports the results of running + /// the Dart integration tests. + Future _testsContainDartIntegrationTestRunner( + Directory uiTestDirectory) async { + return uiTestDirectory + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .cast() + .any((File file) { + return file.basename.endsWith('.java') && + file.readAsStringSync().contains('@RunWith(FlutterTestRunner.class)'); + }); + } } diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 268210d0042..65f398b32ca 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -8,7 +8,9 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'mocks.dart'; @@ -38,15 +40,33 @@ void main() { runner.addCommand(command); }); + void _writeJavaTestFile(Directory pluginDir, String relativeFilePath, + {String runnerClass = 'FlutterTestRunner'}) { + childFileWithSubcomponents(pluginDir, p.posix.split(relativeFilePath)) + .writeAsStringSync(''' +@DartIntegrationTest +@RunWith($runnerClass.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} +'''); + } + test('fails if gcloud auth fails', () async { processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(exitCode: 1) ]; - createFakePlugin('plugin', packagesDir, extraFiles: [ + + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -67,11 +87,16 @@ void main() { MockProcess(), // auth MockProcess(exitCode: 1), // config ]; - createFakePlugin('plugin', packagesDir, extraFiles: [ + + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, ['firebase-test-lab']); @@ -85,18 +110,24 @@ void main() { }); test('only runs gcloud configuration once', () async { - createFakePlugin('plugin1', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory plugin1Dir = + createFakePlugin('plugin1', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); - createFakePlugin('plugin2', packagesDir, extraFiles: [ + _writeJavaTestFile(plugin1Dir, javaTestFileRelativePath); + final Directory plugin2Dir = + createFakePlugin('plugin2', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/bar_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin2Dir, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -164,14 +195,18 @@ void main() { }); test('runs integration tests', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/integration_test/should_not_run.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -237,12 +272,16 @@ void main() { }); test('fails if a test fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(), // auth @@ -258,12 +297,6 @@ void main() { 'firebase-test-lab', '--device', 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', ], errorHandler: (Error e) { commandError = e; @@ -295,12 +328,6 @@ void main() { 'firebase-test-lab', '--device', 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', ], errorHandler: (Error e) { commandError = e; @@ -321,10 +348,14 @@ void main() { }); test('fails for packages with no integration test files', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -333,12 +364,6 @@ void main() { 'firebase-test-lab', '--device', 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', ], errorHandler: (Error e) { commandError = e; @@ -358,6 +383,48 @@ void main() { ); }); + test('fails for packages with no integration_test runner', () async { + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/integration_test/should_not_run.dart', + 'example/android/gradlew', + javaTestFileRelativePath, + ]); + // Use the wrong @RunWith annotation. + _writeJavaTestFile(pluginDir, javaTestFileRelativePath, + runnerClass: 'AndroidJUnit4.class'); + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=redfin,version=30', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No integration_test runner found. ' + 'See the integration_test package README for setup instructions.'), + contains('plugin:\n' + ' No integration_test runner.'), + ]), + ); + }); + test('skips packages with no android directory', () async { createFakePackage('package', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -367,12 +434,6 @@ void main() { 'firebase-test-lab', '--device', 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', ]); expect( @@ -392,17 +453,19 @@ void main() { }); test('builds if gradlew is missing', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', '--test-run-id', 'testRunId', '--build-id', @@ -445,7 +508,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), @@ -453,10 +516,14 @@ void main() { }); test('fails if building to generate gradlew fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['flutter'] = [ MockProcess(exitCode: 1) // flutter build @@ -484,11 +551,14 @@ void main() { }); test('fails if assembleAndroidTest fails', () async { + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; final Directory pluginDir = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); final String gradlewPath = pluginDir .childDirectory('example') @@ -521,11 +591,14 @@ void main() { }); test('fails if assembleDebug fails', () async { + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; final Directory pluginDir = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); final String gradlewPath = pluginDir .childDirectory('example') @@ -562,11 +635,15 @@ void main() { }); test('experimental flag', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); await runCapturingPrint(runner, [ 'firebase-test-lab', From 5dc663274a4a25a539fd8bbd59dbb11da1080036 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sat, 4 Dec 2021 12:43:13 -0500 Subject: [PATCH 154/249] [flutter_plugin_tools] Add a new 'make-deps-path-based' command (#4575) Adds a new command that adds `dependency_overrides` to any packages in the repository that depend on a list of target packages, including an option to target packages that will publish a non-breaking change in a given diff. Adds a new CI step that uses the above in conjunction with a new `--run-on-dirty-packages` to adjust the dependencies of anything in the repository that uses a to-be-published package and then re-run analysis on just those packages. This will allow us to catch in presubmit any changes that are not breaking from a semver standpoint, but will break us due to our strict analysis in CI. Fixes https://github.com/flutter/flutter/issues/89862 --- script/tool/CHANGELOG.md | 4 + .../lib/src/common/git_version_finder.dart | 10 +- .../tool/lib/src/common/plugin_command.dart | 36 ++- .../lib/src/common/repository_package.dart | 9 + .../src/create_all_plugins_app_command.dart | 3 +- script/tool/lib/src/main.dart | 2 + .../lib/src/make_deps_path_based_command.dart | 249 ++++++++++++++ .../tool/lib/src/publish_check_command.dart | 7 +- .../tool/lib/src/publish_plugin_command.dart | 7 +- .../tool/lib/src/version_check_command.dart | 4 +- .../test/common/git_version_finder_test.dart | 12 + .../tool/test/common/plugin_command_test.dart | 98 ++++++ .../test/common/repository_package_test.dart | 20 ++ .../make_deps_path_based_command_test.dart | 304 ++++++++++++++++++ .../test/publish_plugin_command_test.dart | 2 +- 15 files changed, 744 insertions(+), 23 deletions(-) create mode 100644 script/tool/lib/src/make_deps_path_based_command.dart create mode 100644 script/tool/test/make_deps_path_based_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a2a2ed82429..234700ab5a7 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,10 @@ ## NEXT - Ensures that `firebase-test-lab` runs include an `integration_test` runner. +- Adds a `make-deps-path-based` command to convert inter-repo package + dependencies to path-based dependencies. +- Adds a (hidden) `--run-on-dirty-packages` flag for use with + `make-deps-path-based` in CI. ## 0.7.3 diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index 331187335f5..32d30e60feb 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -31,10 +31,16 @@ class GitVersionFinder { } /// Get a list of all the changed files. - Future> getChangedFiles() async { + Future> getChangedFiles( + {bool includeUncommitted = false}) async { final String baseSha = await getBaseSha(); final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', baseSha, 'HEAD']); + .runCommand([ + 'diff', + '--name-only', + baseSha, + if (!includeUncommitted) 'HEAD' + ]); final String changedFilesStdout = changedFilesCommand.stdout.toString(); if (changedFilesStdout.isEmpty) { return []; diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index f40a102dfbc..7166c754e12 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -75,9 +75,15 @@ abstract class PluginCommand extends Command { help: 'Run the command on changed packages/plugins.\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' + 'Packages excluded with $_excludeArg are excluded even if changed.\n' 'See $_baseShaArg if a custom base is needed to determine the diff.\n\n' 'Cannot be combined with $_packagesArg.\n'); + argParser.addFlag(_runOnDirtyPackagesArg, + help: + 'Run the command on packages with changes that have not been committed.\n' + 'Packages excluded with $_excludeArg are excluded even if changed.\n' + 'Cannot be combined with $_packagesArg.\n', + hide: true); argParser.addFlag(_packagesForBranchArg, help: 'This runs on all packages (equivalent to no package selection flag)\n' @@ -103,6 +109,7 @@ abstract class PluginCommand extends Command { static const String _packagesForBranchArg = 'packages-for-branch'; static const String _pluginsArg = 'plugins'; static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _runOnDirtyPackagesArg = 'run-on-dirty-packages'; static const String _shardCountArg = 'shardCount'; static const String _shardIndexArg = 'shardIndex'; @@ -289,6 +296,7 @@ abstract class PluginCommand extends Command { final Set packageSelectionFlags = { _packagesArg, _runOnChangedPackagesArg, + _runOnDirtyPackagesArg, _packagesForBranchArg, }; if (packageSelectionFlags @@ -300,7 +308,7 @@ abstract class PluginCommand extends Command { throw ToolExit(exitInvalidArguments); } - Set plugins = Set.from(getStringListArg(_packagesArg)); + Set packages = Set.from(getStringListArg(_packagesArg)); final bool runOnChangedPackages; if (getBoolArg(_runOnChangedPackagesArg)) { @@ -331,7 +339,21 @@ abstract class PluginCommand extends Command { final List changedFiles = await gitVersionFinder.getChangedFiles(); if (!_changesRequireFullTest(changedFiles)) { - plugins = _getChangedPackages(changedFiles); + packages = _getChangedPackages(changedFiles); + } + } else if (getBoolArg(_runOnDirtyPackagesArg)) { + final GitVersionFinder gitVersionFinder = + GitVersionFinder(await gitDir, 'HEAD'); + print('Running for all packages that have uncommitted changes\n'); + // _changesRequireFullTest is deliberately not used here, as this flag is + // intended for use in CI to re-test packages changed by + // 'make-deps-path-based'. + packages = _getChangedPackages( + await gitVersionFinder.getChangedFiles(includeUncommitted: true)); + // For the same reason, empty is not treated as "all packages" as it is + // for other flags. + if (packages.isEmpty) { + return; } } @@ -347,7 +369,7 @@ abstract class PluginCommand extends Command { in dir.list(followLinks: false)) { // A top-level Dart package is a plugin package. if (_isDartPackage(entity)) { - if (plugins.isEmpty || plugins.contains(p.basename(entity.path))) { + if (packages.isEmpty || packages.contains(p.basename(entity.path))) { yield PackageEnumerationEntry( RepositoryPackage(entity as Directory), excluded: excludedPluginNames.contains(entity.basename)); @@ -364,9 +386,9 @@ abstract class PluginCommand extends Command { path.relative(subdir.path, from: dir.path); final String packageName = path.basename(subdir.path); final String basenamePath = path.basename(entity.path); - if (plugins.isEmpty || - plugins.contains(relativePath) || - plugins.contains(basenamePath)) { + if (packages.isEmpty || + packages.contains(relativePath) || + packages.contains(basenamePath)) { yield PackageEnumerationEntry( RepositoryPackage(subdir as Directory), excluded: excludedPluginNames.contains(basenamePath) || diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index 3b4417ac818..e0c4e4a83bf 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -4,6 +4,7 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; +import 'package:pubspec_parse/pubspec_parse.dart'; import 'core.dart'; @@ -47,6 +48,14 @@ class RepositoryPackage { /// The package's top-level pubspec.yaml. File get pubspecFile => directory.childFile('pubspec.yaml'); + late final Pubspec _parsedPubspec = + Pubspec.parse(pubspecFile.readAsStringSync()); + + /// Returns the parsed [pubspecFile]. + /// + /// Caches for future use. + Pubspec parsePubspec() => _parsedPubspec; + /// True if this appears to be a federated plugin package, according to /// repository conventions. bool get isFederated => 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 5d9b4ed9c72..82f29bd501f 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -178,8 +178,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { final RepositoryPackage package = entry.package; final Directory pluginDirectory = package.directory; final String pluginName = pluginDirectory.basename; - final File pubspecFile = package.pubspecFile; - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + final Pubspec pubspec = package.parsePubspec(); if (pubspec.publishTo != 'none') { pathDependencies[pluginName] = PathDependency(pluginDirectory.path); diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 70a6ab51603..3e8f19b044d 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -20,6 +20,7 @@ import 'license_check_command.dart'; import 'lint_android_command.dart'; import 'lint_podspecs_command.dart'; import 'list_command.dart'; +import 'make_deps_path_based_command.dart'; import 'native_test_command.dart'; import 'publish_check_command.dart'; import 'publish_plugin_command.dart'; @@ -58,6 +59,7 @@ void main(List args) { ..addCommand(LintPodspecsCommand(packagesDir)) ..addCommand(ListCommand(packagesDir)) ..addCommand(NativeTestCommand(packagesDir)) + ..addCommand(MakeDepsPathBasedCommand(packagesDir)) ..addCommand(PublishCheckCommand(packagesDir)) ..addCommand(PublishPluginCommand(packagesDir)) ..addCommand(PubspecCheckCommand(packagesDir)) diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart new file mode 100644 index 00000000000..04869639cf7 --- /dev/null +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -0,0 +1,249 @@ +// 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:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common/core.dart'; +import 'common/git_version_finder.dart'; +import 'common/plugin_command.dart'; + +const int _exitPackageNotFound = 3; +const int _exitCannotUpdatePubspec = 4; + +/// Converts all dependencies on target packages to path-based dependencies. +/// +/// This is to allow for pre-publish testing of changes that could affect other +/// packages in the repository. For instance, this allows for catching cases +/// where a non-breaking change to a platform interface package of a federated +/// plugin would cause post-publish analyzer failures in another package of that +/// plugin. +class MakeDepsPathBasedCommand extends PluginCommand { + /// Creates an instance of the command to convert selected dependencies to + /// path-based. + MakeDepsPathBasedCommand( + Directory packagesDir, { + GitDir? gitDir, + }) : super(packagesDir, gitDir: gitDir) { + argParser.addMultiOption(_targetDependenciesArg, + help: + 'The names of the packages to convert to path-based dependencies.\n' + 'Ignored if --$_targetDependenciesWithNonBreakingUpdatesArg is ' + 'passed.', + valueHelp: 'some_package'); + argParser.addFlag( + _targetDependenciesWithNonBreakingUpdatesArg, + help: 'Causes all packages that have non-breaking version changes ' + 'when compared against the git base to be treated as target ' + 'packages.', + ); + } + + static const String _targetDependenciesArg = 'target-dependencies'; + static const String _targetDependenciesWithNonBreakingUpdatesArg = + 'target-dependencies-with-non-breaking-updates'; + + @override + final String name = 'make-deps-path-based'; + + @override + final String description = + 'Converts package dependencies to path-based references.'; + + @override + Future run() async { + final Set targetDependencies = + getBoolArg(_targetDependenciesWithNonBreakingUpdatesArg) + ? await _getNonBreakingUpdatePackages() + : getStringListArg(_targetDependenciesArg).toSet(); + + if (targetDependencies.isEmpty) { + print('No target dependencies; nothing to do.'); + return; + } + print('Rewriting references to: ${targetDependencies.join(', ')}...'); + + final Map localDependencyPackages = + _findLocalPackages(targetDependencies); + + final String repoRootPath = (await gitDir).path; + for (final File pubspec in await _getAllPubspecs()) { + if (await _addDependencyOverridesIfNecessary( + pubspec, localDependencyPackages)) { + // Print the relative path of the changed pubspec. + final String displayPath = p.posix.joinAll(path + .split(path.relative(pubspec.absolute.path, from: repoRootPath))); + print(' Modified $displayPath'); + } + } + } + + Map _findLocalPackages(Set packageNames) { + final Map targets = + {}; + for (final String packageName in packageNames) { + final Directory topLevelCandidate = + packagesDir.childDirectory(packageName); + // If packages// exists, then either that directory is the + // package, or packages/// exists and is the + // package (in the case of a federated plugin). + if (topLevelCandidate.existsSync()) { + final Directory appFacingCandidate = + topLevelCandidate.childDirectory(packageName); + targets[packageName] = RepositoryPackage(appFacingCandidate.existsSync() + ? appFacingCandidate + : topLevelCandidate); + continue; + } + // If there is no packages/ directory, then either the + // packages doesn't exist, or it is a sub-package of a federated plugin. + // If it's the latter, it will be a directory whose name is a prefix. + for (final FileSystemEntity entity in packagesDir.listSync()) { + if (entity is Directory && packageName.startsWith(entity.basename)) { + final Directory subPackageCandidate = + entity.childDirectory(packageName); + if (subPackageCandidate.existsSync()) { + targets[packageName] = RepositoryPackage(subPackageCandidate); + break; + } + } + } + + if (!targets.containsKey(packageName)) { + printError('Unable to find package "$packageName"'); + throw ToolExit(_exitPackageNotFound); + } + } + return targets; + } + + /// If [pubspecFile] has any dependencies on packages in [localDependencies], + /// adds dependency_overrides entries to redirect them to the local version + /// using path-based dependencies. + /// + /// Returns true if any changes were made. + Future _addDependencyOverridesIfNecessary(File pubspecFile, + Map localDependencies) async { + final String pubspecContents = pubspecFile.readAsStringSync(); + final Pubspec pubspec = Pubspec.parse(pubspecContents); + // Fail if there are any dependency overrides already. If support for that + // is needed at some point, it can be added, but currently it's not and + // relying on that makes the logic here much simpler. + if (pubspec.dependencyOverrides.isNotEmpty) { + printError( + 'Plugins with dependency overrides are not currently supported.'); + throw ToolExit(_exitCannotUpdatePubspec); + } + + final Iterable packagesToOverride = pubspec.dependencies.keys.where( + (String packageName) => localDependencies.containsKey(packageName)); + if (packagesToOverride.isNotEmpty) { + final String commonBasePath = packagesDir.path; + // Find the relative path to the common base. + final int packageDepth = path + .split(path.relative(pubspecFile.parent.absolute.path, + from: commonBasePath)) + .length; + final List relativeBasePathComponents = + List.filled(packageDepth, '..'); + // This is done via strings rather than by manipulating the Pubspec and + // then re-serialiazing so that it's a localized change, rather than + // rewriting the whole file (e.g., destroying comments), which could be + // more disruptive for local use. + String newPubspecContents = pubspecContents + + ''' + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: +'''; + for (final String packageName in packagesToOverride) { + // Find the relative path from the common base to the local package. + final List repoRelativePathComponents = path.split( + path.relative(localDependencies[packageName]!.directory.path, + from: commonBasePath)); + newPubspecContents += ''' + $packageName: + path: ${p.posix.joinAll([ + ...relativeBasePathComponents, + ...repoRelativePathComponents, + ])} +'''; + } + pubspecFile.writeAsStringSync(newPubspecContents); + return true; + } + return false; + } + + /// Returns all pubspecs anywhere under the packages directory. + Future> _getAllPubspecs() => packagesDir.parent + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => + entity is File && p.basename(entity.path) == 'pubspec.yaml') + .map((FileSystemEntity file) => file as File) + .toList(); + + /// Returns all packages that have non-breaking published changes (i.e., a + /// minor or bugfix version change) relative to the git comparison base. + /// + /// Prints status information about what was checked for ease of auditing logs + /// in CI. + Future> _getNonBreakingUpdatePackages() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print('Finding changed packages relative to "$baseSha"\n'); + + final Set changedPackages = {}; + for (final String path in await gitVersionFinder.getChangedFiles()) { + // Git output always uses Posix paths. + final List allComponents = p.posix.split(path); + // Only pubspec changes are potential publishing events. + if (allComponents.last != 'pubspec.yaml' || + allComponents.contains('example')) { + continue; + } + final RepositoryPackage package = + RepositoryPackage(packagesDir.fileSystem.file(path).parent); + final String packageName = package.parsePubspec().name; + if (!await _hasNonBreakingVersionChange(package)) { + // Log packages that had pubspec changes but weren't included for ease + // of auditing CI. + print(' Skipping $packageName; no non-breaking version change.'); + continue; + } + changedPackages.add(packageName); + } + return changedPackages; + } + + Future _hasNonBreakingVersionChange(RepositoryPackage package) async { + final Pubspec pubspec = package.parsePubspec(); + if (pubspec.publishTo == 'none') { + return false; + } + + final String pubspecGitPath = p.posix.joinAll(path.split(path.relative( + package.pubspecFile.absolute.path, + from: (await gitDir).path))); + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final Version? previousVersion = + await gitVersionFinder.getPackageVersion(pubspecGitPath); + if (previousVersion == null) { + // The plugin is new, so nothing can be depending on it yet. + return false; + } + final Version newVersion = pubspec.version!; + if ((newVersion.major > 0 && newVersion.major != previousVersion.major) || + (newVersion.major == 0 && newVersion.minor != previousVersion.minor)) { + // Breaking changes aren't targetted since they won't be picked up + // automatically. + return false; + } + return newVersion != previousVersion; + } +} diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 563e0904552..8fd96b818c1 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -123,13 +123,12 @@ class PublishCheckCommand extends PackageLoopingCommand { } Pubspec? _tryParsePubspec(RepositoryPackage package) { - final File pubspecFile = package.pubspecFile; - try { - return Pubspec.parse(pubspecFile.readAsStringSync()); + return package.parsePubspec(); } on Exception catch (exception) { print( - 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}', + 'Failed to parse `pubspec.yaml` at ${package.pubspecFile.path}: ' + '$exception', ); return null; } diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 4fdecf603ee..28d17a3a248 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -217,16 +217,15 @@ class PublishPluginCommand extends PackageLoopingCommand { /// In cases where a non-null result is returned, that should be returned /// as the final result for the package, without further processing. Future _checkNeedsRelease(RepositoryPackage package) async { - final File pubspecFile = package.pubspecFile; - if (!pubspecFile.existsSync()) { + if (!package.pubspecFile.existsSync()) { logWarning(''' -The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. +The pubspec file for ${package.displayName} does not exist, so no publishing will happen. Safe to ignore if the package is deleted in this commit. '''); return PackageResult.skip('package deleted'); } - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + final Pubspec pubspec = package.parsePubspec(); if (pubspec.name == 'flutter_plugin_tools') { // Ignore flutter_plugin_tools package when running publishing through flutter_plugin_tools. diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 1ec5dc4f249..fcaea335920 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -463,10 +463,8 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. } Pubspec? _tryParsePubspec(RepositoryPackage package) { - final File pubspecFile = package.pubspecFile; - try { - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + final Pubspec pubspec = package.parsePubspec(); return pubspec; } on Exception catch (exception) { printError('${indentation}Failed to parse `pubspec.yaml`: $exception}'); diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart index fa8b1c410dd..ad1a26ffc16 100644 --- a/script/tool/test/common/git_version_finder_test.dart +++ b/script/tool/test/common/git_version_finder_test.dart @@ -88,6 +88,18 @@ file2/file2.cc verify(gitDir .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); }); + + test('include uncommitted files if requested', () async { + const String customBaseSha = 'aklsjdcaskf12312'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); + await finder.getChangedFiles(includeUncommitted: true); + // The call should not have HEAD as a final argument like the default diff. + verify(gitDir.runCommand(['diff', '--name-only', customBaseSha])); + }); } class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 6d586e416b7..222df544f34 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -486,6 +486,104 @@ packages/plugin3/plugin3.dart expect(command.plugins, unorderedEquals([plugin1.path])); }); }); + + group('test run-on-dirty-packages', () { + test('no packages should be tested if there are no changes.', () async { + createFakePackage('a_package', packagesDir); + await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect(command.plugins, unorderedEquals([])); + }); + + test( + 'no packages should be tested if there are no plugin related changes.', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'AUTHORS'), + ]; + createFakePackage('a_package', packagesDir); + await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect(command.plugins, unorderedEquals([])); + }); + + test('no packages should be tested even if special repo files change.', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +.cirrus.yml +.ci.yaml +.ci/Dockerfile +.clang-format +analysis_options.yaml +script/tool_runner.sh +'''), + ]; + createFakePackage('a_package', packagesDir); + await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect(command.plugins, unorderedEquals([])); + }); + + test('Only changed packages should be tested.', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/a_package/lib/a_package.dart'), + ]; + final Directory packageA = createFakePackage('a_package', packagesDir); + createFakePlugin('b_package', packagesDir); + final List output = await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect( + output, + containsAllInOrder([ + contains( + 'Running for all packages that have uncommitted changes'), + ])); + + expect(command.plugins, unorderedEquals([packageA.path])); + }); + + test('multiple packages changed should test all the changed packages', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/a_package/lib/a_package.dart +packages/b_package/lib/src/foo.dart +'''), + ]; + final Directory packageA = createFakePackage('a_package', packagesDir); + final Directory packageB = createFakePackage('b_package', packagesDir); + createFakePackage('c_package', packagesDir); + await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect(command.plugins, + unorderedEquals([packageA.path, packageB.path])); + }); + + test('honors --exclude flag', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/a_package/lib/a_package.dart +packages/b_package/lib/src/foo.dart +'''), + ]; + final Directory packageA = createFakePackage('a_package', packagesDir); + createFakePackage('b_package', packagesDir); + createFakePackage('c_package', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--exclude=b_package', + '--run-on-dirty-packages' + ]); + + expect(command.plugins, unorderedEquals([packageA.path])); + }); + }); }); group('--packages-for-branch', () { diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index 4c20389ae4b..29e3b583212 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -5,6 +5,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -155,4 +156,23 @@ void main() { expect(RepositoryPackage(plugin).isPlatformImplementation, true); }); }); + + group('pubspec', () { + test('file', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + + final File pubspecFile = RepositoryPackage(plugin).pubspecFile; + + expect(pubspecFile.path, plugin.childFile('pubspec.yaml').path); + }); + + test('parsing', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + + final Pubspec pubspec = RepositoryPackage(plugin).parsePubspec(); + + expect(pubspec.name, 'a_plugin'); + }); + }); } diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart new file mode 100644 index 00000000000..29e4f724b33 --- /dev/null +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -0,0 +1,304 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:flutter_plugin_tools/src/make_deps_path_based_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through the process runner, to make mock output + // consistent with other processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); + }); + + processRunner = RecordingProcessRunner(); + final MakeDepsPathBasedCommand command = + MakeDepsPathBasedCommand(packagesDir, gitDir: gitDir); + + runner = CommandRunner( + 'make-deps-path-based_command', 'Test for $MakeDepsPathBasedCommand'); + runner.addCommand(command); + }); + + /// Adds dummy 'dependencies:' entries for each package in [dependencies] + /// to [package]. + void _addDependencies( + RepositoryPackage package, Iterable dependencies) { + final List lines = package.pubspecFile.readAsLinesSync(); + final int dependenciesStartIndex = lines.indexOf('dependencies:'); + assert(dependenciesStartIndex != -1); + lines.insertAll(dependenciesStartIndex + 1, [ + for (final String dependency in dependencies) ' $dependency: ^1.0.0', + ]); + package.pubspecFile.writeAsStringSync(lines.join('\n')); + } + + test('no-ops for no plugins', () async { + RepositoryPackage(createFakePackage('foo', packagesDir, isFlutter: true)); + final RepositoryPackage packageBar = RepositoryPackage( + createFakePackage('bar', packagesDir, isFlutter: true)); + _addDependencies(packageBar, ['foo']); + final String originalPubspecContents = + packageBar.pubspecFile.readAsStringSync(); + + final List output = + await runCapturingPrint(runner, ['make-deps-path-based']); + + expect( + output, + containsAllInOrder([ + contains('No target dependencies'), + ]), + ); + // The 'foo' reference should not have been modified. + expect(packageBar.pubspecFile.readAsStringSync(), originalPubspecContents); + }); + + test('rewrites references', () async { + final RepositoryPackage simplePackage = RepositoryPackage( + createFakePackage('foo', packagesDir, isFlutter: true)); + final Directory pluginGroup = packagesDir.childDirectory('bar'); + + RepositoryPackage(createFakePackage('bar_platform_interface', pluginGroup, + isFlutter: true)); + final RepositoryPackage pluginImplementation = + RepositoryPackage(createFakePlugin('bar_android', pluginGroup)); + final RepositoryPackage pluginAppFacing = + RepositoryPackage(createFakePlugin('bar', pluginGroup)); + + _addDependencies(simplePackage, [ + 'bar', + 'bar_android', + 'bar_platform_interface', + ]); + _addDependencies(pluginAppFacing, [ + 'bar_platform_interface', + 'bar_android', + ]); + _addDependencies(pluginImplementation, [ + 'bar_platform_interface', + ]); + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies=bar,bar_platform_interface' + ]); + + expect( + output, + containsAll([ + 'Rewriting references to: bar, bar_platform_interface...', + ' Modified packages/bar/bar/pubspec.yaml', + ' Modified packages/bar/bar_android/pubspec.yaml', + ' Modified packages/foo/pubspec.yaml', + ])); + expect( + output, + isNot(contains( + ' Modified packages/bar/bar_platform_interface/pubspec.yaml'))); + + expect( + simplePackage.pubspecFile.readAsLinesSync(), + containsAllInOrder([ + '# FOR TESTING ONLY. DO NOT MERGE.', + 'dependency_overrides:', + ' bar:', + ' path: ../bar/bar', + ' bar_platform_interface:', + ' path: ../bar/bar_platform_interface', + ])); + expect( + pluginAppFacing.pubspecFile.readAsLinesSync(), + containsAllInOrder([ + 'dependency_overrides:', + ' bar_platform_interface:', + ' path: ../../bar/bar_platform_interface', + ])); + }); + + group('target-dependencies-with-non-breaking-updates', () { + test('no-ops for no published changes', () async { + final Directory package = createFakePackage('foo', packagesDir); + + final String changedFileOutput = [ + package.childFile('pubspec.yaml'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess( + stdout: RepositoryPackage(package).pubspecFile.readAsStringSync()), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('No target dependencies'), + ]), + ); + }); + + test('includes bugfix version changes as targets', () async { + const String newVersion = '1.0.1'; + final Directory package = + createFakePackage('foo', packagesDir, version: newVersion); + + final File pubspecFile = RepositoryPackage(package).pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('Rewriting references to: foo...'), + ]), + ); + }); + + test('includes minor version changes to 1.0+ as targets', () async { + const String newVersion = '1.1.0'; + final Directory package = + createFakePackage('foo', packagesDir, version: newVersion); + + final File pubspecFile = RepositoryPackage(package).pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('Rewriting references to: foo...'), + ]), + ); + }); + + test('does not include major version changes as targets', () async { + const String newVersion = '2.0.0'; + final Directory package = + createFakePackage('foo', packagesDir, version: newVersion); + + final File pubspecFile = RepositoryPackage(package).pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('No target dependencies'), + ]), + ); + }); + + test('does not include minor version changes to 0.x as targets', () async { + const String newVersion = '0.8.0'; + final Directory package = + createFakePackage('foo', packagesDir, version: newVersion); + + final File pubspecFile = RepositoryPackage(package).pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '0.7.0'); + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('No target dependencies'), + ]), + ); + }); + }); +} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 14e99a10f36..2cb3fc25af2 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -675,7 +675,7 @@ void main() { contains('Running `pub publish ` in ${pluginDir1.path}...'), contains('Published plugin1 successfully!'), contains( - 'The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n'), + 'The pubspec file for plugin2/plugin2 does not exist, so no publishing will happen.\nSafe to ignore if the package is deleted in this commit.\n'), contains('SKIPPING: package deleted'), contains('skipped (with warning)'), ])); From 52f49c1aa2b783e612d82b8b070340b184fdd752 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 6 Dec 2021 13:23:14 -0500 Subject: [PATCH 155/249] Remove deprecated plugins (#4580) This removes all of the deprecated plugins from the tree, as well as all references to them. They were being left in the tree only in case they needed critical fixes before the end of this year, when they will become completely unsupported and be officially discontinued on pub.dev. The end of the year is now close enough that such a release is extremely unlikely, and they are causing some maintenance burden (e.g., the tree is currently closed on an out-of-band failure in android_alarm_manager). In the event that we do have to push an emergency fix in the next several weeks, we can either temporarily restore the plugin from git history, or use a branch. Minor related cleanup: - Scanning the repository for remaining references turned up that `image_picker_for_web` and `video_player_for_web` had copypasta'd the package name of its example application from one of the deprecated plugins, so this fixes those names. - Fixes `federation-safety-check` handling of top-level files in unfederated plugins that accidentally tripped federated plugin heuristics. - Fixes `federation-safety-check` handling of deleted plugins, as it was crashing, and tests that. - Fixes `make-deps-path-based` handling of deleted plugins, as it was crashing, and tests that. --- script/tool/CHANGELOG.md | 3 + .../src/federation_safety_check_command.dart | 7 ++- .../lib/src/make_deps_path_based_command.dart | 16 +++-- .../federation_safety_check_command_test.dart | 60 +++++++++++++++++++ .../make_deps_path_based_command_test.dart | 23 +++++++ 5 files changed, 104 insertions(+), 5 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 234700ab5a7..dec218e2ba2 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,9 @@ dependencies to path-based dependencies. - Adds a (hidden) `--run-on-dirty-packages` flag for use with `make-deps-path-based` in CI. +- Fix `federation-safety-check` handling of plugin deletion, and of top-level + files in unfederated plugins whose names match federated plugin heuristics + (e.g., `packages/foo/foo_android.iml`). ## 0.7.3 diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index 200f9c3f48c..df9d86892e1 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -81,7 +81,8 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { // Count the top-level plugin as changed. _changedPlugins.add(packageName); if (relativeComponents[0] == packageName || - relativeComponents[0].startsWith('${packageName}_')) { + (relativeComponents.length > 1 && + relativeComponents[0].startsWith('${packageName}_'))) { packageName = relativeComponents.removeAt(0); } @@ -178,6 +179,10 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { String pubspecRepoRelativePosixPath) async { final File pubspecFile = childFileWithSubcomponents( packagesDir.parent, p.posix.split(pubspecRepoRelativePosixPath)); + if (!pubspecFile.existsSync()) { + // If the package was deleted, nothing will be published. + return false; + } final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec.publishTo == 'none') { return false; diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index 04869639cf7..5ee42848a81 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -196,19 +196,27 @@ dependency_overrides: Future> _getNonBreakingUpdatePackages() async { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final String baseSha = await gitVersionFinder.getBaseSha(); - print('Finding changed packages relative to "$baseSha"\n'); + print('Finding changed packages relative to "$baseSha"...'); final Set changedPackages = {}; - for (final String path in await gitVersionFinder.getChangedFiles()) { + for (final String changedPath in await gitVersionFinder.getChangedFiles()) { // Git output always uses Posix paths. - final List allComponents = p.posix.split(path); + final List allComponents = p.posix.split(changedPath); // Only pubspec changes are potential publishing events. if (allComponents.last != 'pubspec.yaml' || allComponents.contains('example')) { continue; } final RepositoryPackage package = - RepositoryPackage(packagesDir.fileSystem.file(path).parent); + RepositoryPackage(packagesDir.fileSystem.file(changedPath).parent); + // Ignored deleted packages, as they won't be published. + if (!package.pubspecFile.existsSync()) { + final String directoryName = p.posix.joinAll(path.split(path.relative( + package.directory.absolute.path, + from: packagesDir.path))); + print(' Skipping $directoryName; deleted.'); + continue; + } final String packageName = package.parsePubspec().name; if (!await _hasNonBreakingVersionChange(package)) { // Log packages that had pubspec changes but weren't included for ease diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart index e23485fbc8b..126aa8b41c8 100644 --- a/script/tool/test/federation_safety_check_command_test.dart +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -352,4 +352,64 @@ void main() { ]), ); }); + + test('handles top-level files that match federated package heuristics', + () async { + final Directory plugin = createFakePlugin('foo', packagesDir); + + final String changedFileOutput = [ + // This should be picked up as a change to 'foo', and not crash. + plugin.childFile('foo_bar.baz'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo...'), + ]), + ); + }); + + test('handles deletion of an entire plugin', () async { + // Simulate deletion, in the form of diffs for packages that don't exist in + // the filesystem. + final String changedFileOutput = [ + packagesDir.childDirectory('foo').childFile('pubspec.yaml'), + packagesDir + .childDirectory('foo') + .childDirectory('lib') + .childFile('foo.dart'), + packagesDir + .childDirectory('foo_platform_interface') + .childFile('pubspec.yaml'), + packagesDir + .childDirectory('foo_platform_interface') + .childDirectory('lib') + .childFile('foo.dart'), + packagesDir.childDirectory('foo_web').childFile('pubspec.yaml'), + packagesDir + .childDirectory('foo_web') + .childDirectory('lib') + .childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Ran for 0 package(s)'), + ]), + ); + }); } diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index 29e4f724b33..bdd2139b237 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -173,6 +173,29 @@ void main() { ); }); + test('no-ops for no deleted packages', () async { + final String changedFileOutput = [ + // A change for a file that's not on disk simulates a deletion. + packagesDir.childDirectory('foo').childFile('pubspec.yaml'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('Skipping foo; deleted.'), + contains('No target dependencies'), + ]), + ); + }); + test('includes bugfix version changes as targets', () async { const String newVersion = '1.0.1'; final Directory package = From 3de67cea560a4ca7082f56b1033db1af53845245 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 7 Dec 2021 10:02:24 -0500 Subject: [PATCH 156/249] [flutter_plugin_tools] Improve package targeting (#4577) Improve package targeting: - `--run-on-changed-packages` now includes only changed packages in a federated plugin, not all packages. Include all packages isn't useful since (without changes that would cause them to be included anyway) package dependencies are not path-based between packages. - `--packages` now allows specifying package names of federated plugins. E.g., `--packages=path_provider_ios` will now work, instead of requiring `--packages=path_provider/path_provider_ios`. The fully qualified form still works as well (and is still needed for app-facing packages to disambiguate from the plugin group). Fixes https://github.com/flutter/flutter/issues/94618 --- script/tool/CHANGELOG.md | 5 ++ script/tool/README.md | 9 +- .../tool/lib/src/common/plugin_command.dart | 67 +++++++++++---- .../tool/test/common/plugin_command_test.dart | 85 +++++++++++++++++-- 4 files changed, 139 insertions(+), 27 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index dec218e2ba2..72539c24c5f 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,11 @@ dependencies to path-based dependencies. - Adds a (hidden) `--run-on-dirty-packages` flag for use with `make-deps-path-based` in CI. +- `--packages` now allows using a federated plugin's package as a target without + fully specifying it (if it is not the same as the plugin's name). E.g., + `--packages=path_provide_ios` now works. +- `--run-on-changed-packages` now includes only the changed packages in a + federated plugin, not all packages in that plugin. - Fix `federation-safety-check` handling of plugin deletion, and of top-level files in unfederated plugins whose names match federated plugin heuristics (e.g., `packages/foo/foo_android.iml`). diff --git a/script/tool/README.md b/script/tool/README.md index 1a87f098757..613cb5456e0 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -51,8 +51,13 @@ following shows a number of common commands being run for a specific plugin. All examples assume running from source; see above for running the published version instead. -Note that the `plugins` argument, despite the name, applies to any package. -(It will likely be renamed `packages` in the future.) +Most commands take a `--packages` argument to control which package(s) the +command is targetting. An package name can be any of: +- The name of a package (e.g., `path_provider_android`). +- The name of a federated plugin (e.g., `path_provider`), in which case all + packages that make up that plugin will be targetted. +- A combination federated_plugin_name/package_name (e.g., + `path_provider/path_provider` for the app-facing package). ### Format Code diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 7166c754e12..18466356822 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -339,7 +339,7 @@ abstract class PluginCommand extends Command { final List changedFiles = await gitVersionFinder.getChangedFiles(); if (!_changesRequireFullTest(changedFiles)) { - packages = _getChangedPackages(changedFiles); + packages = _getChangedPackageNames(changedFiles); } } else if (getBoolArg(_runOnDirtyPackagesArg)) { final GitVersionFinder gitVersionFinder = @@ -348,7 +348,7 @@ abstract class PluginCommand extends Command { // _changesRequireFullTest is deliberately not used here, as this flag is // intended for use in CI to re-test packages changed by // 'make-deps-path-based'. - packages = _getChangedPackages( + packages = _getChangedPackageNames( await gitVersionFinder.getChangedFiles(includeUncommitted: true)); // For the same reason, empty is not treated as "all packages" as it is // for other flags. @@ -379,21 +379,23 @@ abstract class PluginCommand extends Command { 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 = - path.relative(subdir.path, from: dir.path); - final String packageName = path.basename(subdir.path); - final String basenamePath = path.basename(entity.path); + // There are three ways for a federated plugin to match: + // - package name (path_provider_android) + // - fully specified name (path_provider/path_provider_android) + // - group name (path_provider), which matches all packages in + // the group + final Set possibleMatches = { + path.basename(subdir.path), // package name + path.basename(entity.path), // group name + path.relative(subdir.path, from: dir.path), // fully specified + }; if (packages.isEmpty || - packages.contains(relativePath) || - packages.contains(basenamePath)) { + packages.intersection(possibleMatches).isNotEmpty) { yield PackageEnumerationEntry( RepositoryPackage(subdir as Directory), - excluded: excludedPluginNames.contains(basenamePath) || - excludedPluginNames.contains(packageName) || - excludedPluginNames.contains(relativePath)); + excluded: excludedPluginNames + .intersection(possibleMatches) + .isNotEmpty); } } } @@ -454,17 +456,48 @@ abstract class PluginCommand extends Command { return gitVersionFinder; } - // Returns packages that have been changed given a list of changed files. + // Returns the names of packages that have been changed given a list of + // changed files. + // + // The names will either be the actual package names, or potentially + // group/name specifiers (for example, path_provider/path_provider) for + // packages in federated plugins. // // The paths must use POSIX separators (e.g., as provided by git output). - Set _getChangedPackages(List changedFiles) { + Set _getChangedPackageNames(List changedFiles) { final Set packages = {}; + + // A helper function that returns true if candidatePackageName looks like an + // implementation package of a plugin called pluginName. Used to determine + // if .../packages/parentName/candidatePackageName/... + // looks like a path in a federated plugin package (candidatePackageName) + // rather than a top-level package (parentName). + bool isFederatedPackage(String candidatePackageName, String parentName) { + return candidatePackageName == parentName || + candidatePackageName.startsWith('${parentName}_'); + } + for (final String path in changedFiles) { final List pathComponents = p.posix.split(path); final int packagesIndex = pathComponents.indexWhere((String element) => element == 'packages'); if (packagesIndex != -1) { - packages.add(pathComponents[packagesIndex + 1]); + // Find the name of the directory directly under packages. This is + // either the name of the package, or a plugin group directory for + // a federated plugin. + final String topLevelName = pathComponents[packagesIndex + 1]; + String packageName = topLevelName; + if (packagesIndex + 2 < pathComponents.length && + isFederatedPackage( + pathComponents[packagesIndex + 2], topLevelName)) { + // This looks like a federated package; use the full specifier if + // the name would be ambiguous (i.e., for the app-facing package). + packageName = pathComponents[packagesIndex + 2]; + if (packageName == topLevelName) { + packageName = '$topLevelName/$packageName'; + } + } + packages.add(packageName); } } if (packages.isEmpty) { diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 222df544f34..28a03c61d59 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -180,6 +180,79 @@ void main() { expect(command.plugins, unorderedEquals([])); }); + test( + 'explicitly specifying the plugin (group) name of a federated plugin ' + 'should include all plugins in the group', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory pluginGroup = packagesDir.childDirectory('plugin1'); + final Directory appFacingPackage = + createFakePlugin('plugin1', pluginGroup); + final Directory platformInterfacePackage = + createFakePlugin('plugin1_platform_interface', pluginGroup); + final Directory implementationPackage = + createFakePlugin('plugin1_web', pluginGroup); + + await runCapturingPrint( + runner, ['sample', '--base-sha=main', '--packages=plugin1']); + + expect( + command.plugins, + unorderedEquals([ + appFacingPackage.path, + platformInterfacePackage.path, + implementationPackage.path + ])); + }); + + test( + 'specifying the app-facing package of a federated plugin using its ' + 'fully qualified name should include only that package', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory pluginGroup = packagesDir.childDirectory('plugin1'); + final Directory appFacingPackage = + createFakePlugin('plugin1', pluginGroup); + createFakePlugin('plugin1_platform_interface', pluginGroup); + createFakePlugin('plugin1_web', pluginGroup); + + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--packages=plugin1/plugin1']); + + expect(command.plugins, unorderedEquals([appFacingPackage.path])); + }); + + test( + 'specifying a package of a federated plugin by its name should ' + 'include only that package', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory pluginGroup = packagesDir.childDirectory('plugin1'); + + createFakePlugin('plugin1', pluginGroup); + final Directory platformInterfacePackage = + createFakePlugin('plugin1_platform_interface', pluginGroup); + createFakePlugin('plugin1_web', pluginGroup); + + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=main', + '--packages=plugin1_platform_interface' + ]); + + expect(command.plugins, + unorderedEquals([platformInterfacePackage.path])); + }); + group('conflicting package selection', () { test('does not allow --packages with --run-on-changed-packages', () async { @@ -442,7 +515,7 @@ packages/plugin1/plugin1_web/plugin1_web.dart }); test( - 'changing one plugin in a federated group should include all plugins in the group', + 'changing one plugin in a federated group should only include that plugin', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: ''' @@ -451,17 +524,13 @@ packages/plugin1/plugin1/plugin1.dart ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - final Directory plugin2 = createFakePlugin('plugin1_platform_interface', + createFakePlugin('plugin1_platform_interface', packagesDir.childDirectory('plugin1')); - final Directory plugin3 = createFakePlugin( - 'plugin1_web', packagesDir.childDirectory('plugin1')); + createFakePlugin('plugin1_web', packagesDir.childDirectory('plugin1')); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); - expect( - command.plugins, - unorderedEquals( - [plugin1.path, plugin2.path, plugin3.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); test('--exclude flag works with --run-on-changed-packages', () async { From 12afeda41e7c0c85a67ca9a2f74fef14e1760aee Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Fri, 10 Dec 2021 03:18:07 -0800 Subject: [PATCH 157/249] Add missing return for nullably typed function (#4598) --- script/tool/lib/src/pubspec_check_command.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 04c3848a7de..4f2b208524d 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -211,6 +211,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { return '"description" is too long. pub.dev recommends package ' 'descriptions of 60-180 characters.'; } + return null; } bool _checkIssueLink(Pubspec pubspec) { From e45a509b3538b460a31e24dbfb9d46c5696e8a50 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 10 Dec 2021 16:09:20 -0800 Subject: [PATCH 158/249] [tools] fix typo in tools (#4607) --- script/tool/lib/src/lint_android_command.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index a7b5c4f2e8b..49b2181a461 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -12,9 +12,9 @@ import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; -/// Lint the CocoaPod podspecs and run unit tests. +/// Run 'gradlew lint'. /// -/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +/// See https://developer.android.com/studio/write/lint. class LintAndroidCommand extends PackageLoopingCommand { /// Creates an instance of the linter command. LintAndroidCommand( From 7855a2036950b0e9a47ac82b867bcaff2276d90e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 15 Dec 2021 11:22:52 -0500 Subject: [PATCH 159/249] [flutter_plugin_tools] Auto-retry failed FTL tests (#4610) Currently the flake situation for Firebase Test Lab tests is very bad, and the task running those tests are some of the slowest tasks in the CI. Re-running failed tests is slow, manual work that is signficantly affecting productivity. There are plans to actually address the flake in the short-to-medium term, but in the immediate term this will improve the CI situation, as well as reducing the drag on engineering time that could be spent on the root causes. Part of https://github.com/flutter/flutter/issues/95063 --- script/tool/CHANGELOG.md | 2 + .../lib/src/firebase_test_lab_command.dart | 74 +++++++++++++------ .../test/firebase_test_lab_command_test.dart | 41 +++++++++- 3 files changed, 93 insertions(+), 24 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 72539c24c5f..12ccf17d4d0 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -13,6 +13,8 @@ - Fix `federation-safety-check` handling of plugin deletion, and of top-level files in unfederated plugins whose names match federated plugin heuristics (e.g., `packages/foo/foo_android.iml`). +- Add an auto-retry for failed Firebase Test Lab tests as a short-term patch + for flake issues. ## 0.7.3 diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 4e53ee8fbac..e824d8ad1a9 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -176,30 +176,22 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { final String testRunId = getStringArg('test-run-id'); final String resultsDir = 'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/'; - final List args = [ - 'firebase', - 'test', - 'android', - 'run', - '--type', - 'instrumentation', - '--app', - 'build/app/outputs/apk/debug/app-debug.apk', - '--test', - 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', - '--timeout', - '7m', - '--results-bucket=${getStringArg('results-bucket')}', - '--results-dir=$resultsDir', - ]; - for (final String device in getStringListArg('device')) { - args.addAll(['--device', device]); - } - final int exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: example.directory); - if (exitCode != 0) { - printError('Test failure for $testName'); + // Automatically retry failures; there is significant flake with these + // tests whose cause isn't yet understood, and having to re-run the + // entire shard for a flake in any one test is extremely slow. This should + // be removed once the root cause of the flake is understood. + // See https://github.com/flutter/flutter/issues/95063 + const int maxRetries = 2; + bool passing = false; + for (int i = 1; i <= maxRetries && !passing; ++i) { + if (i > 1) { + logWarning('$testName failed on attempt ${i - 1}. Retrying...'); + } + passing = await _runFirebaseTest(example, test, resultsDir: resultsDir); + } + if (!passing) { + printError('Test failure for $testName after $maxRetries attempts'); errors.add('$testName failed tests'); } } @@ -238,6 +230,42 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { return true; } + /// Runs [test] from [example] as a Firebase Test Lab test, returning true if + /// the test passed. + /// + /// [resultsDir] should be a unique-to-the-test-run directory to store the + /// results on the server. + Future _runFirebaseTest( + RepositoryPackage example, + File test, { + required String resultsDir, + }) async { + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '7m', + '--results-bucket=${getStringArg('results-bucket')}', + '--results-dir=$resultsDir', + for (final String device in getStringListArg('device')) ...[ + '--device', + device + ], + ]; + final int exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: example.directory); + + return exitCode == 0; + } + /// Builds [target] using Gradle in the given [project]. Assumes Gradle is /// already configured. /// diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 65f398b32ca..1dfd8ba66b5 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -271,7 +271,7 @@ public class MainActivityTest { ); }); - test('fails if a test fails', () async { + test('fails if a test fails twice', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; final Directory pluginDir = @@ -287,6 +287,7 @@ public class MainActivityTest { MockProcess(), // auth MockProcess(), // config MockProcess(exitCode: 1), // integration test #1 + MockProcess(exitCode: 1), // integration test #1 retry MockProcess(), // integration test #2 ]; @@ -315,6 +316,44 @@ public class MainActivityTest { ); }); + test('passes with warning if a test fails once, then passes on retry', + () async { + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + javaTestFileRelativePath, + ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + + processRunner.mockProcessesForExecutable['gcloud'] = [ + MockProcess(), // auth + MockProcess(), // config + MockProcess(exitCode: 1), // integration test #1 + MockProcess(), // integration test #1 retry + MockProcess(), // integration test #2 + ]; + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=redfin,version=30', + ]); + + expect( + output, + containsAllInOrder([ + contains('Testing example/integration_test/bar_test.dart...'), + contains('bar_test.dart failed on attempt 1. Retrying...'), + contains('Testing example/integration_test/foo_test.dart...'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + test('fails for packages with no androidTest directory', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', From 902c337f6fdfc1e980dbf7940438a9518ee68529 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 5 Jan 2022 15:53:05 -0500 Subject: [PATCH 160/249] [ci] Pin Chromium version for web tests (#4620) Switches the web tests from using the version of Chrome installed by the Dockerfile, which is whatever happened to be stable when the image is generated, and thus not hermetic, to a pinned version of Chromium. This uses a slightly modified version of the script that is already used for flutter/packages. Since Chromium doesn't support mp4 playback, this updates the `video_player` integration tests to use WebM on web instead, to avoid having all the tests fail in CI. Part of https://github.com/flutter/flutter/issues/84712 --- script/tool/CHANGELOG.md | 6 ++- .../tool/lib/src/drive_examples_command.dart | 4 +- .../test/drive_examples_command_test.dart | 52 +++++++++++++++++++ script/tool/test/mocks.dart | 3 ++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 12ccf17d4d0..101bfac2b9c 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -10,11 +10,13 @@ `--packages=path_provide_ios` now works. - `--run-on-changed-packages` now includes only the changed packages in a federated plugin, not all packages in that plugin. -- Fix `federation-safety-check` handling of plugin deletion, and of top-level +- Fixes `federation-safety-check` handling of plugin deletion, and of top-level files in unfederated plugins whose names match federated plugin heuristics (e.g., `packages/foo/foo_android.iml`). -- Add an auto-retry for failed Firebase Test Lab tests as a short-term patch +- Adds an auto-retry for failed Firebase Test Lab tests as a short-term patch for flake issues. +- Adds support for `CHROME_EXECUTABLE` in `drive-examples` to match similar + `flutter` behavior. ## 0.7.3 diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 593e557fa39..5bf0298e4e3 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -120,7 +120,9 @@ class DriveExamplesCommand extends PackageLoopingCommand { '-d', 'web-server', '--web-port=7357', - '--browser-name=chrome' + '--browser-name=chrome', + if (platform.environment.containsKey('CHROME_EXECUTABLE')) + '--chrome-binary=${platform.environment['CHROME_EXECUTABLE']}', ], if (getBoolArg(kPlatformWindows)) kPlatformWindows: ['-d', 'windows'], diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index a7a1652c2fc..3c93d8bfe10 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -584,6 +584,58 @@ void main() { ])); }); + test('driving a web plugin with CHROME_EXECUTABLE', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + mockPlatform.environment['CHROME_EXECUTABLE'] = '/path/to/chrome'; + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + '--chrome-binary=/path/to/chrome', + '--driver', + 'test_driver/plugin_test.dart', + '--target', + 'test_driver/plugin.dart' + ], + pluginExampleDirectory.path), + ])); + }); + test('driving when plugin does not suppport Windows is a no-op', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/test_driver/plugin_test.dart', diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 3d0aef1b397..f6333ebd367 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -30,6 +30,9 @@ class MockPlatform extends Mock implements Platform { Uri get script => isWindows ? Uri.file(r'C:\foo\bar', windows: true) : Uri.file('/foo/bar', windows: false); + + @override + Map environment = {}; } class MockProcess extends Mock implements io.Process { From 257f10e19e6a21e7ceefd90f6dab1843cba2a82f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 24 Jan 2022 23:25:15 -0500 Subject: [PATCH 161/249] [all] Update repo links to 'main' (#4690) --- script/tool/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 8cde4dd46d8..4849feee7b8 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages -repository: https://github.com/flutter/plugins/tree/master/script/tool +repository: https://github.com/flutter/plugins/tree/main/script/tool version: 0.7.2 dependencies: From 0f71f1755bbe81e9978889802724a3d29e81974c Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Tue, 25 Jan 2022 13:10:21 -0800 Subject: [PATCH 162/249] [webview_flutter] Remove dependency on flutter.dev and google.com (#4691) --- script/tool/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/script/tool/README.md b/script/tool/README.md index 613cb5456e0..265d3868fc3 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -84,10 +84,13 @@ dart run ./script/tool/bin/flutter_plugin_tools.dart test --packages plugin_name ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart build-examples --packages plugin_name -dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart build-examples --apk --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --android --packages plugin_name ``` +Replace `--apk`/`--android` with the platform you want to test against +(omit it to get a list of valid options). + ### Run Native Tests `native-test` takes one or more platform flags to run tests for. By default it From 70092348670317031030ed7001cfac7538db61e0 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 26 Jan 2022 17:59:32 -0500 Subject: [PATCH 163/249] Enforce asterisk alignment for C++ and ObjC pointers (#4703) So far we've been using the default mode of prevailing-in-file, which means we aren't consistent within each language what mode we use. Now that clang-format can identify ObjC headers (which didn't used to be the case), we can enforce different styles for the two languages. This sets left-aligned for C++ to match the Flutter engine, and right-aligned for ObjC to match the prevaling Apple style. --- script/tool/lib/src/format_command.dart | 2 +- script/tool/test/format_command_test.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index f24a99436c8..10c0779de92 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -138,7 +138,7 @@ class FormatCommand extends PluginCommand { print('Formatting .cc, .cpp, .h, .m, and .mm files...'); final int exitCode = await _runBatched( - getStringArg('clang-format'), ['-i', '--style=Google'], + getStringArg('clang-format'), ['-i', '--style=file'], files: clangFiles); if (exitCode != 0) { printError( diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index d278bb2940b..2890c528e4c 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -301,7 +301,7 @@ void main() { 'clang-format', [ '-i', - '--style=Google', + '--style=file', ..._getPackagesDirRelativePaths(pluginDir, files) ], packagesDir.path), @@ -357,7 +357,7 @@ void main() { '/path/to/clang-format', [ '-i', - '--style=Google', + '--style=file', ..._getPackagesDirRelativePaths(pluginDir, files) ], packagesDir.path), @@ -425,7 +425,7 @@ void main() { 'clang-format', [ '-i', - '--style=Google', + '--style=file', ..._getPackagesDirRelativePaths(pluginDir, clangFiles) ], packagesDir.path), From 4aeb80aed746a566b593a3bf4e7f12309186c9ba Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 10 Feb 2022 17:35:22 -0500 Subject: [PATCH 164/249] [url_launcher] Support new desktop implementation versions (#4779) --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/pubspec_check_command.dart | 58 +++++++++++- .../tool/test/pubspec_check_command_test.dart | 93 ++++++++++++++++++- 3 files changed, 148 insertions(+), 4 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 101bfac2b9c..fbf7610b3ea 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -17,6 +17,7 @@ for flake issues. - Adds support for `CHROME_EXECUTABLE` in `drive-examples` to match similar `flutter` behavior. +- Validates `default_package` entries in plugins. ## 0.7.3 diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 4f2b208524d..2c27c91e049 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:platform/platform.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -100,9 +101,17 @@ class PubspecCheckCommand extends PackageLoopingCommand { } if (isPlugin) { - final String? error = _checkForImplementsError(pubspec, package: package); - if (error != null) { - printError('$indentation$error'); + final String? implementsError = + _checkForImplementsError(pubspec, package: package); + if (implementsError != null) { + printError('$indentation$implementsError'); + passing = false; + } + + final String? defaultPackageError = + _checkForDefaultPackageError(pubspec, package: package); + if (defaultPackageError != null) { + printError('$indentation$defaultPackageError'); passing = false; } } @@ -243,6 +252,49 @@ class PubspecCheckCommand extends PackageLoopingCommand { return null; } + // Validates any "default_package" entries a plugin, returning an error + // string if there are any issues. + // + // Should only be called on plugin packages. + String? _checkForDefaultPackageError( + Pubspec pubspec, { + required RepositoryPackage package, + }) { + final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms']; + if (platformsEntry == null) { + logWarning('Does not implement any platforms'); + return null; + } + final YamlMap platforms = platformsEntry as YamlMap; + final String packageName = package.directory.basename; + + // Validate that the default_package entries look correct (e.g., no typos). + final Set defaultPackages = {}; + for (final MapEntry platformEntry in platforms.entries) { + final String? defaultPackage = + platformEntry.value['default_package'] as String?; + if (defaultPackage != null) { + defaultPackages.add(defaultPackage); + if (!defaultPackage.startsWith('${packageName}_')) { + return '"$defaultPackage" is not an expected implementation name ' + 'for "$packageName"'; + } + } + } + + // Validate that all default_packages are also dependencies. + final Iterable dependencies = pubspec.dependencies.keys; + final Iterable missingPackages = defaultPackages + .where((String package) => !dependencies.contains(package)); + if (missingPackages.isNotEmpty) { + return 'The following default_packages are missing ' + 'corresponding dependencies:\n ' + + missingPackages.join('\n '); + } + + return null; + } + // Returns true if [packageName] appears to be an implementation package // according to repository conventions. bool _isImplementationPackage(RepositoryPackage package) { diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 9ad1eaa620c..42d20240da8 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -70,12 +70,27 @@ environment: String _flutterSection({ bool isPlugin = false, String? implementedPackage, + Map> pluginPlatformDetails = + const >{}, }) { - final String pluginEntry = ''' + String pluginEntry = ''' plugin: ${implementedPackage == null ? '' : ' implements: $implementedPackage'} platforms: '''; + + for (final MapEntry> platform + in pluginPlatformDetails.entries) { + pluginEntry += ''' + ${platform.key}: +'''; + for (final MapEntry detail in platform.value.entries) { + pluginEntry += ''' + ${detail.key}: ${detail.value} +'''; + } + } + return ''' flutter: ${isPlugin ? pluginEntry : ''} @@ -647,6 +662,82 @@ ${_devDependenciesSection()} ); }); + test('fails when a "default_package" looks incorrect', () async { + final Directory pluginDirectory = + createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${_headerSection( + 'plugin_a', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', + )} +${_environmentSection()} +${_flutterSection( + isPlugin: true, + pluginPlatformDetails: >{ + 'android': {'default_package': 'plugin_b_android'} + }, + )} +${_dependenciesSection()} +${_devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + '"plugin_b_android" is not an expected implementation name for "plugin_a"'), + ]), + ); + }); + + test( + 'fails when a "default_package" does not have a corresponding dependency', + () async { + final Directory pluginDirectory = + createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${_headerSection( + 'plugin_a', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', + )} +${_environmentSection()} +${_flutterSection( + isPlugin: true, + pluginPlatformDetails: >{ + 'android': {'default_package': 'plugin_a_android'} + }, + )} +${_dependenciesSection()} +${_devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following default_packages are missing corresponding ' + 'dependencies:\n plugin_a_android'), + ]), + ); + }); + test('passes for an app-facing package without "implements"', () async { final Directory pluginDirectory = createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); From fdba05bf578268ed17796df65639f1ae46e2689f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 14 Feb 2022 15:50:12 -0500 Subject: [PATCH 165/249] [flutter_plugin_tool] Remove podspec --allow-warnings (#4839) --- script/tool/CHANGELOG.md | 3 +- .../tool/lib/src/lint_podspecs_command.dart | 11 +---- script/tool/pubspec.yaml | 3 +- .../tool/test/lint_podspecs_command_test.dart | 42 ------------------- 4 files changed, 4 insertions(+), 55 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index fbf7610b3ea..8f2807f0dd0 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 0.8.0 - Ensures that `firebase-test-lab` runs include an `integration_test` runner. - Adds a `make-deps-path-based` command to convert inter-repo package @@ -18,6 +18,7 @@ - Adds support for `CHROME_EXECUTABLE` in `drive-examples` to match similar `flutter` behavior. - Validates `default_package` entries in plugins. +- Removes `allow-warnings` from the `podspecs` command. ## 0.7.3 diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index ee44a82da5b..198dd947211 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -26,13 +26,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addMultiOption('ignore-warnings', - help: - 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs ' - 'with this basename (example: plugins with known warnings)', - valueHelp: 'podspec_file_name'); - } + }) : super(packagesDir, processRunner: processRunner, platform: platform); @override final String name = 'podspecs'; @@ -118,8 +112,6 @@ class LintPodspecsCommand extends PackageLoopingCommand { Future _runPodLint(String podspecPath, {required bool libraryLint}) async { - final bool allowWarnings = (getStringListArg('ignore-warnings')) - .contains(p.basenameWithoutExtension(podspecPath)); final List arguments = [ 'lib', 'lint', @@ -127,7 +119,6 @@ class LintPodspecsCommand extends PackageLoopingCommand { '--configuration=Debug', // Release targets unsupported arm64 simulators. Use Debug to only build against targeted x86_64 simulator devices. '--skip-tests', '--use-modular-headers', // Flutter sets use_modular_headers! in its templates. - if (allowWarnings) '--allow-warnings', if (libraryLint) '--use-libraries' ]; diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 4849feee7b8..9ca5e2b7758 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.7.2 +version: 0.8.0 dependencies: args: ^2.1.0 @@ -26,7 +26,6 @@ dev_dependencies: build_runner: ^2.0.3 matcher: ^0.12.10 mockito: ^5.0.7 - pedantic: ^1.11.0 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 44247274028..bccbec67866 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -123,48 +123,6 @@ void main() { expect(output, contains('Bar')); }); - test('allow warnings for podspecs with known warnings', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - - final List output = await runCapturingPrint( - runner, ['podspecs', '--ignore-warnings=plugin1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', const ['pod'], packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin1Dir.childFile('plugin1.podspec').path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - '--allow-warnings', - '--use-libraries' - ], - packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin1Dir.childFile('plugin1.podspec').path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - '--allow-warnings', - ], - packagesDir.path), - ]), - ); - - expect(output, contains('Linting plugin1.podspec')); - }); - test('fails if pod is missing', () async { createFakePlugin('plugin1', packagesDir, extraFiles: ['plugin1.podspec']); From 199afd96f1a9eb9e9a032233fc67b26f48d98709 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 15 Feb 2022 17:20:24 -0800 Subject: [PATCH 166/249] [flutter_plugin_tool] Fix iOS/macOS naming (#4861) --- .../tool/lib/src/build_examples_command.dart | 44 ++-- script/tool/lib/src/common/core.dart | 16 +- script/tool/lib/src/common/plugin_utils.dart | 16 +- .../tool/lib/src/drive_examples_command.dart | 63 +++--- script/tool/lib/src/lint_android_command.dart | 2 +- script/tool/lib/src/native_test_command.dart | 42 ++-- script/tool/lib/src/test_command.dart | 2 +- .../tool/lib/src/xcode_analyze_command.dart | 22 +- .../test/build_examples_command_test.dart | 30 +-- .../tool/test/common/plugin_utils_test.dart | 190 +++++++++--------- .../test/drive_examples_command_test.dart | 64 +++--- .../tool/test/lint_android_command_test.dart | 8 +- .../tool/test/native_test_command_test.dart | 96 ++++----- script/tool/test/test_command_test.dart | 2 +- script/tool/test/util.dart | 14 +- .../tool/test/xcode_analyze_command_test.dart | 22 +- 16 files changed, 316 insertions(+), 317 deletions(-) diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 82ed074c462..b88cfe30925 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -33,7 +33,7 @@ const int _exitInvalidPluginToolsConfig = 4; // Flutter build types. These are the values passed to `flutter build `. const String _flutterBuildTypeAndroid = 'apk'; -const String _flutterBuildTypeIos = 'ios'; +const String _flutterBuildTypeIOS = 'ios'; const String _flutterBuildTypeLinux = 'linux'; const String _flutterBuildTypeMacOS = 'macos'; const String _flutterBuildTypeWeb = 'web'; @@ -48,12 +48,12 @@ class BuildExamplesCommand extends PackageLoopingCommand { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(kPlatformLinux); - argParser.addFlag(kPlatformMacos); - argParser.addFlag(kPlatformWeb); - argParser.addFlag(kPlatformWindows); - argParser.addFlag(kPlatformWinUwp); - argParser.addFlag(kPlatformIos); + argParser.addFlag(platformLinux); + argParser.addFlag(platformMacOS); + argParser.addFlag(platformWeb); + argParser.addFlag(platformWindows); + argParser.addFlag(platformWinUwp); + argParser.addFlag(platformIOS); argParser.addFlag(_platformFlagApk); argParser.addOption( kEnableExperiment, @@ -68,39 +68,39 @@ class BuildExamplesCommand extends PackageLoopingCommand { { _platformFlagApk: const _PlatformDetails( 'Android', - pluginPlatform: kPlatformAndroid, + pluginPlatform: platformAndroid, flutterBuildType: _flutterBuildTypeAndroid, ), - kPlatformIos: const _PlatformDetails( + platformIOS: const _PlatformDetails( 'iOS', - pluginPlatform: kPlatformIos, - flutterBuildType: _flutterBuildTypeIos, + pluginPlatform: platformIOS, + flutterBuildType: _flutterBuildTypeIOS, extraBuildFlags: ['--no-codesign'], ), - kPlatformLinux: const _PlatformDetails( + platformLinux: const _PlatformDetails( 'Linux', - pluginPlatform: kPlatformLinux, + pluginPlatform: platformLinux, flutterBuildType: _flutterBuildTypeLinux, ), - kPlatformMacos: const _PlatformDetails( + platformMacOS: const _PlatformDetails( 'macOS', - pluginPlatform: kPlatformMacos, + pluginPlatform: platformMacOS, flutterBuildType: _flutterBuildTypeMacOS, ), - kPlatformWeb: const _PlatformDetails( + platformWeb: const _PlatformDetails( 'web', - pluginPlatform: kPlatformWeb, + pluginPlatform: platformWeb, flutterBuildType: _flutterBuildTypeWeb, ), - kPlatformWindows: const _PlatformDetails( + platformWindows: const _PlatformDetails( 'Win32', - pluginPlatform: kPlatformWindows, + pluginPlatform: platformWindows, pluginPlatformVariant: platformVariantWin32, flutterBuildType: _flutterBuildTypeWin32, ), - kPlatformWinUwp: const _PlatformDetails( + platformWinUwp: const _PlatformDetails( 'UWP', - pluginPlatform: kPlatformWindows, + pluginPlatform: platformWindows, pluginPlatformVariant: platformVariantWinUwp, flutterBuildType: _flutterBuildTypeWinUwp, ), @@ -288,7 +288,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { if (!uwpDirectory.existsSync()) { print('Creating temporary winuwp folder'); final int exitCode = await processRunner.runAndStream(flutterCommand, - ['create', '--platforms=$kPlatformWinUwp', '.'], + ['create', '--platforms=$platformWinUwp', '.'], workingDir: example.directory); if (exitCode == 0) { temporaryPlatformDirectory = uwpDirectory; diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 53778eccb87..15a0d6f1f3b 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -11,40 +11,40 @@ import 'package:yaml/yaml.dart'; typedef Print = void Function(Object? object); /// Key for APK (Android) platform. -const String kPlatformAndroid = 'android'; +const String platformAndroid = 'android'; /// Key for IPA (iOS) platform. -const String kPlatformIos = 'ios'; +const String platformIOS = 'ios'; /// Key for linux platform. -const String kPlatformLinux = 'linux'; +const String platformLinux = 'linux'; /// Key for macos platform. -const String kPlatformMacos = 'macos'; +const String platformMacOS = 'macos'; /// Key for Web platform. -const String kPlatformWeb = 'web'; +const String platformWeb = 'web'; /// Key for windows platform. /// /// Note that this corresponds to the Win32 variant for flutter commands like /// `build` and `run`, but is a general platform containing all Windows /// variants for purposes of the `platform` section of a plugin pubspec). -const String kPlatformWindows = 'windows'; +const String platformWindows = 'windows'; /// Key for WinUWP platform. /// /// Note that UWP is a platform for the purposes of flutter commands like /// `build` and `run`, but a variant of the `windows` platform for the purposes /// of plugin pubspecs). -const String kPlatformWinUwp = 'winuwp'; +const String platformWinUwp = 'winuwp'; /// Key for Win32 variant of the Windows platform. const String platformVariantWin32 = 'win32'; /// Key for UWP variant of the Windows platform. /// -/// See the note on [kPlatformWinUwp]. +/// See the note on [platformWinUwp]. const String platformVariantWinUwp = 'uwp'; /// Key for enable experiment. diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 6cfe9928d68..081ce7f1e81 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -39,12 +39,12 @@ bool pluginSupportsPlatform( PlatformSupport? requiredMode, String? variant, }) { - assert(platform == kPlatformIos || - platform == kPlatformAndroid || - platform == kPlatformWeb || - platform == kPlatformMacos || - platform == kPlatformWindows || - platform == kPlatformLinux); + assert(platform == platformIOS || + platform == platformAndroid || + platform == platformWeb || + platform == platformMacOS || + platform == platformWindows || + platform == platformLinux); final YamlMap? platformEntry = _readPlatformPubspecSectionForPlugin(platform, plugin); @@ -73,7 +73,7 @@ bool pluginSupportsPlatform( // Platforms with variants have a default variant when unspecified for // backward compatibility. Must match the flutter tool logic. const Map defaultVariants = { - kPlatformWindows: platformVariantWin32, + platformWindows: platformVariantWin32, }; if (variant != defaultVariants[platform]) { return false; @@ -87,7 +87,7 @@ bool pluginSupportsPlatform( /// Returns true if [plugin] includes native code for [platform], as opposed to /// being implemented entirely in Dart. bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { - if (platform == kPlatformWeb) { + if (platform == platformWeb) { // Web plugins are always Dart-only. return false; } diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 5bf0298e4e3..d81153a0fef 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -25,19 +25,19 @@ class DriveExamplesCommand extends PackageLoopingCommand { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(kPlatformAndroid, + argParser.addFlag(platformAndroid, help: 'Runs the Android implementation of the examples'); - argParser.addFlag(kPlatformIos, + argParser.addFlag(platformIOS, help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(kPlatformLinux, + argParser.addFlag(platformLinux, help: 'Runs the Linux implementation of the examples'); - argParser.addFlag(kPlatformMacos, + argParser.addFlag(platformMacOS, help: 'Runs the macOS implementation of the examples'); - argParser.addFlag(kPlatformWeb, + argParser.addFlag(platformWeb, help: 'Runs the web implementation of the examples'); - argParser.addFlag(kPlatformWindows, + argParser.addFlag(platformWindows, help: 'Runs the Windows (Win32) implementation of the examples'); - argParser.addFlag(kPlatformWinUwp, + argParser.addFlag(platformWinUwp, help: 'Runs the UWP implementation of the examples [currently a no-op]'); argParser.addOption( @@ -64,13 +64,13 @@ class DriveExamplesCommand extends PackageLoopingCommand { @override Future initializeRun() async { final List platformSwitches = [ - kPlatformAndroid, - kPlatformIos, - kPlatformLinux, - kPlatformMacos, - kPlatformWeb, - kPlatformWindows, - kPlatformWinUwp, + platformAndroid, + platformIOS, + platformLinux, + platformMacOS, + platformWeb, + platformWindows, + platformWinUwp, ]; final int platformCount = platformSwitches .where((String platform) => getBoolArg(platform)) @@ -85,12 +85,12 @@ class DriveExamplesCommand extends PackageLoopingCommand { throw ToolExit(_exitNoPlatformFlags); } - if (getBoolArg(kPlatformWinUwp)) { + if (getBoolArg(platformWinUwp)) { logWarning('Driving UWP applications is not yet supported'); } String? androidDevice; - if (getBoolArg(kPlatformAndroid)) { + if (getBoolArg(platformAndroid)) { final List devices = await _getDevicesForPlatform('android'); if (devices.isEmpty) { printError('No Android devices available'); @@ -99,24 +99,24 @@ class DriveExamplesCommand extends PackageLoopingCommand { androidDevice = devices.first; } - String? iosDevice; - if (getBoolArg(kPlatformIos)) { + String? iOSDevice; + if (getBoolArg(platformIOS)) { final List devices = await _getDevicesForPlatform('ios'); if (devices.isEmpty) { printError('No iOS devices available'); throw ToolExit(_exitNoAvailableDevice); } - iosDevice = devices.first; + iOSDevice = devices.first; } _targetDeviceFlags = >{ - if (getBoolArg(kPlatformAndroid)) - kPlatformAndroid: ['-d', androidDevice!], - if (getBoolArg(kPlatformIos)) kPlatformIos: ['-d', iosDevice!], - if (getBoolArg(kPlatformLinux)) kPlatformLinux: ['-d', 'linux'], - if (getBoolArg(kPlatformMacos)) kPlatformMacos: ['-d', 'macos'], - if (getBoolArg(kPlatformWeb)) - kPlatformWeb: [ + if (getBoolArg(platformAndroid)) + platformAndroid: ['-d', androidDevice!], + if (getBoolArg(platformIOS)) platformIOS: ['-d', iOSDevice!], + if (getBoolArg(platformLinux)) platformLinux: ['-d', 'linux'], + if (getBoolArg(platformMacOS)) platformMacOS: ['-d', 'macos'], + if (getBoolArg(platformWeb)) + platformWeb: [ '-d', 'web-server', '--web-port=7357', @@ -124,12 +124,11 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (platform.environment.containsKey('CHROME_EXECUTABLE')) '--chrome-binary=${platform.environment['CHROME_EXECUTABLE']}', ], - if (getBoolArg(kPlatformWindows)) - kPlatformWindows: ['-d', 'windows'], + if (getBoolArg(platformWindows)) + platformWindows: ['-d', 'windows'], // TODO(stuartmorgan): Check these flags once drive supports UWP: // https://github.com/flutter/flutter/issues/82821 - if (getBoolArg(kPlatformWinUwp)) - kPlatformWinUwp: ['-d', 'winuwp'], + if (getBoolArg(platformWinUwp)) platformWinUwp: ['-d', 'winuwp'], }; } @@ -148,9 +147,9 @@ class DriveExamplesCommand extends PackageLoopingCommand { in _targetDeviceFlags.entries) { final String platform = entry.key; String? variant; - if (platform == kPlatformWindows) { + if (platform == platformWindows) { variant = platformVariantWin32; - } else if (platform == kPlatformWinUwp) { + } else if (platform == platformWinUwp) { variant = platformVariantWinUwp; // TODO(stuartmorgan): Remove this once drive supports UWP. // https://github.com/flutter/flutter/issues/82821 diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index 49b2181a461..8368160c4c9 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -32,7 +32,7 @@ class LintAndroidCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - if (!pluginSupportsPlatform(kPlatformAndroid, package, + if (!pluginSupportsPlatform(platformAndroid, package, requiredMode: PlatformSupport.inline)) { return PackageResult.skip( 'Plugin does not have an Android implemenatation.'); diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 0b0dd26ba22..a0d2ebd4e23 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -17,9 +17,9 @@ import 'common/xcode.dart'; const String _unitTestFlag = 'unit'; const String _integrationTestFlag = 'integration'; -const String _iosDestinationFlag = 'ios-destination'; +const String _iOSDestinationFlag = 'ios-destination'; -const int _exitNoIosSimulators = 3; +const int _exitNoIOSSimulators = 3; /// The command to run native tests for plugins: /// - iOS and macOS: XCTests (XCUnitTest and XCUITest) @@ -34,17 +34,17 @@ class NativeTestCommand extends PackageLoopingCommand { }) : _xcode = Xcode(processRunner: processRunner, log: true), super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addOption( - _iosDestinationFlag, + _iOSDestinationFlag, help: 'Specify the destination when running iOS tests.\n' 'This is passed to the `-destination` argument in the xcodebuild command.\n' 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT ' 'for details on how to specify the destination.', ); - argParser.addFlag(kPlatformAndroid, help: 'Runs Android tests'); - argParser.addFlag(kPlatformIos, help: 'Runs iOS tests'); - argParser.addFlag(kPlatformLinux, help: 'Runs Linux tests'); - argParser.addFlag(kPlatformMacos, help: 'Runs macOS tests'); - argParser.addFlag(kPlatformWindows, help: 'Runs Windows tests'); + argParser.addFlag(platformAndroid, help: 'Runs Android tests'); + argParser.addFlag(platformIOS, help: 'Runs iOS tests'); + argParser.addFlag(platformLinux, help: 'Runs Linux tests'); + argParser.addFlag(platformMacOS, help: 'Runs macOS tests'); + argParser.addFlag(platformWindows, help: 'Runs Windows tests'); // By default, both unit tests and integration tests are run, but provide // flags to disable one or the other. @@ -55,7 +55,7 @@ class NativeTestCommand extends PackageLoopingCommand { } // The device destination flags for iOS tests. - List _iosDestinationFlags = []; + List _iOSDestinationFlags = []; final Xcode _xcode; @@ -84,11 +84,11 @@ this command. @override Future initializeRun() async { _platforms = { - kPlatformAndroid: _PlatformDetails('Android', _testAndroid), - kPlatformIos: _PlatformDetails('iOS', _testIos), - kPlatformLinux: _PlatformDetails('Linux', _testLinux), - kPlatformMacos: _PlatformDetails('macOS', _testMacOS), - kPlatformWindows: _PlatformDetails('Windows', _testWindows), + platformAndroid: _PlatformDetails('Android', _testAndroid), + platformIOS: _PlatformDetails('iOS', _testIOS), + platformLinux: _PlatformDetails('Linux', _testLinux), + platformMacOS: _PlatformDetails('macOS', _testMacOS), + platformWindows: _PlatformDetails('Windows', _testWindows), }; _requestedPlatforms = _platforms.keys .where((String platform) => getBoolArg(platform)) @@ -105,29 +105,29 @@ this command. throw ToolExit(exitInvalidArguments); } - if (getBoolArg(kPlatformWindows) && getBoolArg(_integrationTestFlag)) { + if (getBoolArg(platformWindows) && getBoolArg(_integrationTestFlag)) { logWarning('This command currently only supports unit tests for Windows. ' 'See https://github.com/flutter/flutter/issues/70233.'); } - if (getBoolArg(kPlatformLinux) && getBoolArg(_integrationTestFlag)) { + if (getBoolArg(platformLinux) && getBoolArg(_integrationTestFlag)) { logWarning('This command currently only supports unit tests for Linux. ' 'See https://github.com/flutter/flutter/issues/70235.'); } // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { - String destination = getStringArg(_iosDestinationFlag); + String destination = getStringArg(_iOSDestinationFlag); if (destination.isEmpty) { final String? simulatorId = await _xcode.findBestAvailableIphoneSimulator(); if (simulatorId == null) { printError('Cannot find any available iOS simulators.'); - throw ToolExit(_exitNoIosSimulators); + throw ToolExit(_exitNoIOSSimulators); } destination = 'id=$simulatorId'; } - _iosDestinationFlags = [ + _iOSDestinationFlags = [ '-destination', destination, ]; @@ -333,9 +333,9 @@ this command. return _PlatformResult(RunState.succeeded); } - Future<_PlatformResult> _testIos(RepositoryPackage plugin, _TestMode mode) { + Future<_PlatformResult> _testIOS(RepositoryPackage plugin, _TestMode mode) { return _runXcodeTests(plugin, 'iOS', mode, - extraFlags: _iosDestinationFlags); + extraFlags: _iOSDestinationFlags); } Future<_PlatformResult> _testMacOS(RepositoryPackage plugin, _TestMode mode) { diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index ee3540d90d3..2c5dd9934b4 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -62,7 +62,7 @@ class TestCommand extends PackageLoopingCommand { '--color', if (experiment.isNotEmpty) '--enable-experiment=$experiment', // TODO(ditman): Remove this once all plugins are migrated to 'drive'. - if (pluginSupportsPlatform(kPlatformWeb, package)) '--platform=chrome', + if (pluginSupportsPlatform(platformWeb, package)) '--platform=chrome', ], workingDir: package.directory, ); diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index 3d34dab9f08..4298acb1c7e 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -21,8 +21,8 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { Platform platform = const LocalPlatform(), }) : _xcode = Xcode(processRunner: processRunner, log: true), super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(kPlatformIos, help: 'Analyze iOS'); - argParser.addFlag(kPlatformMacos, help: 'Analyze macOS'); + argParser.addFlag(platformIOS, help: 'Analyze iOS'); + argParser.addFlag(platformMacOS, help: 'Analyze macOS'); } final Xcode _xcode; @@ -36,7 +36,7 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { @override Future initializeRun() async { - if (!(getBoolArg(kPlatformIos) || getBoolArg(kPlatformMacos))) { + if (!(getBoolArg(platformIOS) || getBoolArg(platformMacOS))) { printError('At least one platform flag must be provided.'); throw ToolExit(exitInvalidArguments); } @@ -44,28 +44,28 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - final bool testIos = getBoolArg(kPlatformIos) && - pluginSupportsPlatform(kPlatformIos, package, + final bool testIOS = getBoolArg(platformIOS) && + pluginSupportsPlatform(platformIOS, package, requiredMode: PlatformSupport.inline); - final bool testMacos = getBoolArg(kPlatformMacos) && - pluginSupportsPlatform(kPlatformMacos, package, + final bool testMacOS = getBoolArg(platformMacOS) && + pluginSupportsPlatform(platformMacOS, package, requiredMode: PlatformSupport.inline); final bool multiplePlatformsRequested = - getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos); - if (!(testIos || testMacos)) { + getBoolArg(platformIOS) && getBoolArg(platformMacOS); + if (!(testIOS || testMacOS)) { return PackageResult.skip('Not implemented for target platform(s).'); } final List failures = []; - if (testIos && + if (testIOS && !await _analyzePlugin(package, 'iOS', extraFlags: [ '-destination', 'generic/platform=iOS Simulator' ])) { failures.add('iOS'); } - if (testMacos && !await _analyzePlugin(package, 'macOS')) { + if (testMacOS && !await _analyzePlugin(package, 'macOS')) { failures.add('macOS'); } diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index c3b0cb9d5cd..6d8f0b9d648 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -57,7 +57,7 @@ void main() { test('fails if building fails', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }); processRunner @@ -86,7 +86,7 @@ void main() { createFakePlugin('plugin', packagesDir, examples: [], platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline) + platformIOS: const PlatformDetails(PlatformSupport.inline) }); processRunner @@ -136,7 +136,7 @@ void main() { mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -193,7 +193,7 @@ void main() { mockPlatform.isLinux = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -242,7 +242,7 @@ void main() { mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -288,7 +288,7 @@ void main() { test('building for web', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -338,7 +338,7 @@ void main() { mockPlatform.isWindows = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -389,7 +389,7 @@ void main() { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/test', ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + platformWindows: const PlatformDetails(PlatformSupport.federated, variants: [platformVariantWinUwp]), }); @@ -419,7 +419,7 @@ void main() { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/test', ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + platformWindows: const PlatformDetails(PlatformSupport.federated, variants: [platformVariantWinUwp]), }); @@ -470,7 +470,7 @@ void main() { test('building for Android', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -499,7 +499,7 @@ void main() { test('enable-experiment flag for Android', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -521,7 +521,7 @@ void main() { test('enable-experiment flag for ios', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -547,7 +547,7 @@ void main() { test('logs skipped platforms', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), }); final List output = await runCapturingPrint( @@ -681,8 +681,8 @@ void main() { mockPlatform.isLinux = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index ac619e2622e..cedd40acb7d 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -25,109 +25,109 @@ void main() { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin('plugin', packagesDir)); - expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformLinux, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformMacos, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformWeb, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformWindows, plugin), isFalse); + expect(pluginSupportsPlatform(platformAndroid, plugin), isFalse); + expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); + expect(pluginSupportsPlatform(platformLinux, plugin), isFalse); + expect(pluginSupportsPlatform(platformMacOS, plugin), isFalse); + expect(pluginSupportsPlatform(platformWeb, plugin), isFalse); + expect(pluginSupportsPlatform(platformWindows, plugin), isFalse); }); test('all platforms', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), })); - expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformIos, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformLinux, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformMacos, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformWeb, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformWindows, plugin), isTrue); + expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); + expect(pluginSupportsPlatform(platformIOS, plugin), isTrue); + expect(pluginSupportsPlatform(platformLinux, plugin), isTrue); + expect(pluginSupportsPlatform(platformMacOS, plugin), isTrue); + expect(pluginSupportsPlatform(platformWeb, plugin), isTrue); + expect(pluginSupportsPlatform(platformWindows, plugin), isTrue); }); test('some platforms', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), })); - expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformLinux, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformMacos, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformWeb, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformWindows, plugin), isFalse); + expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); + expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); + expect(pluginSupportsPlatform(platformLinux, plugin), isTrue); + expect(pluginSupportsPlatform(platformMacOS, plugin), isFalse); + expect(pluginSupportsPlatform(platformWeb, plugin), isTrue); + expect(pluginSupportsPlatform(platformWindows, plugin), isFalse); }); test('inline plugins are only detected as inline', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), })); expect( - pluginSupportsPlatform(kPlatformAndroid, plugin, + pluginSupportsPlatform(platformAndroid, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformAndroid, plugin, + pluginSupportsPlatform(platformAndroid, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformIos, plugin, + pluginSupportsPlatform(platformIOS, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformIos, plugin, + pluginSupportsPlatform(platformIOS, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformLinux, plugin, + pluginSupportsPlatform(platformLinux, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformLinux, plugin, + pluginSupportsPlatform(platformLinux, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformMacos, plugin, + pluginSupportsPlatform(platformMacOS, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformMacos, plugin, + pluginSupportsPlatform(platformMacOS, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformWeb, plugin, + pluginSupportsPlatform(platformWeb, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformWeb, plugin, + pluginSupportsPlatform(platformWeb, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, requiredMode: PlatformSupport.federated), isFalse); }); @@ -136,60 +136,60 @@ void main() { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.federated), - kPlatformIos: const PlatformDetails(PlatformSupport.federated), - kPlatformLinux: const PlatformDetails(PlatformSupport.federated), - kPlatformMacos: const PlatformDetails(PlatformSupport.federated), - kPlatformWeb: const PlatformDetails(PlatformSupport.federated), - kPlatformWindows: const PlatformDetails(PlatformSupport.federated), + platformAndroid: const PlatformDetails(PlatformSupport.federated), + platformIOS: const PlatformDetails(PlatformSupport.federated), + platformLinux: const PlatformDetails(PlatformSupport.federated), + platformMacOS: const PlatformDetails(PlatformSupport.federated), + platformWeb: const PlatformDetails(PlatformSupport.federated), + platformWindows: const PlatformDetails(PlatformSupport.federated), })); expect( - pluginSupportsPlatform(kPlatformAndroid, plugin, + pluginSupportsPlatform(platformAndroid, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformAndroid, plugin, + pluginSupportsPlatform(platformAndroid, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformIos, plugin, + pluginSupportsPlatform(platformIOS, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformIos, plugin, + pluginSupportsPlatform(platformIOS, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformLinux, plugin, + pluginSupportsPlatform(platformLinux, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformLinux, plugin, + pluginSupportsPlatform(platformLinux, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformMacos, plugin, + pluginSupportsPlatform(platformMacOS, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformMacos, plugin, + pluginSupportsPlatform(platformMacOS, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformWeb, plugin, + pluginSupportsPlatform(platformWeb, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformWeb, plugin, + pluginSupportsPlatform(platformWeb, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, requiredMode: PlatformSupport.inline), isFalse); }); @@ -199,16 +199,16 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }, )); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, variant: platformVariantWin32), isTrue); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, variant: platformVariantWinUwp), isFalse); }); @@ -217,18 +217,18 @@ void main() { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformWindows: const PlatformDetails( + platformWindows: const PlatformDetails( PlatformSupport.federated, variants: [platformVariantWin32, platformVariantWinUwp], ), })); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, variant: platformVariantWin32), isTrue); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, variant: platformVariantWinUwp), isTrue); }); @@ -237,18 +237,18 @@ void main() { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformWindows: const PlatformDetails( + platformWindows: const PlatformDetails( PlatformSupport.federated, variants: [platformVariantWin32], ), })); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, variant: platformVariantWin32), isTrue); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, variant: platformVariantWinUwp), isFalse); }); @@ -258,17 +258,17 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + platformWindows: const PlatformDetails(PlatformSupport.federated, variants: [platformVariantWinUwp]), }, )); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, variant: platformVariantWin32), isFalse); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, variant: platformVariantWinUwp), isTrue); }); @@ -280,11 +280,11 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, )); - expect(pluginHasNativeCodeForPlatform(kPlatformWeb, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(platformWeb, plugin), isFalse); }); test('returns false for a native-only plugin', () async { @@ -292,15 +292,15 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }, )); - expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isTrue); }); test('returns true for a native+Dart plugin', () async { @@ -308,18 +308,18 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + platformLinux: const PlatformDetails(PlatformSupport.inline, hasNativeCode: true, hasDartCode: true), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + platformMacOS: const PlatformDetails(PlatformSupport.inline, hasNativeCode: true, hasDartCode: true), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + platformWindows: const PlatformDetails(PlatformSupport.inline, hasNativeCode: true, hasDartCode: true), }, )); - expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isTrue); }); test('returns false for a Dart-only plugin', () async { @@ -327,18 +327,18 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + platformLinux: const PlatformDetails(PlatformSupport.inline, hasNativeCode: false, hasDartCode: true), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + platformMacOS: const PlatformDetails(PlatformSupport.inline, hasNativeCode: false, hasDartCode: true), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + platformWindows: const PlatformDetails(PlatformSupport.inline, hasNativeCode: false, hasDartCode: true), }, )); - expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isFalse); - expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isFalse); - expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isFalse); }); }); } diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 3c93d8bfe10..9372c571b6f 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -17,7 +17,7 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; -const String _fakeIosDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; +const String _fakeIOSDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; const String _fakeAndroidDevice = 'emulator-1234'; void main() { @@ -42,7 +42,7 @@ void main() { }); void setMockFlutterDevicesOutput({ - bool hasIosDevice = true, + bool hasIOSDevice = true, bool hasAndroidDevice = true, bool includeBanner = false, }) { @@ -54,7 +54,7 @@ void main() { ╚════════════════════════════════════════════════════════════════════════════╝ '''; final List devices = [ - if (hasIosDevice) '{"id": "$_fakeIosDevice", "targetPlatform": "ios"}', + if (hasIOSDevice) '{"id": "$_fakeIOSDevice", "targetPlatform": "ios"}', if (hasAndroidDevice) '{"id": "$_fakeAndroidDevice", "targetPlatform": "android-x86"}', ]; @@ -104,7 +104,7 @@ void main() { }); test('fails for iOS if no iOS devices are present', () async { - setMockFlutterDevicesOutput(hasIosDevice: false); + setMockFlutterDevicesOutput(hasIOSDevice: false); Error? commandError; final List output = await runCapturingPrint( @@ -130,7 +130,7 @@ void main() { 'example/integration_test/foo_test.dart', ], platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -195,8 +195,8 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -225,7 +225,7 @@ void main() { const [ 'drive', '-d', - _fakeIosDevice, + _fakeIOSDevice, '--driver', 'test_driver/plugin_test.dart', '--target', @@ -245,8 +245,8 @@ void main() { 'example/test_driver/plugin_test.dart', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -278,8 +278,8 @@ void main() { 'example/lib/main.dart', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -314,8 +314,8 @@ void main() { 'example/integration_test/ignore_me.dart', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -344,7 +344,7 @@ void main() { const [ 'drive', '-d', - _fakeIosDevice, + _fakeIOSDevice, '--driver', 'test_driver/integration_test.dart', '--target', @@ -356,7 +356,7 @@ void main() { const [ 'drive', '-d', - _fakeIosDevice, + _fakeIOSDevice, '--driver', 'test_driver/integration_test.dart', '--target', @@ -400,7 +400,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }, ); @@ -473,7 +473,7 @@ void main() { 'example/macos/macos.swift', ], platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -544,7 +544,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -593,7 +593,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -670,7 +670,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }, ); @@ -717,7 +717,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + platformWindows: const PlatformDetails(PlatformSupport.inline, variants: [platformVariantWinUwp]), }, ); @@ -751,7 +751,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), }, ); @@ -801,7 +801,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -834,7 +834,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -889,8 +889,8 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -914,7 +914,7 @@ void main() { const [ 'drive', '-d', - _fakeIosDevice, + _fakeIOSDevice, '--enable-experiment=exp1', '--driver', 'test_driver/plugin_test.dart', @@ -931,7 +931,7 @@ void main() { packagesDir, examples: [], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -963,7 +963,7 @@ void main() { 'example/integration_test/foo_test.dart', ], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -995,7 +995,7 @@ void main() { 'example/test_driver/integration_test.dart', ], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -1031,7 +1031,7 @@ void main() { 'example/integration_test/foo_test.dart', ], platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index 5670a64f30d..a9ad510f7ee 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -44,7 +44,7 @@ void main() { createFakePlugin('plugin1', packagesDir, extraFiles: [ 'example/android/gradlew', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }); final Directory androidDir = @@ -75,7 +75,7 @@ void main() { test('fails if gradlew is missing', () async { createFakePlugin('plugin1', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }); Error? commandError; @@ -97,7 +97,7 @@ void main() { test('fails if linting finds issues', () async { createFakePlugin('plugin1', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }); processRunner.mockProcessesForExecutable['gradlew'] = [ @@ -139,7 +139,7 @@ void main() { test('skips non-inline plugins', () async { createFakePlugin('plugin1', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.federated) + platformAndroid: const PlatformDetails(PlatformSupport.federated) }); final List output = diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 697cbd4b84d..1069a68107c 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -207,7 +207,7 @@ void main() { test('reports skips with no tests', () async { final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -241,7 +241,7 @@ void main() { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final List output = await runCapturingPrint(runner, @@ -258,7 +258,7 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.federated) + platformIOS: const PlatformDetails(PlatformSupport.federated) }); final List output = await runCapturingPrint(runner, @@ -275,7 +275,7 @@ void main() { test('running with correct destination', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline) + platformIOS: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -313,7 +313,7 @@ void main() { () async { final Directory pluginDirectory = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline) + platformIOS: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -366,7 +366,7 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + platformMacOS: const PlatformDetails(PlatformSupport.federated), }); final List output = @@ -385,7 +385,7 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -421,7 +421,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -451,7 +451,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -481,7 +481,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -517,7 +517,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -543,7 +543,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -582,7 +582,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -617,7 +617,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -649,7 +649,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/app/src/test/example_test.java', @@ -681,7 +681,7 @@ void main() { 'plugin1', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -693,7 +693,7 @@ void main() { 'plugin2', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -724,7 +724,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -765,7 +765,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -808,7 +808,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -861,7 +861,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, ); @@ -886,7 +886,7 @@ void main() { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); _createFakeCMakeCache(pluginDirectory, mockPlatform); @@ -925,7 +925,7 @@ void main() { 'example/$debugTestBinaryRelativePath', 'example/$releaseTestBinaryRelativePath' ], platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); _createFakeCMakeCache(pluginDirectory, mockPlatform); @@ -958,7 +958,7 @@ void main() { test('fails if CMake has not been configured', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); Error? commandError; @@ -986,7 +986,7 @@ void main() { final Directory pluginDirectory = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); _createFakeCMakeCache(pluginDirectory, mockPlatform); @@ -1021,7 +1021,7 @@ void main() { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); _createFakeCMakeCache(pluginDirectory, mockPlatform); @@ -1062,7 +1062,7 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -1090,7 +1090,7 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1126,7 +1126,7 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1162,7 +1162,7 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1198,7 +1198,7 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1238,7 +1238,7 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1283,9 +1283,9 @@ void main() { 'android/src/test/example_test.java', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -1337,7 +1337,7 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1374,7 +1374,7 @@ void main() { test('runs only iOS for a iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline) + platformIOS: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -1439,9 +1439,9 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + platformMacOS: const PlatformDetails(PlatformSupport.inline, hasDartCode: true, hasNativeCode: false), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + platformWindows: const PlatformDetails(PlatformSupport.inline, hasDartCode: true, hasNativeCode: false), }, ); @@ -1470,8 +1470,8 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', @@ -1526,8 +1526,8 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', @@ -1625,7 +1625,7 @@ void main() { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); _createFakeCMakeCache(pluginDirectory, mockPlatform); @@ -1664,7 +1664,7 @@ void main() { 'example/$debugTestBinaryRelativePath', 'example/$releaseTestBinaryRelativePath' ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); _createFakeCMakeCache(pluginDirectory, mockPlatform); @@ -1696,7 +1696,7 @@ void main() { test('fails if CMake has not been configured', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); Error? commandError; @@ -1724,7 +1724,7 @@ void main() { final Directory pluginDirectory = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); _createFakeCMakeCache(pluginDirectory, mockPlatform); @@ -1759,7 +1759,7 @@ void main() { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); _createFakeCMakeCache(pluginDirectory, mockPlatform); diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index f8aca38d347..9bcd8d1ae67 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -181,7 +181,7 @@ void main() { packagesDir, extraFiles: ['test/empty_test.dart'], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 9abb34bef35..91d21c1d9bb 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -229,24 +229,24 @@ String _pluginPlatformSection( ' $platform:', ]; switch (platform) { - case kPlatformAndroid: + case platformAndroid: lines.add(' package: io.flutter.plugins.fake'); continue nativeByDefault; nativeByDefault: - case kPlatformIos: - case kPlatformLinux: - case kPlatformMacos: - case kPlatformWindows: + case platformIOS: + case platformLinux: + case platformMacOS: + case platformWindows: if (support.hasNativeCode) { final String className = - platform == kPlatformIos ? 'FLTFakePlugin' : 'FakePlugin'; + platform == platformIOS ? 'FLTFakePlugin' : 'FakePlugin'; lines.add(' pluginClass: $className'); } if (support.hasDartCode) { lines.add(' dartPluginClass: FakeDartPlugin'); } break; - case kPlatformWeb: + case platformWeb: lines.addAll([ ' pluginClass: FakePlugin', ' fileName: ${packageName}_web.dart', diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index 10008ae33a1..097d4d21cb1 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -58,7 +58,7 @@ void main() { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final List output = @@ -71,7 +71,7 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.federated) + platformIOS: const PlatformDetails(PlatformSupport.federated) }); final List output = @@ -84,7 +84,7 @@ void main() { test('runs for iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline) + platformIOS: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -127,7 +127,7 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline) + platformIOS: const PlatformDetails(PlatformSupport.inline) }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -173,7 +173,7 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + platformMacOS: const PlatformDetails(PlatformSupport.federated), }); final List output = await runCapturingPrint( @@ -187,7 +187,7 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -224,7 +224,7 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -254,8 +254,8 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -314,7 +314,7 @@ void main() { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -355,7 +355,7 @@ void main() { test('runs only iOS for a iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline) + platformIOS: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = From 159f6d87b7a9880a85d7f2b27ac3ca57c482c0d2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 9 Mar 2022 11:35:23 -0500 Subject: [PATCH 167/249] [ci] Run analysis with older versions of Flutter (#5000) --- script/tool/CHANGELOG.md | 3 + script/tool/lib/src/analyze_command.dart | 49 ++++--------- .../src/common/package_looping_command.dart | 41 ++++++++++- .../tool/lib/src/common/plugin_command.dart | 3 + script/tool/test/analyze_command_test.dart | 8 +-- .../common/package_looping_command_test.dart | 28 ++++++++ .../tool/test/common/plugin_command_test.dart | 31 +++++++- script/tool/test/util.dart | 70 ++++++++++++++----- 8 files changed, 172 insertions(+), 61 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 8f2807f0dd0..8e5ae210ce0 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -19,6 +19,9 @@ `flutter` behavior. - Validates `default_package` entries in plugins. - Removes `allow-warnings` from the `podspecs` command. +- Adds `skip-if-not-supporting-flutter-version` to allow running tests using a + version of Flutter that not all packages support. (E.g., to allow for running + some tests against old versions of Flutter to help avoid accidental breakage.) ## 0.7.3 diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index faad7f4736e..d8b17bfd562 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -10,12 +10,9 @@ import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; -import 'common/plugin_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; -const int _exitPackagesGetFailed = 3; - /// A command to run Dart analysis on packages. class AnalyzeCommand extends PackageLoopingCommand { /// Creates a analysis command instance. @@ -84,41 +81,8 @@ class AnalyzeCommand extends PackageLoopingCommand { return false; } - /// Ensures that the dependent packages have been fetched for all packages - /// (including their sub-packages) that will be analyzed. - Future _runPackagesGetOnTargetPackages() async { - final List packageDirectories = - await getTargetPackagesAndSubpackages() - .map((PackageEnumerationEntry entry) => entry.package.directory) - .toList(); - final Set packagePaths = - packageDirectories.map((Directory dir) => dir.path).toSet(); - packageDirectories.removeWhere((Directory directory) { - // Remove the 'example' subdirectories; 'flutter packages get' - // automatically runs 'pub get' there as part of handling the parent - // directory. - return directory.basename == 'example' && - packagePaths.contains(directory.parent.path); - }); - for (final Directory package in packageDirectories) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, ['packages', 'get'], - workingDir: package); - if (exitCode != 0) { - return false; - } - } - return true; - } - @override Future initializeRun() async { - print('Fetching dependencies...'); - if (!await _runPackagesGetOnTargetPackages()) { - printError('Unable to get dependencies.'); - throw ToolExit(_exitPackagesGetFailed); - } - _allowedCustomAnalysisDirectories = getStringListArg(_customAnalysisFlag).expand((String item) { if (item.endsWith('.yaml')) { @@ -138,6 +102,19 @@ class AnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { + // For non-example packages, fetch dependencies. 'flutter packages get' + // automatically runs 'pub get' in examples as part of handling the parent + // directory, which is guaranteed to come first in the package enumeration. + if (package.directory.basename != 'example' || + !RepositoryPackage(package.directory.parent).pubspecFile.existsSync()) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, ['packages', 'get'], + workingDir: package.directory); + if (exitCode != 0) { + return PackageResult.fail(['Unable to get dependencies']); + } + } + if (_hasUnexpecetdAnalysisOptions(package)) { return PackageResult.fail(['Unexpected local analysis options']); } diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index bfee71a0f4c..b75aaa4a4a4 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -9,6 +9,8 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; import 'core.dart'; import 'plugin_command.dart'; @@ -75,7 +77,16 @@ abstract class PackageLoopingCommand extends PluginCommand { Platform platform = const LocalPlatform(), GitDir? gitDir, }) : super(packagesDir, - processRunner: processRunner, platform: platform, gitDir: gitDir); + processRunner: processRunner, platform: platform, gitDir: gitDir) { + argParser.addOption( + _skipByFlutterVersionArg, + help: 'Skip any packages that require a Flutter version newer than ' + 'the provided version.', + ); + } + + static const String _skipByFlutterVersionArg = + 'skip-if-not-supporting-flutter-version'; /// Packages that had at least one [logWarning] call. final Set _packagesWithWarnings = @@ -219,6 +230,11 @@ abstract class PackageLoopingCommand extends PluginCommand { _otherWarningCount = 0; _currentPackageEntry = null; + final String minFlutterVersionArg = getStringArg(_skipByFlutterVersionArg); + final Version? minFlutterVersion = minFlutterVersionArg.isEmpty + ? null + : Version.parse(minFlutterVersionArg); + final DateTime runStart = DateTime.now(); await initializeRun(); @@ -242,7 +258,8 @@ abstract class PackageLoopingCommand extends PluginCommand { PackageResult result; try { - result = await runForPackage(entry.package); + result = await _runForPackageIfSupported(entry.package, + minFlutterVersion: minFlutterVersion); } catch (e, stack) { printError(e.toString()); printError(stack.toString()); @@ -285,6 +302,26 @@ abstract class PackageLoopingCommand extends PluginCommand { return true; } + /// Returns the result of running [runForPackage] if the package is supported + /// by any run constraints, or a skip result if it is not. + Future _runForPackageIfSupported( + RepositoryPackage package, { + Version? minFlutterVersion, + }) async { + if (minFlutterVersion != null) { + final Pubspec pubspec = package.parsePubspec(); + final VersionConstraint? flutterConstraint = + pubspec.environment?['flutter']; + if (flutterConstraint != null && + !flutterConstraint.allows(minFlutterVersion)) { + return PackageResult.skip( + 'Does not support Flutter ${minFlutterVersion.toString()}'); + } + } + + return await runForPackage(package); + } + void _printSuccess(String message) { captureOutput ? print(message) : printSuccess(message); } diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 18466356822..fcc87c94ef9 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -409,6 +409,9 @@ abstract class PluginCommand extends Command { /// /// By default, packages excluded via --exclude will not be in the stream, but /// they can be included by passing false for [filterExcluded]. + /// + /// Subpackages are guaranteed to be after the containing package in the + /// stream. Stream getTargetPackagesAndSubpackages( {bool filterExcluded = true}) async* { await for (final PackageEnumerationEntry plugin diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 878facd83c0..087e5ada37b 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -47,10 +47,10 @@ void main() { orderedEquals([ ProcessCall( 'flutter', const ['packages', 'get'], plugin1Dir.path), - ProcessCall( - 'flutter', const ['packages', 'get'], plugin2Dir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['packages', 'get'], plugin2Dir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], plugin2Dir.path), ])); @@ -82,10 +82,10 @@ void main() { orderedEquals([ ProcessCall( 'flutter', const ['packages', 'get'], plugin1Dir.path), - ProcessCall( - 'flutter', const ['packages', 'get'], plugin2Dir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['packages', 'get'], plugin2Dir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], plugin2Dir.path), ])); diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 6e46a3330cc..ea02bd4ea38 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -236,6 +236,34 @@ void main() { expect(command.checkedPackages, isNot(contains(excluded.childDirectory('example2').path))); }); + + test('skips unsupported versions when requested', () async { + final Directory excluded = createFakePlugin('a_plugin', packagesDir, + flutterConstraint: '>=2.10.0'); + final Directory included = createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(includeSubpackages: true, hasLongOutput: false); + final List output = await runCommand(command, arguments: [ + '--skip-if-not-supporting-flutter-version=2.5.0' + ]); + + expect( + command.checkedPackages, + unorderedEquals([ + included.path, + included.childDirectory('example').path, + ])); + expect(command.checkedPackages, isNot(contains(excluded.path))); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for a_package...$_endColor', + '${_startHeadingColor}Running for a_plugin...$_endColor', + '$_startSkipColor SKIPPING: Does not support Flutter 2.5.0$_endColor', + ])); + }); }); group('output', () { diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 28a03c61d59..782ea7267ae 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -253,6 +253,29 @@ packages/plugin1/plugin1/plugin1.dart unorderedEquals([platformInterfacePackage.path])); }); + test('returns subpackages after the enclosing package', () async { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + includeSubpackages: true, + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'subpackage testing'); + localRunner.addCommand(localCommand); + + final Directory package = createFakePackage('apackage', packagesDir); + + await runCapturingPrint(localRunner, ['sample']); + expect( + localCommand.plugins, + containsAllInOrder([ + package.path, + package.childDirectory('example').path, + ])); + }); + group('conflicting package selection', () { test('does not allow --packages with --run-on-changed-packages', () async { @@ -893,11 +916,14 @@ class SamplePluginCommand extends PluginCommand { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), GitDir? gitDir, + this.includeSubpackages = false, }) : super(packagesDir, processRunner: processRunner, platform: platform, gitDir: gitDir); final List plugins = []; + final bool includeSubpackages; + @override final String name = 'sample'; @@ -906,7 +932,10 @@ class SamplePluginCommand extends PluginCommand { @override Future run() async { - await for (final PackageEnumerationEntry entry in getTargetPackages()) { + final Stream packages = includeSubpackages + ? getTargetPackagesAndSubpackages() + : getTargetPackages(); + await for (final PackageEnumerationEntry entry in packages) { plugins.add(entry.package.path); } } diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 91d21c1d9bb..6f7d86e054e 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -85,12 +85,14 @@ Directory createFakePlugin( Map platformSupport = const {}, String? version = '0.0.1', + String flutterConstraint = '>=2.5.0', }) { final Directory pluginDirectory = createFakePackage(name, parentDirectory, isFlutter: true, examples: examples, extraFiles: extraFiles, - version: version); + version: version, + flutterConstraint: flutterConstraint); createFakePubspec( pluginDirectory, @@ -99,6 +101,7 @@ Directory createFakePlugin( isPlugin: true, platformSupport: platformSupport, version: version, + flutterConstraint: flutterConstraint, ); return pluginDirectory; @@ -116,12 +119,16 @@ Directory createFakePackage( List extraFiles = const [], bool isFlutter = false, String? version = '0.0.1', + String flutterConstraint = '>=2.5.0', }) { final Directory packageDirectory = parentDirectory.childDirectory(name); packageDirectory.createSync(recursive: true); createFakePubspec(packageDirectory, - name: name, isFlutter: isFlutter, version: version); + name: name, + isFlutter: isFlutter, + version: version, + flutterConstraint: flutterConstraint); createFakeCHANGELOG(packageDirectory, ''' ## $version * Some changes. @@ -132,7 +139,10 @@ Directory createFakePackage( final Directory exampleDir = packageDirectory.childDirectory(examples.first) ..createSync(); createFakePubspec(exampleDir, - name: '${name}_example', isFlutter: isFlutter, publishTo: 'none'); + name: '${name}_example', + isFlutter: isFlutter, + publishTo: 'none', + flutterConstraint: flutterConstraint); } else if (examples.isNotEmpty) { final Directory exampleDir = packageDirectory.childDirectory('example') ..createSync(); @@ -140,7 +150,10 @@ Directory createFakePackage( final Directory currentExample = exampleDir.childDirectory(example) ..createSync(); createFakePubspec(currentExample, - name: example, isFlutter: isFlutter, publishTo: 'none'); + name: example, + isFlutter: isFlutter, + publishTo: 'none', + flutterConstraint: flutterConstraint); } } @@ -172,40 +185,61 @@ void createFakePubspec( const {}, String publishTo = 'http://no_pub_server.com', String? version, + String dartConstraint = '>=2.0.0 <3.0.0', + String flutterConstraint = '>=2.5.0', }) { isPlugin |= platformSupport.isNotEmpty; - parent.childFile('pubspec.yaml').createSync(); - String yaml = ''' -name: $name + + String environmentSection = ''' +environment: + sdk: "$dartConstraint" '''; + String dependenciesSection = ''' +dependencies: +'''; + String pluginSection = ''; + + // Add Flutter-specific entries if requested. if (isFlutter) { + environmentSection += ''' + flutter: "$flutterConstraint" +'''; + dependenciesSection += ''' + flutter: + sdk: flutter +'''; + if (isPlugin) { - yaml += ''' + pluginSection += ''' flutter: plugin: platforms: '''; for (final MapEntry platform in platformSupport.entries) { - yaml += _pluginPlatformSection(platform.key, platform.value, name); + pluginSection += + _pluginPlatformSection(platform.key, platform.value, name); } } - yaml += ''' -dependencies: - flutter: - sdk: flutter -'''; } - if (version != null) { - yaml += ''' -version: $version + + String yaml = ''' +name: $name +${(version != null) ? 'version: $version' : ''} + +$environmentSection + +$dependenciesSection + +$pluginSection '''; - } + if (publishTo.isNotEmpty) { yaml += ''' publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being published by a broken test. '''; } + parent.childFile('pubspec.yaml').createSync(); parent.childFile('pubspec.yaml').writeAsStringSync(yaml); } From 1bbfb608161595d229a98bf6eded4bf8d4534f8f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 11 Mar 2022 15:15:26 -0500 Subject: [PATCH 168/249] [flutter_plugin_tools] Fix subpackage analysis (#5027) --- script/tool/CHANGELOG.md | 5 +++ script/tool/lib/src/analyze_command.dart | 29 +++++++++------ .../tool/lib/src/common/plugin_command.dart | 20 +++++++---- .../lib/src/make_deps_path_based_command.dart | 4 +++ script/tool/pubspec.yaml | 2 +- script/tool/test/analyze_command_test.dart | 25 +++++++++++++ .../make_deps_path_based_command_test.dart | 36 +++++++++++++++++++ 7 files changed, 103 insertions(+), 18 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 8e5ae210ce0..35786f4a8b1 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.8.1 + +- Fixes an `analyze` regression in 0.8.0 with packages that have non-`example` + sub-packages. + ## 0.8.0 - Ensures that `firebase-test-lab` runs include an `integration_test` runner. diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index d8b17bfd562..824d766f4eb 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -102,16 +102,25 @@ class AnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - // For non-example packages, fetch dependencies. 'flutter packages get' - // automatically runs 'pub get' in examples as part of handling the parent - // directory, which is guaranteed to come first in the package enumeration. - if (package.directory.basename != 'example' || - !RepositoryPackage(package.directory.parent).pubspecFile.existsSync()) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, ['packages', 'get'], - workingDir: package.directory); - if (exitCode != 0) { - return PackageResult.fail(['Unable to get dependencies']); + // Analysis runs over the package and all subpackages, so all of them need + // `flutter packages get` run before analyzing. `example` packages can be + // skipped since 'flutter packages get' automatically runs `pub get` in + // examples as part of handling the parent directory. + final List packagesToGet = [ + package, + ...await getSubpackages(package).toList(), + ]; + for (final RepositoryPackage packageToGet in packagesToGet) { + if (packageToGet.directory.basename != 'example' || + !RepositoryPackage(packageToGet.directory.parent) + .pubspecFile + .existsSync()) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, ['packages', 'get'], + workingDir: packageToGet.directory); + if (exitCode != 0) { + return PackageResult.fail(['Unable to get dependencies']); + } } } diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index fcc87c94ef9..d2100289966 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -417,16 +417,22 @@ abstract class PluginCommand extends Command { await for (final PackageEnumerationEntry plugin in getTargetPackages(filterExcluded: filterExcluded)) { yield plugin; - yield* plugin.package.directory - .list(recursive: true, followLinks: false) - .where(_isDartPackage) - .map((FileSystemEntity directory) => PackageEnumerationEntry( - // _isDartPackage guarantees that this cast is valid. - RepositoryPackage(directory as Directory), - excluded: plugin.excluded)); + yield* getSubpackages(plugin.package).map((RepositoryPackage package) => + PackageEnumerationEntry(package, excluded: plugin.excluded)); } } + /// Returns all Dart package folders (e.g., examples) under the given package. + Stream getSubpackages(RepositoryPackage package, + {bool filterExcluded = true}) async* { + yield* package.directory + .list(recursive: true, followLinks: false) + .where(_isDartPackage) + .map((FileSystemEntity directory) => + // _isDartPackage guarantees that this cast is valid. + RepositoryPackage(directory as Directory)); + } + /// Returns the files contained, recursively, within the packages /// involved in this command execution. Stream getFiles() { diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index 5ee42848a81..c09060310e9 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -207,6 +207,10 @@ dependency_overrides: allComponents.contains('example')) { continue; } + if (!allComponents.contains(packagesDir.basename)) { + print(' Skipping $changedPath; not in packages directory.'); + continue; + } final RepositoryPackage package = RepositoryPackage(packagesDir.fileSystem.file(changedPath).parent); // Ignored deleted packages, as they won't be published. diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 9ca5e2b7758..93a1a87ca33 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.0 +version: 0.8.1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 087e5ada37b..56f4ab9576c 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -71,6 +71,31 @@ void main() { ])); }); + test('runs flutter pub get for non-example subpackages', () async { + final Directory mainPackageDir = createFakePackage('a', packagesDir); + final Directory otherPackages = + mainPackageDir.childDirectory('other_packages'); + final Directory subpackage1 = + createFakePackage('subpackage1', otherPackages); + final Directory subpackage2 = + createFakePackage('subpackage2', otherPackages); + + await runCapturingPrint(runner, ['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', const ['packages', 'get'], + mainPackageDir.path), + ProcessCall( + 'flutter', const ['packages', 'get'], subpackage1.path), + ProcessCall( + 'flutter', const ['packages', 'get'], subpackage2.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + mainPackageDir.path), + ])); + }); + test('don\'t elide a non-contained example package', () async { final Directory plugin1Dir = createFakePlugin('a', packagesDir); final Directory plugin2Dir = createFakePlugin('example', packagesDir); diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index bdd2139b237..2021f24079e 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -323,5 +323,41 @@ void main() { ]), ); }); + + test('skips anything outside of the packages directory', () async { + final Directory toolDir = packagesDir.parent.childDirectory('tool'); + const String newVersion = '1.1.0'; + final Directory package = createFakePackage( + 'flutter_plugin_tools', toolDir, + version: newVersion); + + // Simulate a minor version change so it would be a target. + final File pubspecFile = RepositoryPackage(package).pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains( + 'Skipping /tool/flutter_plugin_tools/pubspec.yaml; not in packages directory.'), + contains('No target dependencies'), + ]), + ); + }); }); } From 2e5c3fbcb13596da62843be2a8c56b7afa68e961 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 15 Mar 2022 17:15:07 -0400 Subject: [PATCH 169/249] [flutter_plugin_tools] packages get -> pub get (#5046) --- script/tool/CHANGELOG.md | 4 +++ script/tool/lib/src/analyze_command.dart | 4 +-- script/tool/test/analyze_command_test.dart | 35 ++++++++----------- .../test/build_examples_command_test.dart | 4 +-- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 35786f4a8b1..7e950352fac 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +- Switches from deprecated `flutter packages` alias to `flutter pub`. + ## 0.8.1 - Fixes an `analyze` regression in 0.8.0 with packages that have non-`example` diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 824d766f4eb..1cd85af076f 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -103,7 +103,7 @@ class AnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { // Analysis runs over the package and all subpackages, so all of them need - // `flutter packages get` run before analyzing. `example` packages can be + // `flutter pub get` run before analyzing. `example` packages can be // skipped since 'flutter packages get' automatically runs `pub get` in // examples as part of handling the parent directory. final List packagesToGet = [ @@ -116,7 +116,7 @@ class AnalyzeCommand extends PackageLoopingCommand { .pubspecFile .existsSync()) { final int exitCode = await processRunner.runAndStream( - flutterCommand, ['packages', 'get'], + flutterCommand, ['pub', 'get'], workingDir: packageToGet.directory); if (exitCode != 0) { return PackageResult.fail(['Unable to get dependencies']); diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 56f4ab9576c..98fb9382a3a 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -45,12 +45,10 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin1Dir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], plugin1Dir.path), - ProcessCall( - 'flutter', const ['packages', 'get'], plugin2Dir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin2Dir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], plugin2Dir.path), ])); @@ -64,8 +62,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin1Dir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], plugin1Dir.path), ])); @@ -85,12 +82,12 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', const ['packages', 'get'], - mainPackageDir.path), ProcessCall( - 'flutter', const ['packages', 'get'], subpackage1.path), + 'flutter', const ['pub', 'get'], mainPackageDir.path), ProcessCall( - 'flutter', const ['packages', 'get'], subpackage2.path), + 'flutter', const ['pub', 'get'], subpackage1.path), + ProcessCall( + 'flutter', const ['pub', 'get'], subpackage2.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], mainPackageDir.path), ])); @@ -105,12 +102,10 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin1Dir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], plugin1Dir.path), - ProcessCall( - 'flutter', const ['packages', 'get'], plugin2Dir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin2Dir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], plugin2Dir.path), ])); @@ -127,7 +122,7 @@ void main() { orderedEquals([ ProcessCall( 'flutter', - const ['packages', 'get'], + const ['pub', 'get'], pluginDir.path, ), ProcessCall( @@ -195,7 +190,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'flutter', const ['packages', 'get'], pluginDir.path), + 'flutter', const ['pub', 'get'], pluginDir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], pluginDir.path), ])); @@ -214,7 +209,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'flutter', const ['packages', 'get'], pluginDir.path), + 'flutter', const ['pub', 'get'], pluginDir.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], pluginDir.path), ])); @@ -232,11 +227,11 @@ void main() { }); }); - test('fails if "packages get" fails', () async { + test('fails if "pub get" fails', () async { createFakePlugin('foo', packagesDir); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) // flutter packages get + MockProcess(exitCode: 1) // flutter pub get ]; Error? commandError; @@ -304,7 +299,7 @@ void main() { orderedEquals([ ProcessCall( 'flutter', - const ['packages', 'get'], + const ['pub', 'get'], pluginDir.path, ), ProcessCall( diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 6d8f0b9d648..29a87907165 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -63,7 +63,7 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess(exitCode: 1) // flutter packages get + MockProcess(exitCode: 1) // flutter pub get ]; Error? commandError; @@ -92,7 +92,7 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess(exitCode: 1) // flutter packages get + MockProcess(exitCode: 1) // flutter pub get ]; Error? commandError; From f0c3b6baee6f1045469ebe2044e8abc59c7e900e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 18 Mar 2022 11:10:25 -0400 Subject: [PATCH 170/249] [flutter_plugin_tool] Add custom-test command (#5058) --- script/tool/CHANGELOG.md | 3 +- script/tool/lib/src/custom_test_command.dart | 77 +++++ script/tool/lib/src/main.dart | 2 + script/tool/pubspec.yaml | 2 +- .../tool/test/custom_test_command_test.dart | 281 ++++++++++++++++++ 5 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 script/tool/lib/src/custom_test_command.dart create mode 100644 script/tool/test/custom_test_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7e950352fac..214eb68b2f2 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.8.2 +- Adds a new `custom-test` command. - Switches from deprecated `flutter packages` alias to `flutter pub`. ## 0.8.1 diff --git a/script/tool/lib/src/custom_test_command.dart b/script/tool/lib/src/custom_test_command.dart new file mode 100644 index 00000000000..1c1dfc068a1 --- /dev/null +++ b/script/tool/lib/src/custom_test_command.dart @@ -0,0 +1,77 @@ +// 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:platform/platform.dart'; + +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +const String _scriptName = 'run_tests.dart'; +const String _legacyScriptName = 'run_tests.sh'; + +/// A command to run custom, package-local tests on packages. +/// +/// This is an escape hatch for adding tests that this tooling doesn't support. +/// It should be used sparingly; prefer instead to add functionality to this +/// tooling to eliminate the need for bespoke tests. +class CustomTestCommand extends PackageLoopingCommand { + /// Creates a custom test command instance. + CustomTestCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform); + + @override + final String name = 'custom-test'; + + @override + final String description = 'Runs package-specific custom tests defined in ' + 'a package\'s tool/$_scriptName file.\n\n' + 'This command requires "dart" to be in your path.'; + + @override + Future runForPackage(RepositoryPackage package) async { + final File script = + package.directory.childDirectory('tool').childFile(_scriptName); + final File legacyScript = package.directory.childFile(_legacyScriptName); + String? customSkipReason; + bool ranTests = false; + + // Run the custom Dart script if presest. + if (script.existsSync()) { + final int exitCode = await processRunner.runAndStream( + 'dart', ['run', 'tool/$_scriptName'], + workingDir: package.directory); + if (exitCode != 0) { + return PackageResult.fail(); + } + ranTests = true; + } + + // Run the legacy script if present. + if (legacyScript.existsSync()) { + if (platform.isWindows) { + customSkipReason = '$_legacyScriptName is not supported on Windows. ' + 'Please migrate to $_scriptName.'; + } else { + final int exitCode = await processRunner.runAndStream( + legacyScript.path, [], + workingDir: package.directory); + if (exitCode != 0) { + return PackageResult.fail(); + } + ranTests = true; + } + } + + if (!ranTests) { + return PackageResult.skip(customSkipReason ?? 'No custom tests'); + } + + return PackageResult.success(); + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 3e8f19b044d..5a71a0a8889 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -12,6 +12,7 @@ import 'analyze_command.dart'; import 'build_examples_command.dart'; import 'common/core.dart'; import 'create_all_plugins_app_command.dart'; +import 'custom_test_command.dart'; import 'drive_examples_command.dart'; import 'federation_safety_check_command.dart'; import 'firebase_test_lab_command.dart'; @@ -50,6 +51,7 @@ void main(List args) { ..addCommand(AnalyzeCommand(packagesDir)) ..addCommand(BuildExamplesCommand(packagesDir)) ..addCommand(CreateAllPluginsAppCommand(packagesDir)) + ..addCommand(CustomTestCommand(packagesDir)) ..addCommand(DriveExamplesCommand(packagesDir)) ..addCommand(FederationSafetyCheckCommand(packagesDir)) ..addCommand(FirebaseTestLabCommand(packagesDir)) diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 93a1a87ca33..f005c565be1 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.1 +version: 0.8.2 dependencies: args: ^2.1.0 diff --git a/script/tool/test/custom_test_command_test.dart b/script/tool/test/custom_test_command_test.dart new file mode 100644 index 00000000000..6a34c2689f6 --- /dev/null +++ b/script/tool/test/custom_test_command_test.dart @@ -0,0 +1,281 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/custom_test_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + + group('posix', () { + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final CustomTestCommand analyzeCommand = CustomTestCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'custom_test_command', 'Test for custom_test_command'); + runner.addCommand(analyzeCommand); + }); + + test('runs both new and legacy when both are present', () async { + final Directory package = + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall(package.childFile('run_tests.sh').path, + const [], package.path), + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('runs when only new is present', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['tool/run_tests.dart']); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('runs when only legacy is present', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['run_tests.sh']); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall(package.childFile('run_tests.sh').path, + const [], package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('skips when neither is present', () async { + createFakePlugin('a_package', packagesDir); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect(processRunner.recordedCalls, isEmpty); + + expect( + output, + containsAllInOrder([ + contains('Skipped 1 package(s)'), + ])); + }); + + test('fails if new fails', () async { + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['custom-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package') + ])); + }); + + test('fails if legacy fails', () async { + final Directory package = + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable[ + package.childFile('run_tests.sh').path] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['custom-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package') + ])); + }); + }); + + group('Windows', () { + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockPlatform = MockPlatform(isWindows: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final CustomTestCommand analyzeCommand = CustomTestCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'custom_test_command', 'Test for custom_test_command'); + runner.addCommand(analyzeCommand); + }); + + test('runs new and skips old when both are present', () async { + final Directory package = + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('runs when only new is present', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['tool/run_tests.dart']); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('skips package when only legacy is present', () async { + createFakePlugin('a_package', packagesDir, + extraFiles: ['run_tests.sh']); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect(processRunner.recordedCalls, isEmpty); + + expect( + output, + containsAllInOrder([ + contains('run_tests.sh is not supported on Windows'), + contains('Skipped 1 package(s)'), + ])); + }); + + test('fails if new fails', () async { + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['custom-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package') + ])); + }); + }); +} From b532e1233badb5ade6ca4e7c32093dc024cf3f74 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Thu, 24 Mar 2022 15:35:09 -0700 Subject: [PATCH 171/249] [tool] Fix typo in `publish-plugin` readme (#5107) --- script/tool/CHANGELOG.md | 4 ++++ script/tool/README.md | 17 ++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 214eb68b2f2..45f7f527e22 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +- Updates `publish-plugin` command documentation. + ## 0.8.2 - Adds a new `custom-test` command. diff --git a/script/tool/README.md b/script/tool/README.md index 265d3868fc3..05be917014f 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -109,11 +109,18 @@ dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packa ### Publish a Release -``sh +**Releases are automated for `flutter/plugins` and `flutter/packages`.** + +The manual procedure described here is _deprecated_, and should only be used when +the automated process fails. Please, read +[Releasing a Plugin or Package](https://github.com/flutter/flutter/wiki/Releasing-a-Plugin-or-Package) +on the Flutter Wiki first. + +```sh cd git checkout -dart run ./script/tool/bin/flutter_plugin_tools.dart publish-plugin --package -`` +dart run ./script/tool/bin/flutter_plugin_tools.dart publish-plugin --packages +``` By default the tool tries to push tags to the `upstream` remote, but some additional settings can be configured. Run `dart run ./script/tool/bin/flutter_plugin_tools.dart @@ -127,10 +134,6 @@ _everything_, including untracked or uncommitted files in version control. directory and refuse to publish if there are any mismatched files with version control present. -Automated publishing is under development. Follow -[flutter/flutter#27258](https://github.com/flutter/flutter/issues/27258) -for updates. - ## Updating the Tool For flutter/plugins, just changing the source here is all that's needed. From b0567c3fcc3f48c5ef833660f4bbfcce7c592fca Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 4 Apr 2022 15:26:13 -0400 Subject: [PATCH 172/249] Add supported OS version tables to READMEs (#5106) --- script/tool/CHANGELOG.md | 1 + .../lib/src/common/repository_package.dart | 9 + script/tool/lib/src/main.dart | 2 + script/tool/lib/src/readme_check_command.dart | 152 ++++++++++ .../test/common/repository_package_test.dart | 4 + .../tool/test/readme_check_command_test.dart | 278 ++++++++++++++++++ 6 files changed, 446 insertions(+) create mode 100644 script/tool/lib/src/readme_check_command.dart create mode 100644 script/tool/test/readme_check_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 45f7f527e22..df76e4819e6 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +- Adds a new `readme-check` command. - Updates `publish-plugin` command documentation. ## 0.8.2 diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index e0c4e4a83bf..72e7f948fe9 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -48,6 +48,9 @@ class RepositoryPackage { /// The package's top-level pubspec.yaml. File get pubspecFile => directory.childFile('pubspec.yaml'); + /// The package's top-level README. + File get readmeFile => directory.childFile('README.md'); + late final Pubspec _parsedPubspec = Pubspec.parse(pubspecFile.readAsStringSync()); @@ -62,6 +65,12 @@ class RepositoryPackage { directory.parent.basename != 'packages' && directory.basename.startsWith(directory.parent.basename); + /// True if this appears to be the app-facing package of a federated plugin, + /// according to repository conventions. + bool get isAppFacing => + directory.parent.basename != 'packages' && + directory.basename == directory.parent.basename; + /// True if this appears to be a platform interface package, according to /// repository conventions. bool get isPlatformInterface => diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 5a71a0a8889..aa1cf300bd2 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -26,6 +26,7 @@ import 'native_test_command.dart'; import 'publish_check_command.dart'; import 'publish_plugin_command.dart'; import 'pubspec_check_command.dart'; +import 'readme_check_command.dart'; import 'test_command.dart'; import 'version_check_command.dart'; import 'xcode_analyze_command.dart'; @@ -65,6 +66,7 @@ void main(List args) { ..addCommand(PublishCheckCommand(packagesDir)) ..addCommand(PublishPluginCommand(packagesDir)) ..addCommand(PubspecCheckCommand(packagesDir)) + ..addCommand(ReadmeCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) ..addCommand(XcodeAnalyzeCommand(packagesDir)); diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart new file mode 100644 index 00000000000..99e271c7338 --- /dev/null +++ b/script/tool/lib/src/readme_check_command.dart @@ -0,0 +1,152 @@ +// 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:git/git.dart'; +import 'package:platform/platform.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to enforce README conventions across the repository. +class ReadmeCheckCommand extends PackageLoopingCommand { + /// Creates an instance of the README check command. + ReadmeCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ); + + // Standardized capitalizations for platforms that a plugin can support. + static const Map _standardPlatformNames = { + 'android': 'Android', + 'ios': 'iOS', + 'linux': 'Linux', + 'macos': 'macOS', + 'web': 'Web', + 'windows': 'Windows', + }; + + @override + final String name = 'readme-check'; + + @override + final String description = + 'Checks that READMEs follow repository conventions.'; + + @override + bool get hasLongOutput => false; + + @override + Future runForPackage(RepositoryPackage package) async { + final File readme = package.readmeFile; + + if (!readme.existsSync()) { + return PackageResult.fail(['Missing README.md']); + } + + final List errors = []; + + final Pubspec pubspec = package.parsePubspec(); + final bool isPlugin = pubspec.flutter?['plugin'] != null; + + if (isPlugin && (!package.isFederated || package.isAppFacing)) { + final String? error = _validateSupportedPlatforms(package, pubspec); + if (error != null) { + errors.add(error); + } + } + + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); + } + + /// Validates that the plugin has a supported platforms table following the + /// expected format, returning an error string if any issues are found. + String? _validateSupportedPlatforms( + RepositoryPackage package, Pubspec pubspec) { + final List contents = package.readmeFile.readAsLinesSync(); + + // Example table following expected format: + // | | Android | iOS | Web | + // |----------------|---------|----------|------------------------| + // | **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | + final int detailsLineNumber = + contents.indexWhere((String line) => line.startsWith('| **Support**')); + if (detailsLineNumber == -1) { + return 'No OS support table found'; + } + final int osLineNumber = detailsLineNumber - 2; + if (osLineNumber < 0 || !contents[osLineNumber].startsWith('|')) { + return 'OS support table does not have the expected header format'; + } + + // Utility method to convert an iterable of strings to a case-insensitive + // sorted, comma-separated string of its elements. + String sortedListString(Iterable entries) { + final List entryList = entries.toList(); + entryList.sort( + (String a, String b) => a.toLowerCase().compareTo(b.toLowerCase())); + return entryList.join(', '); + } + + // Validate that the supported OS lists match. + final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms']; + if (platformsEntry == null) { + logWarning('Plugin not support any platforms'); + return null; + } + final YamlMap platformSupportMaps = platformsEntry as YamlMap; + final Set actuallySupportedPlatform = + platformSupportMaps.keys.toSet().cast(); + final Iterable documentedPlatforms = contents[osLineNumber] + .split('|') + .map((String entry) => entry.trim()) + .where((String entry) => entry.isNotEmpty); + final Set documentedPlatformsLowercase = + documentedPlatforms.map((String entry) => entry.toLowerCase()).toSet(); + if (actuallySupportedPlatform.length != documentedPlatforms.length || + actuallySupportedPlatform + .intersection(documentedPlatformsLowercase) + .length != + actuallySupportedPlatform.length) { + printError(''' +${indentation}OS support table does not match supported platforms: +${indentation * 2}Actual: ${sortedListString(actuallySupportedPlatform)} +${indentation * 2}Documented: ${sortedListString(documentedPlatformsLowercase)} +'''); + return 'Incorrect OS support table'; + } + + // Enforce a standard set of capitalizations for the OS headings. + final Iterable incorrectCapitalizations = documentedPlatforms + .toSet() + .difference(_standardPlatformNames.values.toSet()); + if (incorrectCapitalizations.isNotEmpty) { + final Iterable expectedVersions = incorrectCapitalizations + .map((String name) => _standardPlatformNames[name.toLowerCase()]!); + printError(''' +${indentation}Incorrect OS capitalization: ${sortedListString(incorrectCapitalizations)} +${indentation * 2}Please use standard capitalizations: ${sortedListString(expectedVersions)} +'''); + return 'Incorrect OS support formatting'; + } + + // TODO(stuartmorgan): Add validation that the minimums in the table are + // consistent with what the current implementations require. See + // https://github.com/flutter/flutter/issues/84200 + return null; + } +} diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index 29e3b583212..0a8ea36eb90 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -126,6 +126,7 @@ void main() { test('all return false for a simple plugin', () { final Directory plugin = createFakePlugin('a_plugin', packagesDir); expect(RepositoryPackage(plugin).isFederated, false); + expect(RepositoryPackage(plugin).isAppFacing, false); expect(RepositoryPackage(plugin).isPlatformInterface, false); expect(RepositoryPackage(plugin).isFederated, false); }); @@ -134,6 +135,7 @@ void main() { final Directory plugin = createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isAppFacing, true); expect(RepositoryPackage(plugin).isPlatformInterface, false); expect(RepositoryPackage(plugin).isPlatformImplementation, false); }); @@ -142,6 +144,7 @@ void main() { final Directory plugin = createFakePlugin('a_plugin_platform_interface', packagesDir.childDirectory('a_plugin')); expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isAppFacing, false); expect(RepositoryPackage(plugin).isPlatformInterface, true); expect(RepositoryPackage(plugin).isPlatformImplementation, false); }); @@ -152,6 +155,7 @@ void main() { final Directory plugin = createFakePlugin( 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isAppFacing, false); expect(RepositoryPackage(plugin).isPlatformInterface, false); expect(RepositoryPackage(plugin).isPlatformImplementation, true); }); diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart new file mode 100644 index 00000000000..aec2fa07845 --- /dev/null +++ b/script/tool/test/readme_check_command_test.dart @@ -0,0 +1,278 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.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/readme_check_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final ReadmeCheckCommand command = ReadmeCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'readme_check_command', 'Test for readme_check_command'); + runner.addCommand(command); + }); + + test('fails when README is missing', () async { + createFakePackage('a_package', packagesDir); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing README.md'), + ]), + ); + }); + + group('plugin OS support', () { + test( + 'does not check support table for anything other than app-facing plugin packages', + () async { + const String federatedPluginName = 'a_federated_plugin'; + final Directory federatedDir = + packagesDir.childDirectory(federatedPluginName); + final List packageDirectories = [ + // A non-plugin package. + createFakePackage('a_package', packagesDir), + // Non-app-facing parts of a federated plugin. + createFakePlugin( + '${federatedPluginName}_platform_interface', federatedDir), + createFakePlugin('${federatedPluginName}_android', federatedDir), + ]; + + for (final Directory package in packageDirectories) { + package.childFile('README.md').writeAsStringSync(''' +A very useful package. +'''); + } + + final List output = await runCapturingPrint(runner, [ + 'readme-check', + ]); + + expect( + output, + containsAll([ + contains('Running for a_package...'), + contains('Running for a_federated_plugin_platform_interface...'), + contains('Running for a_federated_plugin_android...'), + contains('No issues found!'), + ]), + ); + }); + + test('fails when non-federated plugin is missing an OS support table', + () async { + final Directory pluginDir = createFakePlugin('a_plugin', packagesDir); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No OS support table found'), + ]), + ); + }); + + test( + 'fails when app-facing part of a federated plugin is missing an OS support table', + () async { + final Directory pluginDir = + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No OS support table found'), + ]), + ); + }); + + test('fails the OS support table is missing the header', () async { + final Directory pluginDir = createFakePlugin('a_plugin', packagesDir); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. + +| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('OS support table does not have the expected header format'), + ]), + ); + }); + + test('fails if the OS support table is missing a supported OS', () async { + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. + +| | Android | iOS | +|----------------|---------|----------| +| **Support** | SDK 21+ | iOS 10+* | +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains(' OS support table does not match supported platforms:\n' + ' Actual: android, ios, web\n' + ' Documented: android, ios'), + contains('Incorrect OS support table'), + ]), + ); + }); + + test('fails if the OS support table lists an extra OS', () async { + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + }, + ); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. + +| | Android | iOS | Web | +|----------------|---------|----------|------------------------| +| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains(' OS support table does not match supported platforms:\n' + ' Actual: android, ios\n' + ' Documented: android, ios, web'), + contains('Incorrect OS support table'), + ]), + ); + }); + + test('fails if the OS support table has unexpected OS formatting', + () async { + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. + +| | android | ios | MacOS | web | +|----------------|---------|----------|-------|------------------------| +| **Support** | SDK 21+ | iOS 10+* | 10.11 | [See `camera_web `][1] | +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains(' Incorrect OS capitalization: android, ios, MacOS, web\n' + ' Please use standard capitalizations: Android, iOS, macOS, Web\n'), + contains('Incorrect OS support formatting'), + ]), + ); + }); + }); +} From 57e6a62dc8f561b931fda91f8c53ffdac22ec5d1 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 15 Apr 2022 14:01:46 -0400 Subject: [PATCH 173/249] [flutter_plugin_tools] Preserve Dart SDK version in all-plugins-app (#5281) Fixes `all-plugins-app` to preserve the original application's Dart SDK version to avoid changing language feature opt-ins that the template may rely on. --- script/tool/CHANGELOG.md | 3 ++ .../src/create_all_plugins_app_command.dart | 27 +++++++++++++---- .../create_all_plugins_app_command_test.dart | 30 ++++++++++++++++--- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index df76e4819e6..6e632c67684 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,9 @@ - Adds a new `readme-check` command. - Updates `publish-plugin` command documentation. +- Fixes `all-plugins-app` to preserve the original application's Dart SDK + version to avoid changing language feature opt-ins that the template may + rely on. ## 0.8.2 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 82f29bd501f..6b44ab78878 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -147,6 +147,19 @@ class CreateAllPluginsAppCommand extends PluginCommand { } Future _genPubspecWithAllPlugins() async { + final RepositoryPackage buildAllApp = RepositoryPackage(appDirectory); + // Read the old pubspec file's Dart SDK version, in order to preserve it + // in the new file. The template sometimes relies on having opted in to + // specific language features via SDK version, so using a different one + // can cause compilation failures. + final Pubspec originalPubspec = buildAllApp.parsePubspec(); + const String dartSdkKey = 'sdk'; + final VersionConstraint dartSdkConstraint = + originalPubspec.environment?[dartSdkKey] ?? + VersionConstraint.compatibleWith( + Version.parse('2.12.0'), + ); + final Map pluginDeps = await _getValidPathDependencies(); final Pubspec pubspec = Pubspec( @@ -154,9 +167,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { description: 'Flutter app containing all 1st party plugins.', version: Version.parse('1.0.0+1'), environment: { - 'sdk': VersionConstraint.compatibleWith( - Version.parse('2.12.0'), - ), + dartSdkKey: dartSdkConstraint, }, dependencies: { 'flutter': SdkDependency('flutter'), @@ -166,8 +177,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { }, dependencyOverrides: pluginDeps, ); - final File pubspecFile = appDirectory.childFile('pubspec.yaml'); - pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + buildAllApp.pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); } Future> _getValidPathDependencies() async { @@ -212,7 +222,12 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} for (final MapEntry entry in values.entries) { buffer.writeln(); if (entry.value is VersionConstraint) { - buffer.write(' ${entry.key}: ${entry.value}'); + String value = entry.value.toString(); + // Range constraints require quoting. + if (value.startsWith('>') || value.startsWith('<')) { + value = "'$value'"; + } + buffer.write(' ${entry.key}: $value'); } else if (entry.value is SdkDependency) { final SdkDependency dep = entry.value as SdkDependency; buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 0066cc53f61..917adca020d 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -2,10 +2,15 @@ // 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:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:flutter_plugin_tools/src/create_all_plugins_app_command.dart'; +import 'package:platform/platform.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; import 'util.dart'; @@ -76,14 +81,31 @@ void main() { ])); }); - test('pubspec is compatible with null-safe app code', () async { + test('pubspec preserves existing Dart SDK version', () async { + const String baselineProjectName = 'baseline'; + final Directory baselineProjectDirectory = + testRoot.childDirectory(baselineProjectName); + io.Process.runSync( + getFlutterCommand(const LocalPlatform()), + [ + 'create', + '--template=app', + '--project-name=$baselineProjectName', + baselineProjectDirectory.path, + ], + ); + final Pubspec baselinePubspec = + RepositoryPackage(baselineProjectDirectory).parsePubspec(); + createFakePlugin('plugina', packagesDir); await runCapturingPrint(runner, ['all-plugins-app']); - final String pubspec = - command.appDirectory.childFile('pubspec.yaml').readAsStringSync(); + final Pubspec generatedPubspec = + RepositoryPackage(command.appDirectory).parsePubspec(); - expect(pubspec, contains(RegExp('sdk:\\s*(?:["\']>=|[^])2\\.12\\.'))); + const String dartSdkKey = 'sdk'; + expect(generatedPubspec.environment?[dartSdkKey], + baselinePubspec.environment?[dartSdkKey]); }); test('handles --output-dir', () async { From cc32f688bbb747de24726f38e3601fb73cc47a75 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 21 Apr 2022 16:08:21 -0400 Subject: [PATCH 174/249] [flutter_plugin_tools] Run pub get for custom-test (#5322) When running a Dart test script for `custom-test`, `pub get` needs to be run first in order for it to work in a clean environment such as CI. This will unblock enabling Pigeon's Dart tests in flutter/packages. --- script/tool/CHANGELOG.md | 3 +- script/tool/lib/src/custom_test_command.dart | 13 ++++- script/tool/pubspec.yaml | 2 +- .../tool/test/custom_test_command_test.dart | 47 ++++++++++++++++++- 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 6e632c67684..367f258fbee 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,10 +1,11 @@ -## NEXT +## 0.8.2+1 - Adds a new `readme-check` command. - Updates `publish-plugin` command documentation. - Fixes `all-plugins-app` to preserve the original application's Dart SDK version to avoid changing language feature opt-ins that the template may rely on. +- Fixes `custom-test` to run `pub get` before running Dart test scripts. ## 0.8.2 diff --git a/script/tool/lib/src/custom_test_command.dart b/script/tool/lib/src/custom_test_command.dart index 1c1dfc068a1..cd9ac32606a 100644 --- a/script/tool/lib/src/custom_test_command.dart +++ b/script/tool/lib/src/custom_test_command.dart @@ -43,10 +43,19 @@ class CustomTestCommand extends PackageLoopingCommand { // Run the custom Dart script if presest. if (script.existsSync()) { - final int exitCode = await processRunner.runAndStream( + // Ensure that dependencies are available. + final int pubGetExitCode = await processRunner.runAndStream( + 'dart', ['pub', 'get'], + workingDir: package.directory); + if (pubGetExitCode != 0) { + return PackageResult.fail( + ['Unable to get script dependencies']); + } + + final int testExitCode = await processRunner.runAndStream( 'dart', ['run', 'tool/$_scriptName'], workingDir: package.directory); - if (exitCode != 0) { + if (testExitCode != 0) { return PackageResult.fail(); } ranTests = true; diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index f005c565be1..14b22a16e3f 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.2 +version: 0.8.2+1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/custom_test_command_test.dart b/script/tool/test/custom_test_command_test.dart index 6a34c2689f6..bc30d9a1d2e 100644 --- a/script/tool/test/custom_test_command_test.dart +++ b/script/tool/test/custom_test_command_test.dart @@ -85,6 +85,21 @@ void main() { ])); }); + test('runs pub get before running Dart test script', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['tool/run_tests.dart']); + + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + }); + test('runs when only legacy is present', () async { final Directory package = createFakePlugin('a_package', packagesDir, extraFiles: ['run_tests.sh']); @@ -128,7 +143,8 @@ void main() { ]); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), + MockProcess(exitCode: 0), // pub get + MockProcess(exitCode: 1), // test script ]; Error? commandError; @@ -146,6 +162,32 @@ void main() { ])); }); + test('fails if pub get fails', () async { + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['custom-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to get script dependencies') + ])); + }); + test('fails if legacy fails', () async { final Directory package = createFakePlugin('a_package', packagesDir, extraFiles: [ @@ -260,7 +302,8 @@ void main() { ]); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), + MockProcess(exitCode: 0), // pub get + MockProcess(exitCode: 1), // test script ]; Error? commandError; From 0d2567922b262216d747c5f14e841b2a37f81bf9 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 21 Apr 2022 17:29:09 -0400 Subject: [PATCH 175/249] [various] Replaces manual hashing with Object.hash (#5314) --- script/tool/test/util.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 6f7d86e054e..fab4f39cc10 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -434,8 +434,7 @@ class ProcessCall { } @override - int get hashCode => - (executable.hashCode) ^ (args.hashCode) ^ (workingDir?.hashCode ?? 0); + int get hashCode => Object.hash(executable, args, workingDir); @override String toString() { From 88829b67ac2afe61042187fe9ff958527c0e3922 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 26 Apr 2022 16:39:09 -0400 Subject: [PATCH 176/249] [flutter_plugin_tool] Allow re-pathifying dependencies (#5376) --- script/tool/CHANGELOG.md | 5 ++ .../lib/src/make_deps_path_based_command.dart | 45 ++++++++++++------ .../make_deps_path_based_command_test.dart | 46 +++++++++++++++++++ 3 files changed, 81 insertions(+), 15 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 367f258fbee..bfc0cafa88c 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +- Allows `make-deps-path-based` to skip packages it has alredy rewritten, so + that running multiple times won't fail after the first time. + ## 0.8.2+1 - Adds a new `readme-check` command. diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index c09060310e9..45a4427d321 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -16,6 +16,8 @@ import 'common/plugin_command.dart'; const int _exitPackageNotFound = 3; const int _exitCannotUpdatePubspec = 4; +enum _RewriteOutcome { changed, noChangesNeeded, alreadyChanged } + /// Converts all dependencies on target packages to path-based dependencies. /// /// This is to allow for pre-publish testing of changes that could affect other @@ -48,6 +50,10 @@ class MakeDepsPathBasedCommand extends PluginCommand { static const String _targetDependenciesWithNonBreakingUpdatesArg = 'target-dependencies-with-non-breaking-updates'; + // The comment to add to temporary dependency overrides. + static const String _dependencyOverrideWarningComment = + '# FOR TESTING ONLY. DO NOT MERGE.'; + @override final String name = 'make-deps-path-based'; @@ -73,12 +79,19 @@ class MakeDepsPathBasedCommand extends PluginCommand { final String repoRootPath = (await gitDir).path; for (final File pubspec in await _getAllPubspecs()) { - if (await _addDependencyOverridesIfNecessary( - pubspec, localDependencyPackages)) { - // Print the relative path of the changed pubspec. - final String displayPath = p.posix.joinAll(path - .split(path.relative(pubspec.absolute.path, from: repoRootPath))); - print(' Modified $displayPath'); + final String displayPath = p.posix.joinAll( + path.split(path.relative(pubspec.absolute.path, from: repoRootPath))); + final _RewriteOutcome outcome = await _addDependencyOverridesIfNecessary( + pubspec, localDependencyPackages); + switch (outcome) { + case _RewriteOutcome.changed: + print(' Modified $displayPath'); + break; + case _RewriteOutcome.alreadyChanged: + print(' Skipped $displayPath - Already rewritten'); + break; + case _RewriteOutcome.noChangesNeeded: + break; } } } @@ -125,16 +138,18 @@ class MakeDepsPathBasedCommand extends PluginCommand { /// If [pubspecFile] has any dependencies on packages in [localDependencies], /// adds dependency_overrides entries to redirect them to the local version /// using path-based dependencies. - /// - /// Returns true if any changes were made. - Future _addDependencyOverridesIfNecessary(File pubspecFile, + Future<_RewriteOutcome> _addDependencyOverridesIfNecessary(File pubspecFile, Map localDependencies) async { final String pubspecContents = pubspecFile.readAsStringSync(); final Pubspec pubspec = Pubspec.parse(pubspecContents); - // Fail if there are any dependency overrides already. If support for that - // is needed at some point, it can be added, but currently it's not and - // relying on that makes the logic here much simpler. + // Fail if there are any dependency overrides already, other than ones + // created by this script. If support for that is needed at some point, it + // can be added, but currently it's not and relying on that makes the logic + // here much simpler. if (pubspec.dependencyOverrides.isNotEmpty) { + if (pubspecContents.contains(_dependencyOverrideWarningComment)) { + return _RewriteOutcome.alreadyChanged; + } printError( 'Plugins with dependency overrides are not currently supported.'); throw ToolExit(_exitCannotUpdatePubspec); @@ -158,7 +173,7 @@ class MakeDepsPathBasedCommand extends PluginCommand { String newPubspecContents = pubspecContents + ''' -# FOR TESTING ONLY. DO NOT MERGE. +$_dependencyOverrideWarningComment dependency_overrides: '''; for (final String packageName in packagesToOverride) { @@ -175,9 +190,9 @@ dependency_overrides: '''; } pubspecFile.writeAsStringSync(newPubspecContents); - return true; + return _RewriteOutcome.changed; } - return false; + return _RewriteOutcome.noChangesNeeded; } /// Returns all pubspecs anywhere under the packages directory. diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index 2021f24079e..da241c3d83f 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -144,6 +144,52 @@ void main() { ])); }); + // This test case ensures that running CI using this command on an interim + // PR that itself used this command won't fail on the rewrite step. + test('running a second time no-ops without failing', () async { + final RepositoryPackage simplePackage = RepositoryPackage( + createFakePackage('foo', packagesDir, isFlutter: true)); + final Directory pluginGroup = packagesDir.childDirectory('bar'); + + RepositoryPackage(createFakePackage('bar_platform_interface', pluginGroup, + isFlutter: true)); + final RepositoryPackage pluginImplementation = + RepositoryPackage(createFakePlugin('bar_android', pluginGroup)); + final RepositoryPackage pluginAppFacing = + RepositoryPackage(createFakePlugin('bar', pluginGroup)); + + _addDependencies(simplePackage, [ + 'bar', + 'bar_android', + 'bar_platform_interface', + ]); + _addDependencies(pluginAppFacing, [ + 'bar_platform_interface', + 'bar_android', + ]); + _addDependencies(pluginImplementation, [ + 'bar_platform_interface', + ]); + + await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies=bar,bar_platform_interface' + ]); + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies=bar,bar_platform_interface' + ]); + + expect( + output, + containsAll([ + 'Rewriting references to: bar, bar_platform_interface...', + ' Skipped packages/bar/bar/pubspec.yaml - Already rewritten', + ' Skipped packages/bar/bar_android/pubspec.yaml - Already rewritten', + ' Skipped packages/foo/pubspec.yaml - Already rewritten', + ])); + }); + group('target-dependencies-with-non-breaking-updates', () { test('no-ops for no published changes', () async { final Directory package = createFakePackage('foo', packagesDir); From 4cecb9b264b408bcfc7702afc740140448e31a2e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 28 Apr 2022 12:34:11 -0400 Subject: [PATCH 177/249] [flutter_plugin_tools] Adds update-excerpts command (#5339) --- script/tool/CHANGELOG.md | 5 +- script/tool/README.md | 11 + .../tool/lib/src/license_check_command.dart | 33 +- script/tool/lib/src/main.dart | 2 + .../tool/lib/src/update_excerpts_command.dart | 225 ++++++++++++++ script/tool/pubspec.yaml | 3 +- script/tool/test/format_command_test.dart | 2 +- .../tool/test/license_check_command_test.dart | 45 ++- .../test/update_excerpts_command_test.dart | 284 ++++++++++++++++++ 9 files changed, 604 insertions(+), 6 deletions(-) create mode 100644 script/tool/lib/src/update_excerpts_command.dart create mode 100644 script/tool/test/update_excerpts_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index bfc0cafa88c..82a31154e13 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 0.8.3 +- Adds a new `update-excerpts` command to maintain README files using the + `code-excerpter` package from flutter/site-shared. +- `license-check` now ignores submodules. - Allows `make-deps-path-based` to skip packages it has alredy rewritten, so that running multiple times won't fail after the first time. diff --git a/script/tool/README.md b/script/tool/README.md index 05be917014f..d52ee08fdc3 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -107,6 +107,17 @@ dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages plugin_name ``` +### Update README.md from Example Sources + +`update-excerpts` requires sources that are in a submodule. If you didn't clone +with submodules, you will need to `git submodule update --init --recursive` +before running this command. + +```sh +cd +dart run ./script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages plugin_name +``` + ### Publish a Release **Releases are automated for `flutter/plugins` and `flutter/packages`.** diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index d2c129ff7b4..87e4c8b1486 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:git/git.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; @@ -105,7 +107,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /// Validates that code files have copyright and license blocks. class LicenseCheckCommand extends PluginCommand { /// Creates a new license check command for [packagesDir]. - LicenseCheckCommand(Directory packagesDir) : super(packagesDir); + LicenseCheckCommand(Directory packagesDir, + {Platform platform = const LocalPlatform(), GitDir? gitDir}) + : super(packagesDir, platform: platform, gitDir: gitDir); @override final String name = 'license-check'; @@ -116,7 +120,14 @@ class LicenseCheckCommand extends PluginCommand { @override Future run() async { - final Iterable allFiles = await _getAllFiles(); + // Create a set of absolute paths to submodule directories, with trailing + // separator, to do prefix matching with to test directory inclusion. + final Iterable submodulePaths = (await _getSubmoduleDirectories()) + .map( + (Directory dir) => '${dir.absolute.path}${platform.pathSeparator}'); + + final Iterable allFiles = (await _getAllFiles()).where( + (File file) => !submodulePaths.any(file.absolute.path.startsWith)); final Iterable codeFiles = allFiles.where((File file) => _codeFileExtensions.contains(p.extension(file.path)) && @@ -275,6 +286,24 @@ class LicenseCheckCommand extends PluginCommand { .where((FileSystemEntity entity) => entity is File) .map((FileSystemEntity file) => file as File) .toList(); + + // Returns the directories containing mapped submodules, if any. + Future> _getSubmoduleDirectories() async { + final List submodulePaths = []; + final Directory repoRoot = + packagesDir.fileSystem.directory((await gitDir).path); + final File submoduleSpec = repoRoot.childFile('.gitmodules'); + if (submoduleSpec.existsSync()) { + final RegExp pathLine = RegExp(r'path\s*=\s*(.*)'); + for (final String line in submoduleSpec.readAsLinesSync()) { + final RegExpMatch? match = pathLine.firstMatch(line); + if (match != null) { + submodulePaths.add(repoRoot.childDirectory(match.group(1)!.trim())); + } + } + } + return submodulePaths; + } } enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty } diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index aa1cf300bd2..9c572ee270e 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -28,6 +28,7 @@ import 'publish_plugin_command.dart'; import 'pubspec_check_command.dart'; import 'readme_check_command.dart'; import 'test_command.dart'; +import 'update_excerpts_command.dart'; import 'version_check_command.dart'; import 'xcode_analyze_command.dart'; @@ -68,6 +69,7 @@ void main(List args) { ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(ReadmeCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) + ..addCommand(UpdateExcerptsCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) ..addCommand(XcodeAnalyzeCommand(packagesDir)); diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart new file mode 100644 index 00000000000..320a3c59632 --- /dev/null +++ b/script/tool/lib/src/update_excerpts_command.dart @@ -0,0 +1,225 @@ +// 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:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:git/git.dart'; +import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to update README code excerpts from code files. +class UpdateExcerptsCommand extends PackageLoopingCommand { + /// Creates a excerpt updater command instance. + UpdateExcerptsCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ) { + argParser.addFlag(_failOnChangeFlag, hide: true); + } + + static const String _failOnChangeFlag = 'fail-on-change'; + + static const String _buildRunnerConfigName = 'excerpt'; + // The name of the build_runner configuration file that will be in an example + // directory if the package is set up to use `code-excerpt`. + static const String _buildRunnerConfigFile = + 'build.$_buildRunnerConfigName.yaml'; + + // The relative directory path to put the extracted excerpt yaml files. + static const String _excerptOutputDir = 'excerpts'; + + // The filename to store the pre-modification copy of the pubspec. + static const String _originalPubspecFilename = + 'pubspec.plugin_tools_original.yaml'; + + @override + final String name = 'update-excerpts'; + + @override + final String description = 'Updates code excerpts in README.md files, based ' + 'on code from code files, via code-excerpt'; + + @override + Future runForPackage(RepositoryPackage package) async { + final Iterable configuredExamples = package + .getExamples() + .where((RepositoryPackage example) => + example.directory.childFile(_buildRunnerConfigFile).existsSync()); + + if (configuredExamples.isEmpty) { + return PackageResult.skip( + 'No $_buildRunnerConfigFile found in example(s).'); + } + + final Directory repoRoot = + packagesDir.fileSystem.directory((await gitDir).path); + + for (final RepositoryPackage example in configuredExamples) { + _addSubmoduleDependencies(example, repoRoot: repoRoot); + + try { + // Ensure that dependencies are available. + final int pubGetExitCode = await processRunner.runAndStream( + 'dart', ['pub', 'get'], + workingDir: example.directory); + if (pubGetExitCode != 0) { + return PackageResult.fail( + ['Unable to get script dependencies']); + } + + // Update the excerpts. + if (!await _extractSnippets(example)) { + return PackageResult.fail(['Unable to extract excerpts']); + } + if (!await _injectSnippets(example, targetPackage: package)) { + return PackageResult.fail(['Unable to inject excerpts']); + } + } finally { + // Clean up the pubspec changes and extracted excerpts directory. + _undoPubspecChanges(example); + final Directory excerptDirectory = + example.directory.childDirectory(_excerptOutputDir); + if (excerptDirectory.existsSync()) { + excerptDirectory.deleteSync(recursive: true); + } + } + } + + if (getBoolArg(_failOnChangeFlag)) { + final String? stateError = await _validateRepositoryState(); + if (stateError != null) { + printError('README.md is out of sync with its source excerpts.\n\n' + 'If you edited code in README.md directly, you should instead edit ' + 'the example source files. If you edited source files, run the ' + 'repository tooling\'s "$name" command on this package, and update ' + 'your PR with the resulting changes.'); + return PackageResult.fail([stateError]); + } + } + + return PackageResult.success(); + } + + /// Runs the extraction step to create the excerpt files for the given + /// example, returning true on success. + Future _extractSnippets(RepositoryPackage example) async { + final int exitCode = await processRunner.runAndStream( + 'dart', + [ + 'run', + 'build_runner', + 'build', + '--config', + _buildRunnerConfigName, + '--output', + _excerptOutputDir, + '--delete-conflicting-outputs', + ], + workingDir: example.directory); + return exitCode == 0; + } + + /// Runs the injection step to update [targetPackage]'s README with the latest + /// excerpts from [example], returning true on success. + Future _injectSnippets( + RepositoryPackage example, { + required RepositoryPackage targetPackage, + }) async { + final String relativeReadmePath = + getRelativePosixPath(targetPackage.readmeFile, from: example.directory); + final int exitCode = await processRunner.runAndStream( + 'dart', + [ + 'run', + 'code_excerpt_updater', + '--write-in-place', + '--yaml', + '--no-escape-ng-interpolation', + relativeReadmePath, + ], + workingDir: example.directory); + return exitCode == 0; + } + + /// Adds `code_excerpter` and `code_excerpt_updater` to [package]'s + /// `dev_dependencies` using path-based references to the submodule copies. + /// + /// This is done on the fly rather than being checked in so that: + /// - Just building examples don't require everyone to check out submodules. + /// - Examples can be analyzed/built even on versions of Flutter that these + /// submodules do not support. + void _addSubmoduleDependencies(RepositoryPackage package, + {required Directory repoRoot}) { + final String pubspecContents = package.pubspecFile.readAsStringSync(); + // Save aside a copy of the current pubspec state. This allows restoration + // to the previous state regardless of its git status at the time the script + // ran. + package.directory + .childFile(_originalPubspecFilename) + .writeAsStringSync(pubspecContents); + + // Update the actual pubspec. + final YamlEditor editablePubspec = YamlEditor(pubspecContents); + const String devDependenciesKey = 'dev_dependencies'; + final YamlNode root = editablePubspec.parseAt([]); + // Ensure that there's a `dev_dependencies` entry to update. + if ((root as YamlMap)[devDependenciesKey] == null) { + editablePubspec.update(['dev_dependencies'], YamlMap()); + } + final Set submoduleDependencies = { + 'code_excerpter', + 'code_excerpt_updater', + }; + final String relativeRootPath = + getRelativePosixPath(repoRoot, from: package.directory); + for (final String dependency in submoduleDependencies) { + editablePubspec.update([ + devDependenciesKey, + dependency + ], { + 'path': '$relativeRootPath/site-shared/packages/$dependency' + }); + } + package.pubspecFile.writeAsStringSync(editablePubspec.toString()); + } + + /// Restores the version of the pubspec that was present before running + /// [_addSubmoduleDependencies]. + void _undoPubspecChanges(RepositoryPackage package) { + package.directory + .childFile(_originalPubspecFilename) + .renameSync(package.pubspecFile.path); + } + + /// Checks the git state, returning an error string unless nothing has + /// changed. + Future _validateRepositoryState() async { + final io.ProcessResult modifiedFiles = await processRunner.run( + 'git', + ['ls-files', '--modified'], + workingDir: packagesDir, + logOnError: true, + ); + if (modifiedFiles.exitCode != 0) { + return 'Unable to determine local file state'; + } + + final String stdout = modifiedFiles.stdout as String; + return stdout.trim().isEmpty ? null : 'Snippets are out of sync'; + } +} diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 14b22a16e3f..9f9910f934f 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.2+1 +version: 0.8.3 dependencies: args: ^2.1.0 @@ -21,6 +21,7 @@ dependencies: test: ^1.17.3 uuid: ^3.0.4 yaml: ^3.1.0 + yaml_edit: ^2.0.2 dev_dependencies: build_runner: ^2.0.3 diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index 2890c528e4c..6c10a7dc320 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -448,7 +448,7 @@ void main() { ])); }); - test('fails if files are changed with --file-on-change', () async { + test('fails if files are changed with --fail-on-change', () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index e97274afd09..efaf969c83f 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -7,24 +7,35 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/license_check_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { - group('$LicenseCheckCommand', () { + group('LicenseCheckCommand', () { late CommandRunner runner; late FileSystem fileSystem; + late Platform platform; late Directory root; setUp(() { fileSystem = MemoryFileSystem(); + platform = MockPlatformWithSeparator(); final Directory packagesDir = fileSystem.currentDirectory.childDirectory('packages'); root = packagesDir.parent; + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + final LicenseCheckCommand command = LicenseCheckCommand( packagesDir, + platform: platform, + gitDir: gitDir, ); runner = CommandRunner('license_test', 'Test for $LicenseCheckCommand'); @@ -123,6 +134,33 @@ void main() { } }); + test('ignores submodules', () async { + const String submoduleName = 'a_submodule'; + + final File submoduleSpec = root.childFile('.gitmodules'); + submoduleSpec.writeAsStringSync(''' +[submodule "$submoduleName"] + path = $submoduleName + url = https://github.com/foo/$submoduleName +'''); + + const List submoduleFiles = [ + '$submoduleName/foo.dart', + '$submoduleName/a/b/bar.dart', + '$submoduleName/LICENSE', + ]; + for (final String filePath in submoduleFiles) { + root.childFile(filePath).createSync(recursive: true); + } + + final List output = + await runCapturingPrint(runner, ['license-check']); + + for (final String filePath in submoduleFiles) { + expect(output, isNot(contains('Checking $filePath'))); + } + }); + test('passes if all checked files have license blocks', () async { final File checked = root.childFile('checked.cc'); checked.createSync(); @@ -509,6 +547,11 @@ void main() { }); } +class MockPlatformWithSeparator extends MockPlatform { + @override + String get pathSeparator => isWindows ? r'\' : '/'; +} + const String _correctLicenseFileText = ''' Copyright 2013 The Flutter Authors. All rights reserved. diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart new file mode 100644 index 00000000000..30189cf23a0 --- /dev/null +++ b/script/tool/test/update_excerpts_command_test.dart @@ -0,0 +1,284 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:flutter_plugin_tools/src/update_excerpts_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + processRunner = RecordingProcessRunner(); + final UpdateExcerptsCommand command = UpdateExcerptsCommand( + packagesDir, + processRunner: processRunner, + platform: MockPlatform(), + gitDir: gitDir, + ); + + runner = CommandRunner( + 'update_excerpts_command', 'Test for update_excerpts_command'); + runner.addCommand(command); + }); + + test('runs pub get before running scripts', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + final Directory example = package.childDirectory('example'); + + await runCapturingPrint(runner, ['update-excerpts']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['pub', 'get'], example.path), + ProcessCall( + 'dart', + const [ + 'run', + 'build_runner', + 'build', + '--config', + 'excerpt', + '--output', + 'excerpts', + '--delete-conflicting-outputs', + ], + example.path), + ])); + }); + + test('runs when config is present', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + final Directory example = package.childDirectory('example'); + + final List output = + await runCapturingPrint(runner, ['update-excerpts']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall( + 'dart', + const [ + 'run', + 'build_runner', + 'build', + '--config', + 'excerpt', + '--output', + 'excerpts', + '--delete-conflicting-outputs', + ], + example.path), + ProcessCall( + 'dart', + const [ + 'run', + 'code_excerpt_updater', + '--write-in-place', + '--yaml', + '--no-escape-ng-interpolation', + '../README.md', + ], + example.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('skips when no config is present', () async { + createFakePlugin('a_package', packagesDir); + + final List output = + await runCapturingPrint(runner, ['update-excerpts']); + + expect(processRunner.recordedCalls, isEmpty); + + expect( + output, + containsAllInOrder([ + contains('Skipped 1 package(s)'), + ])); + }); + + test('restores pubspec even if running the script fails', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), // dart pub get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts'], errorHandler: (Error e) { + commandError = e; + }); + + // Check that it's definitely a failure in a step between making the changes + // and restoring the original. + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to get script dependencies') + ])); + + final String examplePubspecContent = RepositoryPackage(package) + .getExamples() + .first + .pubspecFile + .readAsStringSync(); + expect(examplePubspecContent, isNot(contains('code_excerpter'))); + expect(examplePubspecContent, isNot(contains('code_excerpt_updater'))); + }); + + test('fails if pub get fails', () async { + createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), // dart pub get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to get script dependencies') + ])); + }); + + test('fails if extraction fails', () async { + createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // dart pub get + MockProcess(exitCode: 1), // dart run build_runner ... + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to extract excerpts') + ])); + }); + + test('fails if injection fails', () async { + createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // dart pub get + MockProcess(exitCode: 0), // dart run build_runner ... + MockProcess(exitCode: 1), // dart run code_excerpt_updater ... + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to inject excerpts') + ])); + }); + + test('fails if files are changed with --fail-on-change', () async { + createFakePlugin('a_plugin', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess(stdout: changedFilePath), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('README.md is out of sync with its source excerpts'), + ])); + }); + + test('fails if git ls-files fails', () async { + createFakePlugin('a_plugin', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess(exitCode: 1) + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to determine local file state'), + ])); + }); +} From 6770bf9245227f5f84ff3d0982beede4a5843511 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 28 Apr 2022 13:19:12 -0400 Subject: [PATCH 178/249] [flutter_plugin_tools] Remove UWP (#5432) --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/build_examples_command.dart | 48 ++-------- script/tool/lib/src/common/core.dart | 19 ---- script/tool/lib/src/common/plugin_utils.dart | 21 ----- .../tool/lib/src/drive_examples_command.dart | 24 +---- .../test/build_examples_command_test.dart | 90 +------------------ .../tool/test/common/plugin_utils_test.dart | 79 ---------------- .../test/drive_examples_command_test.dart | 34 ------- script/tool/test/util.dart | 16 ---- 9 files changed, 12 insertions(+), 320 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 82a31154e13..7587ff33f02 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,7 @@ - `license-check` now ignores submodules. - Allows `make-deps-path-based` to skip packages it has alredy rewritten, so that running multiple times won't fail after the first time. +- Removes UWP support, since Flutter has dropped support for UWP. ## 0.8.2+1 diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index b88cfe30925..1aade357555 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -37,8 +37,7 @@ const String _flutterBuildTypeIOS = 'ios'; const String _flutterBuildTypeLinux = 'linux'; const String _flutterBuildTypeMacOS = 'macos'; const String _flutterBuildTypeWeb = 'web'; -const String _flutterBuildTypeWin32 = 'windows'; -const String _flutterBuildTypeWinUwp = 'winuwp'; +const String _flutterBuildTypeWindows = 'windows'; /// A command to build the example applications for packages. class BuildExamplesCommand extends PackageLoopingCommand { @@ -52,7 +51,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { argParser.addFlag(platformMacOS); argParser.addFlag(platformWeb); argParser.addFlag(platformWindows); - argParser.addFlag(platformWinUwp); argParser.addFlag(platformIOS); argParser.addFlag(_platformFlagApk); argParser.addOption( @@ -93,16 +91,9 @@ class BuildExamplesCommand extends PackageLoopingCommand { flutterBuildType: _flutterBuildTypeWeb, ), platformWindows: const _PlatformDetails( - 'Win32', + 'Windows', pluginPlatform: platformWindows, - pluginPlatformVariant: platformVariantWin32, - flutterBuildType: _flutterBuildTypeWin32, - ), - platformWinUwp: const _PlatformDetails( - 'UWP', - pluginPlatform: platformWindows, - pluginPlatformVariant: platformVariantWinUwp, - flutterBuildType: _flutterBuildTypeWinUwp, + flutterBuildType: _flutterBuildTypeWindows, ), }; @@ -146,9 +137,8 @@ class BuildExamplesCommand extends PackageLoopingCommand { // no package-level platform information for non-plugin packages. final Set<_PlatformDetails> buildPlatforms = isPlugin ? requestedPlatforms - .where((_PlatformDetails platform) => pluginSupportsPlatform( - platform.pluginPlatform, package, - variant: platform.pluginPlatformVariant)) + .where((_PlatformDetails platform) => + pluginSupportsPlatform(platform.pluginPlatform, package)) .toSet() : requestedPlatforms.toSet(); @@ -280,22 +270,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { }) async { final String enableExperiment = getStringArg(kEnableExperiment); - // The UWP template is not yet stable, so the UWP directory - // needs to be created on the fly with 'flutter create .' - Directory? temporaryPlatformDirectory; - if (flutterBuildType == _flutterBuildTypeWinUwp) { - final Directory uwpDirectory = example.directory.childDirectory('winuwp'); - if (!uwpDirectory.existsSync()) { - print('Creating temporary winuwp folder'); - final int exitCode = await processRunner.runAndStream(flutterCommand, - ['create', '--platforms=$platformWinUwp', '.'], - workingDir: example.directory); - if (exitCode == 0) { - temporaryPlatformDirectory = uwpDirectory; - } - } - } - final int exitCode = await processRunner.runAndStream( flutterCommand, [ @@ -308,13 +282,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { ], workingDir: example.directory, ); - - if (temporaryPlatformDirectory != null && - temporaryPlatformDirectory.existsSync()) { - print('Cleaning up ${temporaryPlatformDirectory.path}'); - temporaryPlatformDirectory.deleteSync(recursive: true); - } - return exitCode == 0; } } @@ -324,7 +291,6 @@ class _PlatformDetails { const _PlatformDetails( this.label, { required this.pluginPlatform, - this.pluginPlatformVariant, required this.flutterBuildType, this.extraBuildFlags = const [], }); @@ -335,10 +301,6 @@ class _PlatformDetails { /// The key in a pubspec's platform: entry. final String pluginPlatform; - /// The supportedVariants key under a plugin's [pluginPlatform] entry, if - /// applicable. - final String? pluginPlatformVariant; - /// The `flutter build` build type. final String flutterBuildType; diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 15a0d6f1f3b..de1cefd7225 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -26,27 +26,8 @@ const String platformMacOS = 'macos'; const String platformWeb = 'web'; /// Key for windows platform. -/// -/// Note that this corresponds to the Win32 variant for flutter commands like -/// `build` and `run`, but is a general platform containing all Windows -/// variants for purposes of the `platform` section of a plugin pubspec). const String platformWindows = 'windows'; -/// Key for WinUWP platform. -/// -/// Note that UWP is a platform for the purposes of flutter commands like -/// `build` and `run`, but a variant of the `windows` platform for the purposes -/// of plugin pubspecs). -const String platformWinUwp = 'winuwp'; - -/// Key for Win32 variant of the Windows platform. -const String platformVariantWin32 = 'win32'; - -/// Key for UWP variant of the Windows platform. -/// -/// See the note on [platformWinUwp]. -const String platformVariantWinUwp = 'uwp'; - /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 081ce7f1e81..94f294ebd96 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -37,7 +37,6 @@ bool pluginSupportsPlatform( String platform, RepositoryPackage plugin, { PlatformSupport? requiredMode, - String? variant, }) { assert(platform == platformIOS || platform == platformAndroid || @@ -61,26 +60,6 @@ bool pluginSupportsPlatform( } } - // If a variant is specified, check for that variant. - if (variant != null) { - const String variantsKey = 'supportedVariants'; - if (platformEntry.containsKey(variantsKey)) { - if (!(platformEntry['supportedVariants']! as YamlList) - .contains(variant)) { - return false; - } - } else { - // Platforms with variants have a default variant when unspecified for - // backward compatibility. Must match the flutter tool logic. - const Map defaultVariants = { - platformWindows: platformVariantWin32, - }; - if (variant != defaultVariants[platform]) { - return false; - } - } - } - return true; } diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index d81153a0fef..0e1efa842ed 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -36,10 +36,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { argParser.addFlag(platformWeb, help: 'Runs the web implementation of the examples'); argParser.addFlag(platformWindows, - help: 'Runs the Windows (Win32) implementation of the examples'); - argParser.addFlag(platformWinUwp, - help: - 'Runs the UWP implementation of the examples [currently a no-op]'); + help: 'Runs the Windows implementation of the examples'); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -70,7 +67,6 @@ class DriveExamplesCommand extends PackageLoopingCommand { platformMacOS, platformWeb, platformWindows, - platformWinUwp, ]; final int platformCount = platformSwitches .where((String platform) => getBoolArg(platform)) @@ -85,10 +81,6 @@ class DriveExamplesCommand extends PackageLoopingCommand { throw ToolExit(_exitNoPlatformFlags); } - if (getBoolArg(platformWinUwp)) { - logWarning('Driving UWP applications is not yet supported'); - } - String? androidDevice; if (getBoolArg(platformAndroid)) { final List devices = await _getDevicesForPlatform('android'); @@ -126,9 +118,6 @@ class DriveExamplesCommand extends PackageLoopingCommand { ], if (getBoolArg(platformWindows)) platformWindows: ['-d', 'windows'], - // TODO(stuartmorgan): Check these flags once drive supports UWP: - // https://github.com/flutter/flutter/issues/82821 - if (getBoolArg(platformWinUwp)) platformWinUwp: ['-d', 'winuwp'], }; } @@ -146,16 +135,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { for (final MapEntry> entry in _targetDeviceFlags.entries) { final String platform = entry.key; - String? variant; - if (platform == platformWindows) { - variant = platformVariantWin32; - } else if (platform == platformWinUwp) { - variant = platformVariantWinUwp; - // TODO(stuartmorgan): Remove this once drive supports UWP. - // https://github.com/flutter/flutter/issues/82821 - return PackageResult.skip('Drive does not yet support UWP'); - } - if (pluginSupportsPlatform(platform, package, variant: variant)) { + if (pluginSupportsPlatform(platform, package)) { deviceFlags.addAll(entry.value); } else { print('Skipping unsupported platform ${entry.key}...'); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 29a87907165..2bdb1bc0c2b 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -313,7 +313,7 @@ void main() { }); test( - 'building for win32 when plugin is not set up for Windows results in no-op', + 'building for Windows when plugin is not set up for Windows results in no-op', () async { mockPlatform.isWindows = true; createFakePlugin('plugin', packagesDir); @@ -325,7 +325,7 @@ void main() { output, containsAllInOrder([ contains('Running for plugin'), - contains('Win32 is not supported by this plugin'), + contains('Windows is not supported by this plugin'), ]), ); @@ -334,7 +334,7 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for win32', () async { + test('building for Windows', () async { mockPlatform.isWindows = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { @@ -350,7 +350,7 @@ void main() { expect( output, containsAllInOrder([ - '\nBUILDING plugin/example for Win32 (windows)', + '\nBUILDING plugin/example for Windows', ]), ); @@ -364,88 +364,6 @@ void main() { ])); }); - test('building for UWP when plugin does not support UWP is a no-op', - () async { - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--winuwp']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('UWP is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for UWP', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--winuwp']); - - expect( - output, - containsAllInOrder([ - contains('BUILDING plugin/example for UWP (winuwp)'), - ]), - ); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'winuwp'], pluginExampleDirectory.path), - ])); - }); - - test('building for UWP creates a folder if necessary', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--winuwp']); - - expect( - output, - contains('Creating temporary winuwp folder'), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['create', '--platforms=winuwp', '.'], - pluginExampleDirectory.path), - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'winuwp'], pluginExampleDirectory.path), - ])); - }); - test( 'building for Android when plugin is not set up for Android results in no-op', () async { diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index cedd40acb7d..af5cb7dfe4c 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -193,85 +193,6 @@ void main() { requiredMode: PlatformSupport.inline), isFalse); }); - - test('windows without variants is only win32', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }, - )); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isFalse); - }); - - test('windows with both variants matches win32 and winuwp', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails( - PlatformSupport.federated, - variants: [platformVariantWin32, platformVariantWinUwp], - ), - })); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isTrue); - }); - - test('win32 plugin is only win32', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails( - PlatformSupport.federated, - variants: [platformVariantWin32], - ), - })); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isFalse); - }); - - test('winup plugin is only winuwp', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }, - )); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isFalse); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isTrue); - }); }); group('pluginHasNativeCodeForPlatform', () { diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 9372c571b6f..ac57eb7251a 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -708,40 +708,6 @@ void main() { ])); }); - test('driving UWP is a no-op', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ], - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline, - variants: [platformVariantWinUwp]), - }, - ); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--winuwp', - ]); - - expect( - output, - containsAllInOrder([ - contains('Driving UWP applications is not yet supported'), - contains('Running for plugin'), - contains('SKIPPING: Drive does not yet support UWP'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --windows on a - // non-Windows plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - test('driving on an Android plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index fab4f39cc10..2b1719ee800 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -47,7 +47,6 @@ Directory createPackagesDirectory( class PlatformDetails { const PlatformDetails( this.type, { - this.variants = const [], this.hasNativeCode = true, this.hasDartCode = false, }); @@ -55,9 +54,6 @@ class PlatformDetails { /// The type of support for the platform. final PlatformSupport type; - /// Any 'supportVariants' to list in the pubspec. - final List variants; - /// Whether or not the plugin includes native code. /// /// Ignored for web, which does not have native code. @@ -293,18 +289,6 @@ String _pluginPlatformSection( entry = lines.join('\n') + '\n'; } - // Add any variants. - if (support.variants.isNotEmpty) { - entry += ''' - supportedVariants: -'''; - for (final String variant in support.variants) { - entry += ''' - - $variant -'''; - } - } - return entry; } From d43fae6fb432cc42d87f82c108e10e6b04a2b1d3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 28 Apr 2022 22:29:09 -0400 Subject: [PATCH 179/249] [flutter_plugin_tools] Validate code blocks in readme-check (#5436) --- script/tool/CHANGELOG.md | 7 + script/tool/lib/src/readme_check_command.dart | 93 +++++++++++-- script/tool/pubspec.yaml | 2 +- .../tool/test/readme_check_command_test.dart | 131 ++++++++++++++++++ 4 files changed, 223 insertions(+), 10 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7587ff33f02..1bce029a559 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.8.4 + +- `readme-check` now validates that there's a info tag on code blocks to + identify (and for supported languages, syntax highlight) the language. +- `readme-check` now has a `--require-excerpts` flag to require that any Dart + code blocks be managed by `code_excerpter`. + ## 0.8.3 - Adds a new `update-excerpts` command to maintain README files using the diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index 99e271c7338..9432c4b7fa4 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -26,7 +26,12 @@ class ReadmeCheckCommand extends PackageLoopingCommand { processRunner: processRunner, platform: platform, gitDir: gitDir, - ); + ) { + argParser.addFlag(_requireExcerptsArg, + help: 'Require that Dart code blocks be managed by code-excerpt.'); + } + + static const String _requireExcerptsArg = 'require-excerpts'; // Standardized capitalizations for platforms that a plugin can support. static const Map _standardPlatformNames = { @@ -61,8 +66,15 @@ class ReadmeCheckCommand extends PackageLoopingCommand { final Pubspec pubspec = package.parsePubspec(); final bool isPlugin = pubspec.flutter?['plugin'] != null; + final List readmeLines = package.readmeFile.readAsLinesSync(); + + final String? blockValidationError = _validateCodeBlocks(readmeLines); + if (blockValidationError != null) { + errors.add(blockValidationError); + } + if (isPlugin && (!package.isFederated || package.isAppFacing)) { - final String? error = _validateSupportedPlatforms(package, pubspec); + final String? error = _validateSupportedPlatforms(readmeLines, pubspec); if (error != null) { errors.add(error); } @@ -73,23 +85,86 @@ class ReadmeCheckCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } + /// Validates that code blocks (``` ... ```) follow repository standards. + String? _validateCodeBlocks(List readmeLines) { + final RegExp codeBlockDelimiterPattern = RegExp(r'^\s*```\s*([^ ]*)\s*'); + final List missingLanguageLines = []; + final List missingExcerptLines = []; + bool inBlock = false; + for (int i = 0; i < readmeLines.length; ++i) { + final RegExpMatch? match = + codeBlockDelimiterPattern.firstMatch(readmeLines[i]); + if (match == null) { + continue; + } + if (inBlock) { + inBlock = false; + continue; + } + inBlock = true; + + final int humanReadableLineNumber = i + 1; + + // Ensure that there's a language tag. + final String infoString = match[1] ?? ''; + if (infoString.isEmpty) { + missingLanguageLines.add(humanReadableLineNumber); + continue; + } + + // Check for code-excerpt usage if requested. + if (getBoolArg(_requireExcerptsArg) && infoString == 'dart') { + const String excerptTagStart = ' ' + 'tag on the previous line, and ensure that a build.excerpt.yaml is ' + 'configured for the source example.\n'); + errorSummary ??= 'Missing code-excerpt management for code block'; + } + + return errorSummary; + } + /// Validates that the plugin has a supported platforms table following the /// expected format, returning an error string if any issues are found. String? _validateSupportedPlatforms( - RepositoryPackage package, Pubspec pubspec) { - final List contents = package.readmeFile.readAsLinesSync(); - + List readmeLines, Pubspec pubspec) { // Example table following expected format: // | | Android | iOS | Web | // |----------------|---------|----------|------------------------| // | **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | - final int detailsLineNumber = - contents.indexWhere((String line) => line.startsWith('| **Support**')); + final int detailsLineNumber = readmeLines + .indexWhere((String line) => line.startsWith('| **Support**')); if (detailsLineNumber == -1) { return 'No OS support table found'; } final int osLineNumber = detailsLineNumber - 2; - if (osLineNumber < 0 || !contents[osLineNumber].startsWith('|')) { + if (osLineNumber < 0 || !readmeLines[osLineNumber].startsWith('|')) { return 'OS support table does not have the expected header format'; } @@ -111,7 +186,7 @@ class ReadmeCheckCommand extends PackageLoopingCommand { final YamlMap platformSupportMaps = platformsEntry as YamlMap; final Set actuallySupportedPlatform = platformSupportMaps.keys.toSet().cast(); - final Iterable documentedPlatforms = contents[osLineNumber] + final Iterable documentedPlatforms = readmeLines[osLineNumber] .split('|') .map((String entry) => entry.trim()) .where((String entry) => entry.isNotEmpty); diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 9f9910f934f..af38193294a 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.3 +version: 0.8.4 dependencies: args: ^2.1.0 diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart index aec2fa07845..b6e016dccab 100644 --- a/script/tool/test/readme_check_command_test.dart +++ b/script/tool/test/readme_check_command_test.dart @@ -275,4 +275,135 @@ A very useful plugin. ); }); }); + + group('code blocks', () { + test('fails on missing info string', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + +``` +void main() { + // ... +} +``` +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Code block at line 3 is missing a language identifier.'), + contains('Missing language identifier for code block'), + ]), + ); + }); + + test('allows unknown info strings', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + +```someunknowninfotag +A B C +``` +'''); + + final List output = await runCapturingPrint(runner, [ + 'readme-check', + ]); + + expect( + output, + containsAll([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + + test('allows space around info strings', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + +``` dart +A B C +``` +'''); + + final List output = await runCapturingPrint(runner, [ + 'readme-check', + ]); + + expect( + output, + containsAll([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes when excerpt requirement is met', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + + +```dart +A B C +``` +'''); + + final List output = await runCapturingPrint( + runner, ['readme-check', '--require-excerpts']); + + expect( + output, + containsAll([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + + test('fails on missing excerpt tag when requested', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + +```dart +A B C +``` +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check', '--require-excerpts'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Dart code block at line 3 is not managed by code-excerpt.'), + contains('Missing code-excerpt management for code block'), + ]), + ); + }); + }); } From fbf53f284b3919d4523fdeb56d89def38e22ab05 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 2 May 2022 17:44:12 -0400 Subject: [PATCH 180/249] [flutter_plugin_tools] Support non-plugin packages for `drive-examples` (#5468) --- script/tool/CHANGELOG.md | 5 + script/tool/lib/src/common/core.dart | 23 +-- .../src/common/package_looping_command.dart | 1 - script/tool/lib/src/common/plugin_utils.dart | 10 +- .../lib/src/common/repository_package.dart | 14 +- .../tool/lib/src/drive_examples_command.dart | 68 ++++++-- .../src/federation_safety_check_command.dart | 1 - .../lib/src/make_deps_path_based_command.dart | 3 +- .../tool/lib/src/publish_check_command.dart | 1 - .../tool/lib/src/publish_plugin_command.dart | 1 - .../tool/lib/src/pubspec_check_command.dart | 1 - script/tool/lib/src/readme_check_command.dart | 1 - script/tool/lib/src/test_command.dart | 2 +- .../tool/lib/src/version_check_command.dart | 1 - .../test/common/repository_package_test.dart | 43 ++++- .../create_all_plugins_app_command_test.dart | 1 - .../test/drive_examples_command_test.dart | 161 ++++++++++++++++++ script/tool/test/list_command_test.dart | 2 +- .../tool/test/pubspec_check_command_test.dart | 115 +++++++++---- script/tool/test/util.dart | 56 +++--- 20 files changed, 398 insertions(+), 112 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1bce029a559..2ce644178fa 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +- `drive-examples` now supports non-plugin packages. +- Commands that iterate over examples now include non-Flutter example packages. + ## 0.8.4 - `readme-check` now validates that there's a info tag on code blocks to diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index de1cefd7225..13678d720a7 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -4,7 +4,6 @@ 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. @@ -31,26 +30,14 @@ const String platformWindows = 'windows'; /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; -/// Returns whether the given directory contains a Flutter package. -bool isFlutterPackage(FileSystemEntity entity) { +/// Returns whether the given directory is a Dart package. +bool isPackage(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; - } + // Per https://dart.dev/guides/libraries/create-library-packages#what-makes-a-library-package + return entity.childFile('pubspec.yaml').existsSync() && + entity.childDirectory('lib').existsSync(); } /// Prints `successMessage` in green. diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index b75aaa4a4a4..b48743be317 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -10,7 +10,6 @@ import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'core.dart'; import 'plugin_command.dart'; diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 94f294ebd96..f33d3d73bb7 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.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 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:yaml/yaml.dart'; @@ -111,13 +110,8 @@ YamlMap? _readPlatformPubspecSectionForPlugin( /// section from [plugin]'s pubspec.yaml, or null if either it is not present, /// or the pubspec couldn't be read. YamlMap? _readPluginPubspecSection(RepositoryPackage package) { - final File pubspecFile = package.pubspecFile; - if (!pubspecFile.existsSync()) { - return null; - } - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + final Pubspec pubspec = package.parsePubspec(); + final Map? flutterSection = pubspec.flutter; if (flutterSection == null) { return null; } diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index 72e7f948fe9..76519e040ae 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -8,6 +8,8 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'core.dart'; +export 'package:pubspec_parse/pubspec_parse.dart' show Pubspec; + /// A package in the repository. // // TODO(stuartmorgan): Add more package-related info here, such as an on-demand @@ -59,6 +61,12 @@ class RepositoryPackage { /// Caches for future use. Pubspec parsePubspec() => _parsedPubspec; + /// Returns true if the package depends on Flutter. + bool requiresFlutter() { + final Pubspec pubspec = parsePubspec(); + return pubspec.dependencies.containsKey('flutter'); + } + /// True if this appears to be a federated plugin package, according to /// repository conventions. bool get isFederated => @@ -91,7 +99,7 @@ class RepositoryPackage { if (!exampleDirectory.existsSync()) { return []; } - if (isFlutterPackage(exampleDirectory)) { + if (isPackage(exampleDirectory)) { return [RepositoryPackage(exampleDirectory)]; } // Only look at the subdirectories of the example directory if the example @@ -99,8 +107,8 @@ class RepositoryPackage { // example directory for other Dart packages. return exampleDirectory .listSync() - .where((FileSystemEntity entity) => isFlutterPackage(entity)) - // isFlutterPackage guarantees that the cast to Directory is safe. + .where((FileSystemEntity entity) => isPackage(entity)) + // isPackage guarantees that the cast to Directory is safe. .map((FileSystemEntity entity) => RepositoryPackage(entity as Directory)); } diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 0e1efa842ed..68ad9713a9b 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -123,6 +123,8 @@ class DriveExamplesCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { + final bool isPlugin = isFlutterPlugin(package); + if (package.isPlatformInterface && !package.getSingleExampleDeprecated().directory.existsSync()) { // Platform interface packages generally aren't intended to have @@ -131,23 +133,23 @@ class DriveExamplesCommand extends PackageLoopingCommand { 'Platform interfaces are not expected to have integration tests.'); } - final List deviceFlags = []; - for (final MapEntry> entry - in _targetDeviceFlags.entries) { - final String platform = entry.key; - if (pluginSupportsPlatform(platform, package)) { - deviceFlags.addAll(entry.value); - } else { - print('Skipping unsupported platform ${entry.key}...'); + // For plugin packages, skip if the plugin itself doesn't support any + // requested platform(s). + if (isPlugin) { + final Iterable requestedPlatforms = _targetDeviceFlags.keys; + final Iterable unsupportedPlatforms = requestedPlatforms.where( + (String platform) => !pluginSupportsPlatform(platform, package)); + for (final String platform in unsupportedPlatforms) { + print('Skipping unsupported platform $platform...'); + } + if (unsupportedPlatforms.length == requestedPlatforms.length) { + return PackageResult.skip( + '${package.displayName} does not support any requested platform.'); } - } - // If there is no supported target platform, skip the plugin. - if (deviceFlags.isEmpty) { - return PackageResult.skip( - '${package.displayName} does not support any requested platform.'); } int examplesFound = 0; + int supportedExamplesFound = 0; bool testsRan = false; final List errors = []; for (final RepositoryPackage example in package.getExamples()) { @@ -155,6 +157,15 @@ class DriveExamplesCommand extends PackageLoopingCommand { final String exampleName = getRelativePosixPath(example.directory, from: packagesDir); + // Skip examples that don't support any requested platform(s). + final List deviceFlags = _deviceFlagsForExample(example); + if (deviceFlags.isEmpty) { + print( + 'Skipping $exampleName; does not support any requested platforms.'); + continue; + } + ++supportedExamplesFound; + final List drivers = await _getDrivers(example); if (drivers.isEmpty) { print('No driver tests found for $exampleName'); @@ -195,14 +206,41 @@ class DriveExamplesCommand extends PackageLoopingCommand { } } if (!testsRan) { - printError('No driver tests were run ($examplesFound example(s) found).'); - errors.add('No tests ran (use --exclude if this is intentional).'); + // It is an error for a plugin not to have integration tests, because that + // is the only way to test the method channel communication. + if (isPlugin) { + printError( + 'No driver tests were run ($examplesFound example(s) found).'); + errors.add('No tests ran (use --exclude if this is intentional).'); + } else { + return PackageResult.skip(supportedExamplesFound == 0 + ? 'No example supports requested platform(s).' + : 'No example is configured for driver tests.'); + } } return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); } + /// Returns the device flags for the intersection of the requested platforms + /// and the platforms supported by [example]. + List _deviceFlagsForExample(RepositoryPackage example) { + final List deviceFlags = []; + for (final MapEntry> entry + in _targetDeviceFlags.entries) { + final String platform = entry.key; + if (example.directory.childDirectory(platform).existsSync()) { + deviceFlags.addAll(entry.value); + } else { + final String exampleName = + getRelativePosixPath(example.directory, from: packagesDir); + print('Skipping unsupported platform $platform for $exampleName'); + } + } + return deviceFlags; + } + Future> _getDevicesForPlatform(String platform) async { final List deviceIds = []; diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index df9d86892e1..383637a9e89 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -8,7 +8,6 @@ import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/file_utils.dart'; diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index 45a4427d321..370fc3559f7 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -3,15 +3,14 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; const int _exitPackageNotFound = 3; const int _exitCannotUpdatePubspec = 4; diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 8fd96b818c1..b6b83dabcb4 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -10,7 +10,6 @@ import 'package:file/file.dart'; import 'package:http/http.dart' as http; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 28d17a3a248..05f0afd0c06 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -13,7 +13,6 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 2c27c91e049..654675ebb85 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -5,7 +5,6 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:platform/platform.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index 9432c4b7fa4..0cb64920dea 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -5,7 +5,6 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:platform/platform.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 2c5dd9934b4..d115b6557ad 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -43,7 +43,7 @@ class TestCommand extends PackageLoopingCommand { } bool passed; - if (isFlutterPackage(package.directory)) { + if (package.requiresFlutter()) { passed = await _runFlutterTests(package); } else { passed = await _runDartTests(package); diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index fcaea335920..f69611f5a06 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -9,7 +9,6 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index 0a8ea36eb90..2d0e11475c3 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -5,7 +5,6 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/repository_package.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -97,7 +96,7 @@ void main() { }); group('getExamples', () { - test('handles a single example', () async { + test('handles a single Flutter example', () async { final Directory plugin = createFakePlugin('a_plugin', packagesDir); final List examples = @@ -107,7 +106,7 @@ void main() { expect(examples[0].path, plugin.childDirectory('example').path); }); - test('handles multiple examples', () async { + test('handles multiple Flutter examples', () async { final Directory plugin = createFakePlugin('a_plugin', packagesDir, examples: ['example1', 'example2']); @@ -120,6 +119,30 @@ void main() { expect(examples[1].path, plugin.childDirectory('example').childDirectory('example2').path); }); + + test('handles a single non-Flutter example', () async { + final Directory package = createFakePackage('a_package', packagesDir); + + final List examples = + RepositoryPackage(package).getExamples().toList(); + + expect(examples.length, 1); + expect(examples[0].path, package.childDirectory('example').path); + }); + + test('handles multiple non-Flutter examples', () async { + final Directory package = createFakePackage('a_package', packagesDir, + examples: ['example1', 'example2']); + + final List examples = + RepositoryPackage(package).getExamples().toList(); + + expect(examples.length, 2); + expect(examples[0].path, + package.childDirectory('example').childDirectory('example1').path); + expect(examples[1].path, + package.childDirectory('example').childDirectory('example2').path); + }); }); group('federated plugin queries', () { @@ -179,4 +202,18 @@ void main() { expect(pubspec.name, 'a_plugin'); }); }); + + group('requiresFlutter', () { + test('returns true for Flutter package', () async { + final Directory package = + createFakePackage('a_package', packagesDir, isFlutter: true); + expect(RepositoryPackage(package).requiresFlutter(), true); + }); + + test('returns false for non-Flutter package', () async { + final Directory package = + createFakePackage('a_package', packagesDir, isFlutter: false); + expect(RepositoryPackage(package).requiresFlutter(), false); + }); + }); } diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 917adca020d..9e2ee29326d 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -10,7 +10,6 @@ import 'package:file/local.dart'; import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:flutter_plugin_tools/src/create_all_plugins_app_command.dart'; import 'package:platform/platform.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; import 'util.dart'; diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index ac57eb7251a..214efb42022 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -128,6 +128,7 @@ void main() { extraFiles: [ 'example/test_driver/integration_test.dart', 'example/integration_test/foo_test.dart', + 'example/ios/ios.m', ], platformSupport: { platformIOS: const PlatformDetails(PlatformSupport.inline), @@ -193,6 +194,8 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -243,6 +246,8 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/plugin_test.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -276,6 +281,8 @@ void main() { packagesDir, extraFiles: [ 'example/lib/main.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -312,6 +319,8 @@ void main() { 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/integration_test/ignore_me.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -398,6 +407,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/linux/linux.cc', ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), @@ -542,6 +552,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -591,6 +602,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -668,6 +680,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/windows/windows.cpp', ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), @@ -715,6 +728,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/android/android.java', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -853,6 +867,8 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -927,6 +943,7 @@ void main() { extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -959,6 +976,7 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/integration_test.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -995,6 +1013,7 @@ void main() { 'example/test_driver/integration_test.dart', 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', + 'example/macos/macos.swift', ], platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), @@ -1060,5 +1079,147 @@ void main() { pluginExampleDirectory.path), ])); }); + + group('packages', () { + test('can be driven', () async { + final Directory package = + createFakePackage('a_package', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/test_driver/integration_test.dart', + 'example/web/index.html', + ]); + final Directory exampleDirectory = package.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/foo_test.dart' + ], + exampleDirectory.path), + ])); + }); + + test('are skipped when example does not support platform', () async { + createFakePackage('a_package', packagesDir, + isFlutter: true, + extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/test_driver/integration_test.dart', + ]); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + contains('Skipping a_package/example; does not support any ' + 'requested platforms'), + contains('SKIPPING: No example supports requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls.isEmpty, true); + }); + + test('drive only supported examples if there is more than one', () async { + final Directory package = createFakePackage('a_package', packagesDir, + isFlutter: true, + examples: [ + 'with_web', + 'without_web' + ], + extraFiles: [ + 'example/with_web/integration_test/foo_test.dart', + 'example/with_web/test_driver/integration_test.dart', + 'example/with_web/web/index.html', + 'example/without_web/integration_test/foo_test.dart', + 'example/without_web/test_driver/integration_test.dart', + ]); + final Directory supportedExampleDirectory = + package.childDirectory('example').childDirectory('with_web'); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + contains( + 'Skipping a_package/example/without_web; does not support any requested platforms.'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/foo_test.dart' + ], + supportedExampleDirectory.path), + ])); + }); + + test('are skipped when there is no integration testing', () async { + createFakePackage('a_package', packagesDir, + isFlutter: true, extraFiles: ['example/web/index.html']); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + contains('SKIPPING: No example is configured for driver tests.'), + ]), + ); + + expect(processRunner.recordedCalls.isEmpty, true); + }); + }); }); } diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index fcdf9fafdb6..9e70f72e748 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -12,7 +12,7 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$ListCommand', () { + group('ListCommand', () { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 42d20240da8..30b6ab6004e 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -155,6 +155,21 @@ ${_flutterSection(isPlugin: true)} ${_dependenciesSection()} ${_devDependenciesSection()} ${_falseSecretsSection()} +'''); + + pluginDirectory + .childDirectory('example') + .childFile('pubspec.yaml') + .writeAsStringSync(''' +${_headerSection( + 'plugin_example', + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${_environmentSection()} +${_dependenciesSection()} +${_flutterSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -172,15 +187,31 @@ ${_falseSecretsSection()} }); test('passes for a Flutter package following conventions', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory packageDirectory = + createFakePackage('a_package', packagesDir); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${_headerSection('plugin')} + packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${_headerSection('a_package')} ${_environmentSection()} ${_dependenciesSection()} ${_devDependenciesSection()} ${_flutterSection()} ${_falseSecretsSection()} +'''); + + packageDirectory + .childDirectory('example') + .childFile('pubspec.yaml') + .writeAsStringSync(''' +${_headerSection( + 'a_package', + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${_environmentSection()} +${_dependenciesSection()} +${_flutterSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -190,8 +221,8 @@ ${_falseSecretsSection()} expect( output, containsAllInOrder([ - contains('Running for plugin...'), - contains('Running for plugin/example...'), + contains('Running for a_package...'), + contains('Running for a_package/example...'), contains('No issues found!'), ]), ); @@ -221,7 +252,8 @@ ${_dependenciesSection()} }); test('fails when homepage is included', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeHomepage: true)} @@ -248,7 +280,8 @@ ${_devDependenciesSection()} }); test('fails when repository is missing', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeRepository: false)} @@ -274,7 +307,8 @@ ${_devDependenciesSection()} }); test('fails when homepage is given instead of repository', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} @@ -301,7 +335,8 @@ ${_devDependenciesSection()} }); test('fails when repository is incorrect', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} @@ -327,7 +362,8 @@ ${_devDependenciesSection()} }); test('fails when issue tracker is missing', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeIssueTracker: false)} @@ -353,8 +389,9 @@ ${_devDependenciesSection()} }); test('fails when description is too short', () async { - final Directory pluginDirectory = - createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + final Directory pluginDirectory = createFakePlugin( + 'a_plugin', packagesDir.childDirectory('a_plugin'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: 'Too short')} @@ -383,7 +420,8 @@ ${_devDependenciesSection()} test( 'allows short descriptions for non-app-facing parts of federated plugins', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: 'Too short')} @@ -410,7 +448,8 @@ ${_devDependenciesSection()} }); test('fails when description is too long', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); const String description = 'This description is too long. It just goes ' 'on and on and on and on and on. pub.dev will down-score it because ' @@ -442,7 +481,8 @@ ${_devDependenciesSection()} }); test('fails when environment section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} @@ -469,7 +509,8 @@ ${_environmentSection()} }); test('fails when flutter section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} @@ -496,7 +537,8 @@ ${_devDependenciesSection()} }); test('fails when dependencies section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} @@ -550,7 +592,8 @@ ${_dependenciesSection()} }); test('fails when false_secrets section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} @@ -580,7 +623,8 @@ ${_devDependenciesSection()} test('fails when an implemenation package is missing "implements"', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true)} @@ -608,7 +652,8 @@ ${_devDependenciesSection()} test('fails when an implemenation package has the wrong "implements"', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true)} @@ -636,7 +681,8 @@ ${_devDependenciesSection()} test('passes for a correct implemenation package', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -663,8 +709,9 @@ ${_devDependenciesSection()} }); test('fails when a "default_package" looks incorrect', () async { - final Directory pluginDirectory = - createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + final Directory pluginDirectory = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -702,8 +749,9 @@ ${_devDependenciesSection()} test( 'fails when a "default_package" does not have a corresponding dependency', () async { - final Directory pluginDirectory = - createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + final Directory pluginDirectory = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -739,8 +787,9 @@ ${_devDependenciesSection()} }); test('passes for an app-facing package without "implements"', () async { - final Directory pluginDirectory = - createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + final Directory pluginDirectory = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -769,8 +818,8 @@ ${_devDependenciesSection()} test('passes for a platform interface package without "implements"', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_platform_interface', - packagesDir.childDirectory('plugin_a')); + 'plugin_a_platform_interface', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -799,7 +848,8 @@ ${_devDependenciesSection()} test('validates some properties even for unpublished packages', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); // Environment section is in the wrong location. // Missing 'implements'. @@ -829,7 +879,8 @@ ${_environmentSection()} }); test('ignores some checks for unpublished packages', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); // Missing metadata that is only useful for published packages, such as // repository and issue tracker. @@ -886,7 +937,7 @@ ${_devDependenciesSection()} test('repository check works', () async { final Directory packageDirectory = - createFakePackage('package', packagesDir); + createFakePackage('package', packagesDir, examples: []); packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('package')} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 2b1719ee800..8a2bf099cc8 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -107,6 +107,12 @@ Directory createFakePlugin( /// /// [extraFiles] is an optional list of package-relative paths, using unix-style /// separators, of extra files to create in the package. +/// +/// If [includeCommonFiles] is true, common but non-critical files like +/// CHANGELOG.md and AUTHORS will be included. +/// +/// If non-null, [directoryName] will be used for the directory instead of +/// [name]. // TODO(stuartmorgan): Convert the return to a RepositoryPackage. Directory createFakePackage( String name, @@ -116,37 +122,43 @@ Directory createFakePackage( bool isFlutter = false, String? version = '0.0.1', String flutterConstraint = '>=2.5.0', + bool includeCommonFiles = true, + String? directoryName, + String? publishTo, }) { - final Directory packageDirectory = parentDirectory.childDirectory(name); + final Directory packageDirectory = + parentDirectory.childDirectory(directoryName ?? name); packageDirectory.createSync(recursive: true); + packageDirectory.childDirectory('lib').createSync(); createFakePubspec(packageDirectory, name: name, isFlutter: isFlutter, version: version, flutterConstraint: flutterConstraint); - createFakeCHANGELOG(packageDirectory, ''' + if (includeCommonFiles) { + createFakeCHANGELOG(packageDirectory, ''' ## $version * Some changes. '''); - createFakeAuthors(packageDirectory); + createFakeAuthors(packageDirectory); + } if (examples.length == 1) { - final Directory exampleDir = packageDirectory.childDirectory(examples.first) - ..createSync(); - createFakePubspec(exampleDir, - name: '${name}_example', + createFakePackage('${name}_example', packageDirectory, + directoryName: examples.first, + examples: [], + includeCommonFiles: false, isFlutter: isFlutter, publishTo: 'none', flutterConstraint: flutterConstraint); } else if (examples.isNotEmpty) { - final Directory exampleDir = packageDirectory.childDirectory('example') - ..createSync(); - for (final String example in examples) { - final Directory currentExample = exampleDir.childDirectory(example) - ..createSync(); - createFakePubspec(currentExample, - name: example, + final Directory examplesDirectory = + packageDirectory.childDirectory('example')..createSync(); + for (final String exampleName in examples) { + createFakePackage(exampleName, examplesDirectory, + examples: [], + includeCommonFiles: false, isFlutter: isFlutter, publishTo: 'none', flutterConstraint: flutterConstraint); @@ -179,7 +191,7 @@ void createFakePubspec( bool isPlugin = false, Map platformSupport = const {}, - String publishTo = 'http://no_pub_server.com', + String? publishTo, String? version, String dartConstraint = '>=2.0.0 <3.0.0', String flutterConstraint = '>=2.5.0', @@ -219,9 +231,16 @@ flutter: } } - String yaml = ''' + // Default to a fake server to avoid ever accidentally publishing something + // from a test. Does not use 'none' since that changes the behavior of some + // commands. + final String publishToSection = + 'publish_to: ${publishTo ?? 'http://no_pub_server.com'}'; + + final String yaml = ''' name: $name ${(version != null) ? 'version: $version' : ''} +$publishToSection $environmentSection @@ -230,11 +249,6 @@ $dependenciesSection $pluginSection '''; - if (publishTo.isNotEmpty) { - yaml += ''' -publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being published by a broken test. -'''; - } parent.childFile('pubspec.yaml').createSync(); parent.childFile('pubspec.yaml').writeAsStringSync(yaml); } From 1186d6831f1b7042ffb27ab2642f96e15da59626 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 3 May 2022 00:44:10 -0400 Subject: [PATCH 181/249] [flutter_plugin_tools] Include examples in `test` (#5453) --- script/tool/CHANGELOG.md | 3 +- script/tool/lib/src/test_command.dart | 4 +- script/tool/pubspec.yaml | 2 +- script/tool/test/test_command_test.dart | 50 +++++++++++++++++++++++-- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2ce644178fa..b2319c63dc4 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.8.5 +- Updates `test` to inculde the Dart unit tests of examples, if any. - `drive-examples` now supports non-plugin packages. - Commands that iterate over examples now include non-Flutter example packages. diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index d115b6557ad..a1a995dbd88 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -36,6 +36,9 @@ class TestCommand extends PackageLoopingCommand { final String description = 'Runs the Dart tests for all packages.\n\n' 'This command requires "flutter" to be in your path.'; + @override + bool get includeSubpackages => true; + @override Future runForPackage(RepositoryPackage package) async { if (!package.directory.childDirectory('test').existsSync()) { @@ -88,7 +91,6 @@ class TestCommand extends PackageLoopingCommand { exitCode = await processRunner.runAndStream( 'dart', [ - 'pub', 'run', if (experiment.isNotEmpty) '--enable-experiment=$experiment', 'test', diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index af38193294a..32bfc1b6228 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.4 +version: 0.8.5 dependencies: args: ^2.1.0 diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 9bcd8d1ae67..386eaf0d345 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -58,6 +58,28 @@ void main() { ); }); + test('runs flutter test on Flutter package example tests', () async { + final Directory pluginDir = createFakePlugin('a_plugin', packagesDir, + extraFiles: [ + 'test/empty_test.dart', + 'example/test/an_example_test.dart' + ]); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], pluginDir.path), + ProcessCall( + getFlutterCommand(mockPlatform), + const ['test', '--color'], + pluginDir.childDirectory('example').path), + ]), + ); + }); + test('fails when Flutter tests fail', () async { createFakePlugin('plugin1', packagesDir, extraFiles: ['test/empty_test.dart']); @@ -102,7 +124,7 @@ void main() { ); }); - test('runs pub run test on non-Flutter packages', () async { + test('runs dart run test on non-Flutter packages', () async { final Directory pluginDir = createFakePlugin('a', packagesDir, extraFiles: ['test/empty_test.dart']); final Directory packageDir = createFakePackage('b', packagesDir, @@ -121,12 +143,34 @@ void main() { ProcessCall('dart', const ['pub', 'get'], packageDir.path), ProcessCall( 'dart', - const ['pub', 'run', '--enable-experiment=exp1', 'test'], + const ['run', '--enable-experiment=exp1', 'test'], packageDir.path), ]), ); }); + test('runs dart run test on non-Flutter package examples', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir, + extraFiles: [ + 'test/empty_test.dart', + 'example/test/an_example_test.dart' + ]); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('dart', const ['pub', 'get'], packageDir.path), + ProcessCall('dart', const ['run', 'test'], packageDir.path), + ProcessCall('dart', const ['pub', 'get'], + packageDir.childDirectory('example').path), + ProcessCall('dart', const ['run', 'test'], + packageDir.childDirectory('example').path), + ]), + ); + }); + test('fails when getting non-Flutter package dependencies fails', () async { createFakePackage('a_package', packagesDir, extraFiles: ['test/empty_test.dart']); @@ -217,7 +261,7 @@ void main() { ProcessCall('dart', const ['pub', 'get'], packageDir.path), ProcessCall( 'dart', - const ['pub', 'run', '--enable-experiment=exp1', 'test'], + const ['run', '--enable-experiment=exp1', 'test'], packageDir.path), ]), ); From 5a06b47e5113ebf8c9b0ebd35e773b5b3109254e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 3 May 2022 13:59:11 -0400 Subject: [PATCH 182/249] [tools] Remove single-example RepositoryPackage method (#5600) --- .../lib/src/common/repository_package.dart | 9 --- .../tool/lib/src/drive_examples_command.dart | 3 +- .../lib/src/firebase_test_lab_command.dart | 42 +++++++--- script/tool/lib/src/lint_android_command.dart | 39 +++++---- .../test/firebase_test_lab_command_test.dart | 81 +++++++++++++++++-- .../tool/test/lint_android_command_test.dart | 64 +++++++++++++-- 6 files changed, 185 insertions(+), 53 deletions(-) diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index 76519e040ae..579ba01868e 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -112,13 +112,4 @@ class RepositoryPackage { .map((FileSystemEntity entity) => RepositoryPackage(entity as Directory)); } - - /// Returns the example directory, assuming there is only one. - /// - /// DO NOT USE THIS METHOD. It exists only to easily find code that was - /// written to use a single example and needs to be restructured to handle - /// multiple examples. New code should always use [getExamples]. - // TODO(stuartmorgan): Eliminate all uses of this. - RepositoryPackage getSingleExampleDeprecated() => - RepositoryPackage(directory.childDirectory('example')); } diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 68ad9713a9b..15366e17ae8 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -125,8 +125,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { Future runForPackage(RepositoryPackage package) async { final bool isPlugin = isFlutterPlugin(package); - if (package.isPlatformInterface && - !package.getSingleExampleDeprecated().directory.existsSync()) { + if (package.isPlatformInterface && package.getExamples().isEmpty) { // Platform interface packages generally aren't intended to have // examples, and don't need integration tests, so skip rather than fail. return PackageResult.skip( diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index e824d8ad1a9..6cc3c129c6b 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -119,7 +119,32 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - final RepositoryPackage example = package.getSingleExampleDeprecated(); + final List results = []; + for (final RepositoryPackage example in package.getExamples()) { + results.add(await _runForExample(example, package: package)); + } + + // If all results skipped, report skip overall. + if (results + .every((PackageResult result) => result.state == RunState.skipped)) { + return PackageResult.skip('No examples support Android.'); + } + // Otherwise, report failure if there were any failures. + final List allErrors = results + .map((PackageResult result) => + result.state == RunState.failed ? result.details : []) + .expand((List list) => list) + .toList(); + return allErrors.isEmpty + ? PackageResult.success() + : PackageResult.fail(allErrors); + } + + /// Runs the test for the given example of [package]. + Future _runForExample( + RepositoryPackage example, { + required RepositoryPackage package, + }) async { final Directory androidDirectory = example.directory.childDirectory('android'); if (!androidDirectory.existsSync()) { @@ -163,7 +188,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { // Used within the loop to ensure a unique GCS output location for each // test file's run. int resultsCounter = 0; - for (final File test in _findIntegrationTestFiles(package)) { + for (final File test in _findIntegrationTestFiles(example)) { final String testName = getRelativePosixPath(test, from: package.directory); print('Testing $testName...'); @@ -175,7 +200,8 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { final String buildId = getStringArg('build-id'); final String testRunId = getStringArg('test-run-id'); final String resultsDir = - 'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/'; + 'plugins_android_test/${package.displayName}/$buildId/$testRunId/' + '${example.directory.basename}/${resultsCounter++}/'; // Automatically retry failures; there is significant flake with these // tests whose cause isn't yet understood, and having to re-run the @@ -299,12 +325,10 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { return true; } - /// Finds and returns all integration test files for [package]. - Iterable _findIntegrationTestFiles(RepositoryPackage package) sync* { - final Directory integrationTestDir = package - .getSingleExampleDeprecated() - .directory - .childDirectory('integration_test'); + /// Finds and returns all integration test files for [example]. + Iterable _findIntegrationTestFiles(RepositoryPackage example) sync* { + final Directory integrationTestDir = + example.directory.childDirectory('integration_test'); if (!integrationTestDir.existsSync()) { return; diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index 8368160c4c9..467dc7942d9 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -28,7 +28,7 @@ class LintAndroidCommand extends PackageLoopingCommand { @override final String description = 'Runs "gradlew lint" on Android plugins.\n\n' - 'Requires the example to have been build at least once before running.'; + 'Requires the examples to have been build at least once before running.'; @override Future runForPackage(RepositoryPackage package) async { @@ -38,25 +38,30 @@ class LintAndroidCommand extends PackageLoopingCommand { 'Plugin does not have an Android implemenatation.'); } - final RepositoryPackage example = package.getSingleExampleDeprecated(); - final GradleProject project = GradleProject(example.directory, - processRunner: processRunner, platform: platform); + bool failed = false; + for (final RepositoryPackage example in package.getExamples()) { + final GradleProject project = GradleProject(example.directory, + processRunner: processRunner, platform: platform); - if (!project.isConfigured()) { - return PackageResult.fail(['Build example before linting']); - } + if (!project.isConfigured()) { + return PackageResult.fail(['Build examples before linting']); + } - final String packageName = package.directory.basename; + final String packageName = package.directory.basename; - // Only lint one build mode to avoid extra work. - // Only lint the plugin project itself, to avoid failing due to errors in - // dependencies. - // - // TODO(stuartmorgan): Consider adding an XML parser to read and summarize - // all results. Currently, only the first three errors will be shown inline, - // and the rest have to be checked via the CI-uploaded artifact. - final int exitCode = await project.runCommand('$packageName:lintDebug'); + // Only lint one build mode to avoid extra work. + // Only lint the plugin project itself, to avoid failing due to errors in + // dependencies. + // + // TODO(stuartmorgan): Consider adding an XML parser to read and summarize + // all results. Currently, only the first three errors will be shown + // inline, and the rest have to be checked via the CI-uploaded artifact. + final int exitCode = await project.runCommand('$packageName:lintDebug'); + if (exitCode != 0) { + failed = true; + } + } - return exitCode == 0 ? PackageResult.success() : PackageResult.fail(); + return failed ? PackageResult.fail() : PackageResult.success(); } } diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 1dfd8ba66b5..db658e19b28 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -17,7 +17,7 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$FirebaseTestLabCommand', () { + group('FirebaseTestLabCommand', () { FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; @@ -173,7 +173,7 @@ public class MainActivityTest { '/packages/plugin1/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin1/example'), ProcessCall( @@ -187,7 +187,7 @@ public class MainActivityTest { '/packages/plugin2/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin2/example'), ]), @@ -254,7 +254,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -264,13 +264,78 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/1/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), ); }); + test('runs for all examples', () async { + const List examples = ['example1', 'example2']; + const String javaTestFileExampleRelativePath = + 'android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = createFakePlugin('plugin', packagesDir, + examples: examples, + extraFiles: [ + for (final String example in examples) ...[ + 'example/$example/integration_test/a_test.dart', + 'example/$example/android/gradlew', + 'example/$example/$javaTestFileExampleRelativePath', + ], + ]); + for (final String example in examples) { + _writeJavaTestFile( + pluginDir, 'example/$example/$javaTestFileExampleRelativePath'); + } + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=redfin,version=30', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + output, + containsAllInOrder([ + contains('Testing example/example1/integration_test/a_test.dart...'), + contains('Testing example/example2/integration_test/a_test.dart...'), + ]), + ); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall( + '/packages/plugin/example/example1/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/example1/integration_test/a_test.dart' + .split(' '), + '/packages/plugin/example/example1/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example1/0/ --device model=redfin,version=30 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example/example1'), + ProcessCall( + '/packages/plugin/example/example2/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/example2/integration_test/a_test.dart' + .split(' '), + '/packages/plugin/example/example2/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example2/0/ --device model=redfin,version=30 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example/example2'), + ]), + ); + }); + test('fails if a test fails twice', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; @@ -479,7 +544,7 @@ public class MainActivityTest { output, containsAllInOrder([ contains('Running for package'), - contains('package/example does not support Android'), + contains('No examples support Android'), ]), ); expect(output, @@ -547,7 +612,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), @@ -717,7 +782,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index a9ad510f7ee..91608d3785c 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -16,7 +16,7 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$LintAndroidCommand', () { + group('LintAndroidCommand', () { FileSystem fileSystem; late Directory packagesDir; late CommandRunner runner; @@ -72,6 +72,47 @@ void main() { ])); }); + test('runs on all examples', () async { + final List examples = ['example1', 'example2']; + final Directory pluginDir = createFakePlugin('plugin1', packagesDir, + examples: examples, + extraFiles: [ + 'example/example1/android/gradlew', + 'example/example2/android/gradlew', + ], + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline) + }); + + final Iterable exampleAndroidDirs = examples.map( + (String example) => pluginDir + .childDirectory('example') + .childDirectory(example) + .childDirectory('android')); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + for (final Directory directory in exampleAndroidDirs) + ProcessCall( + directory.childFile('gradlew').path, + const ['plugin1:lintDebug'], + directory.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + test('fails if gradlew is missing', () async { createFakePlugin('plugin1', packagesDir, platformSupport: { @@ -89,18 +130,25 @@ void main() { output, containsAllInOrder( [ - contains('Build example before linting'), + contains('Build examples before linting'), ], )); }); test('fails if linting finds issues', () async { - createFakePlugin('plugin1', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); + final Directory pluginDir = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/android/gradlew', + ], platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline) + }); - processRunner.mockProcessesForExecutable['gradlew'] = [ + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ MockProcess(exitCode: 1), ]; @@ -115,7 +163,7 @@ void main() { output, containsAllInOrder( [ - contains('Build example before linting'), + contains('The following packages had errors:'), ], )); }); From 5ec6644f3f260d23bbc6d0d386f7612fdf3e81a1 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 3 May 2022 17:14:11 -0400 Subject: [PATCH 183/249] [tools] Convert test utils to RepositoryPackage (#5605) --- script/tool/lib/src/common/core.dart | 14 +- script/tool/lib/src/common/gradle.dart | 6 +- .../tool/lib/src/common/plugin_command.dart | 14 +- .../lib/src/common/repository_package.dart | 39 +++ .../src/create_all_plugins_app_command.dart | 22 +- .../lib/src/firebase_test_lab_command.dart | 4 +- script/tool/lib/src/lint_android_command.dart | 2 +- .../lib/src/make_deps_path_based_command.dart | 2 +- script/tool/lib/src/native_test_command.dart | 14 +- .../tool/lib/src/publish_check_command.dart | 4 +- script/tool/lib/src/test_command.dart | 2 +- .../tool/lib/src/version_check_command.dart | 2 +- script/tool/test/analyze_command_test.dart | 84 +++--- .../test/build_examples_command_test.dart | 53 ++-- script/tool/test/common/gradle_test.dart | 39 +-- .../common/package_looping_command_test.dart | 210 +++++++++------ .../tool/test/common/plugin_command_test.dart | 201 ++++++++++----- .../tool/test/common/plugin_utils_test.dart | 40 ++- .../test/common/repository_package_test.dart | 97 +++---- .../create_all_plugins_app_command_test.dart | 14 +- .../tool/test/custom_test_command_test.dart | 24 +- .../test/drive_examples_command_test.dart | 59 ++--- .../federation_safety_check_command_test.dart | 100 ++++---- .../test/firebase_test_lab_command_test.dart | 79 +++--- script/tool/test/format_command_test.dart | 39 +-- .../tool/test/lint_android_command_test.dart | 23 +- .../tool/test/lint_podspecs_command_test.dart | 10 +- script/tool/test/list_command_test.dart | 32 +-- .../make_deps_path_based_command_test.dart | 56 ++-- .../tool/test/native_test_command_test.dart | 242 +++++++++--------- .../tool/test/publish_check_command_test.dart | 25 +- .../test/publish_plugin_command_test.dart | 112 ++++---- .../tool/test/pubspec_check_command_test.dart | 116 ++++----- .../tool/test/readme_check_command_test.dart | 57 +++-- script/tool/test/test_command_test.dart | 64 +++-- .../test/update_excerpts_command_test.dart | 18 +- script/tool/test/util.dart | 71 ++--- .../tool/test/version_check_command_test.dart | 80 +++--- .../tool/test/xcode_analyze_command_test.dart | 40 ++- 39 files changed, 1124 insertions(+), 986 deletions(-) diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 13678d720a7..b91029f1a5c 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -30,14 +30,22 @@ const String platformWindows = 'windows'; /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; +/// Target platforms supported by Flutter. +// ignore: public_member_api_docs +enum FlutterPlatform { android, ios, linux, macos, web, windows } + /// Returns whether the given directory is a Dart package. bool isPackage(FileSystemEntity entity) { if (entity is! Directory) { return false; } - // Per https://dart.dev/guides/libraries/create-library-packages#what-makes-a-library-package - return entity.childFile('pubspec.yaml').existsSync() && - entity.childDirectory('lib').existsSync(); + // According to + // https://dart.dev/guides/libraries/create-library-packages#what-makes-a-library-package + // a package must also have a `lib/` directory, but in practice that's not + // always true. flutter/plugins has some special cases (espresso, some + // federated implementation packages) that don't have any source, so this + // deliberately doesn't check that there's a lib directory. + return entity.childFile('pubspec.yaml').existsSync(); } /// Prints `successMessage` in green. diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart index 9da4e89811e..74653607501 100644 --- a/script/tool/lib/src/common/gradle.dart +++ b/script/tool/lib/src/common/gradle.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; import 'process_runner.dart'; +import 'repository_package.dart'; const String _gradleWrapperWindows = 'gradlew.bat'; const String _gradleWrapperNonWindows = 'gradlew'; @@ -21,7 +22,7 @@ class GradleProject { }); /// The directory of a Flutter project to run Gradle commands in. - final Directory flutterProject; + final RepositoryPackage flutterProject; /// The [ProcessRunner] used to run commands. Overridable for testing. final ProcessRunner processRunner; @@ -30,7 +31,8 @@ class GradleProject { final Platform platform; /// The project's 'android' directory. - Directory get androidDirectory => flutterProject.childDirectory('android'); + Directory get androidDirectory => + flutterProject.platformDirectory(FlutterPlatform.android); /// The path to the Gradle wrapper file for the project. File get gradleWrapper => androidDirectory.childFile( diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index d2100289966..0ec890368fb 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -368,7 +368,7 @@ abstract class PluginCommand extends Command { await for (final FileSystemEntity entity in dir.list(followLinks: false)) { // A top-level Dart package is a plugin package. - if (_isDartPackage(entity)) { + if (isPackage(entity)) { if (packages.isEmpty || packages.contains(p.basename(entity.path))) { yield PackageEnumerationEntry( RepositoryPackage(entity as Directory), @@ -378,7 +378,7 @@ abstract class PluginCommand extends Command { // Look for Dart packages under this top-level directory. await for (final FileSystemEntity subdir in entity.list(followLinks: false)) { - if (_isDartPackage(subdir)) { + if (isPackage(subdir)) { // There are three ways for a federated plugin to match: // - package name (path_provider_android) // - fully specified name (path_provider/path_provider_android) @@ -427,9 +427,9 @@ abstract class PluginCommand extends Command { {bool filterExcluded = true}) async* { yield* package.directory .list(recursive: true, followLinks: false) - .where(_isDartPackage) + .where(isPackage) .map((FileSystemEntity directory) => - // _isDartPackage guarantees that this cast is valid. + // isPackage guarantees that this cast is valid. RepositoryPackage(directory as Directory)); } @@ -448,12 +448,6 @@ abstract class PluginCommand extends Command { .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(); - } - /// Retrieve an instance of [GitVersionFinder] based on `_baseShaArg` and [gitDir]. /// /// Throws tool exit if [gitDir] nor root directory is a git directory. diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index 579ba01868e..5f448d36d7e 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -9,6 +9,7 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'core.dart'; export 'package:pubspec_parse/pubspec_parse.dart' show Pubspec; +export 'core.dart' show FlutterPlatform; /// A package in the repository. // @@ -53,6 +54,44 @@ class RepositoryPackage { /// The package's top-level README. File get readmeFile => directory.childFile('README.md'); + /// The package's top-level README. + File get changelogFile => directory.childFile('CHANGELOG.md'); + + /// The package's top-level README. + File get authorsFile => directory.childFile('AUTHORS'); + + /// The lib directory containing the package's code. + Directory get libDirectory => directory.childDirectory('lib'); + + /// The test directory containing the package's Dart tests. + Directory get testDirectory => directory.childDirectory('test'); + + /// Returns the directory containing support for [platform]. + Directory platformDirectory(FlutterPlatform platform) { + late final String directoryName; + switch (platform) { + case FlutterPlatform.android: + directoryName = 'android'; + break; + case FlutterPlatform.ios: + directoryName = 'ios'; + break; + case FlutterPlatform.linux: + directoryName = 'linux'; + break; + case FlutterPlatform.macos: + directoryName = 'macos'; + break; + case FlutterPlatform.web: + directoryName = 'web'; + break; + case FlutterPlatform.windows: + directoryName = 'windows'; + break; + } + return directory.childDirectory(directoryName); + } + late final Pubspec _parsedPubspec = Pubspec.parse(pubspecFile.readAsStringSync()); 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 6b44ab78878..ad836e19d9c 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -30,11 +30,14 @@ class CreateAllPluginsAppCommand extends PluginCommand { 'Defaults to the repository root.'); } - /// The location of the synthesized app project. - Directory get appDirectory => packagesDir.fileSystem + /// The location to create the synthesized app project. + Directory get _appDirectory => packagesDir.fileSystem .directory(getStringArg(_outputDirectoryFlag)) .childDirectory('all_plugins'); + /// The synthesized app project. + RepositoryPackage get app => RepositoryPackage(_appDirectory); + @override String get description => 'Generate Flutter app that includes all plugins in packages.'; @@ -73,7 +76,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { '--template=app', '--project-name=all_plugins', '--android-language=java', - appDirectory.path, + _appDirectory.path, ], ); @@ -83,8 +86,8 @@ class CreateAllPluginsAppCommand extends PluginCommand { } Future _updateAppGradle() async { - final File gradleFile = appDirectory - .childDirectory('android') + final File gradleFile = app + .platformDirectory(FlutterPlatform.android) .childDirectory('app') .childFile('build.gradle'); if (!gradleFile.existsSync()) { @@ -119,8 +122,8 @@ class CreateAllPluginsAppCommand extends PluginCommand { } Future _updateManifest() async { - final File manifestFile = appDirectory - .childDirectory('android') + final File manifestFile = app + .platformDirectory(FlutterPlatform.android) .childDirectory('app') .childDirectory('src') .childDirectory('main') @@ -147,12 +150,11 @@ class CreateAllPluginsAppCommand extends PluginCommand { } Future _genPubspecWithAllPlugins() async { - final RepositoryPackage buildAllApp = RepositoryPackage(appDirectory); // Read the old pubspec file's Dart SDK version, in order to preserve it // in the new file. The template sometimes relies on having opted in to // specific language features via SDK version, so using a different one // can cause compilation failures. - final Pubspec originalPubspec = buildAllApp.parsePubspec(); + final Pubspec originalPubspec = app.parsePubspec(); const String dartSdkKey = 'sdk'; final VersionConstraint dartSdkConstraint = originalPubspec.environment?[dartSdkKey] ?? @@ -177,7 +179,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { }, dependencyOverrides: pluginDeps, ); - buildAllApp.pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + app.pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); } Future> _getValidPathDependencies() async { diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 6cc3c129c6b..4505259b731 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -146,7 +146,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { required RepositoryPackage package, }) async { final Directory androidDirectory = - example.directory.childDirectory('android'); + example.platformDirectory(FlutterPlatform.android); if (!androidDirectory.existsSync()) { return PackageResult.skip( '${example.displayName} does not support Android.'); @@ -171,7 +171,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } // Ensures that gradle wrapper exists - final GradleProject project = GradleProject(example.directory, + final GradleProject project = GradleProject(example, processRunner: processRunner, platform: platform); if (!await _ensureGradleWrapperExists(project)) { return PackageResult.fail(['Unable to build example apk']); diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index 467dc7942d9..8ba1d643a89 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -40,7 +40,7 @@ class LintAndroidCommand extends PackageLoopingCommand { bool failed = false; for (final RepositoryPackage example in package.getExamples()) { - final GradleProject project = GradleProject(example.directory, + final GradleProject project = GradleProject(example, processRunner: processRunner, platform: platform); if (!project.isConfigured()) { diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index 370fc3559f7..9b861c34ec9 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -178,7 +178,7 @@ dependency_overrides: for (final String packageName in packagesToOverride) { // Find the relative path from the common base to the local package. final List repoRelativePathComponents = path.split( - path.relative(localDependencies[packageName]!.directory.path, + path.relative(localDependencies[packageName]!.path, from: commonBasePath)); newPubspecContents += ''' $packageName: diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index a0d2ebd4e23..81b13cbb75e 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -198,22 +198,22 @@ this command. Future<_PlatformResult> _testAndroid( RepositoryPackage plugin, _TestMode mode) async { bool exampleHasUnitTests(RepositoryPackage example) { - return example.directory - .childDirectory('android') + return example + .platformDirectory(FlutterPlatform.android) .childDirectory('app') .childDirectory('src') .childDirectory('test') .existsSync() || - example.directory.parent - .childDirectory('android') + plugin + .platformDirectory(FlutterPlatform.android) .childDirectory('src') .childDirectory('test') .existsSync(); } bool exampleHasNativeIntegrationTests(RepositoryPackage example) { - final Directory integrationTestDirectory = example.directory - .childDirectory('android') + final Directory integrationTestDirectory = example + .platformDirectory(FlutterPlatform.android) .childDirectory('app') .childDirectory('src') .childDirectory('androidTest'); @@ -269,7 +269,7 @@ this command. _printRunningExampleTestsMessage(example, 'Android'); final GradleProject project = GradleProject( - example.directory, + example, processRunner: processRunner, platform: platform, ); diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index b6b83dabcb4..af8eac2257e 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -246,12 +246,12 @@ HTTP response: ${pubVersionFinderResponse.httpResponse.body} bool _passesAuthorsCheck(RepositoryPackage package) { final List pathComponents = - package.directory.fileSystem.path.split(package.directory.path); + package.directory.fileSystem.path.split(package.path); if (pathComponents.contains('third_party')) { // Third-party packages aren't required to have an AUTHORS file. return true; } - return package.directory.childFile('AUTHORS').existsSync(); + return package.authorsFile.existsSync(); } void _printImportantStatusMessage(String message, {required bool isError}) { diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index a1a995dbd88..27a01c95e85 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -41,7 +41,7 @@ class TestCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - if (!package.directory.childDirectory('test').existsSync()) { + if (!package.testDirectory.existsSync()) { return PackageResult.skip('No test/ directory.'); } diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index f69611f5a06..fb768c19b52 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -384,7 +384,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final Version fromPubspec = pubspec.version!; // get first version from CHANGELOG - final File changelog = package.directory.childFile('CHANGELOG.md'); + final File changelog = package.changelogFile; final List lines = changelog.readAsLinesSync(); String? firstLineWithText; final Iterator iterator = lines.iterator; diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 98fb9382a3a..e293e8b85e9 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -37,45 +37,45 @@ void main() { }); test('analyzes all packages', () async { - final Directory plugin1Dir = createFakePlugin('a', packagesDir); - final Directory plugin2Dir = createFakePlugin('b', packagesDir); + final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); + final RepositoryPackage plugin2 = createFakePlugin('b', packagesDir); await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin1Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin1Dir.path), - ProcessCall('flutter', const ['pub', 'get'], plugin2Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin2Dir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin1.path), + ProcessCall( + 'dart', const ['analyze', '--fatal-infos'], plugin1.path), + ProcessCall('flutter', const ['pub', 'get'], plugin2.path), + ProcessCall( + 'dart', const ['analyze', '--fatal-infos'], plugin2.path), ])); }); test('skips flutter pub get for examples', () async { - final Directory plugin1Dir = createFakePlugin('a', packagesDir); + final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin1Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin1Dir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin1.path), + ProcessCall( + 'dart', const ['analyze', '--fatal-infos'], plugin1.path), ])); }); test('runs flutter pub get for non-example subpackages', () async { - final Directory mainPackageDir = createFakePackage('a', packagesDir); - final Directory otherPackages = - mainPackageDir.childDirectory('other_packages'); - final Directory subpackage1 = - createFakePackage('subpackage1', otherPackages); - final Directory subpackage2 = - createFakePackage('subpackage2', otherPackages); + final RepositoryPackage mainPackage = createFakePackage('a', packagesDir); + final Directory otherPackagesDir = + mainPackage.directory.childDirectory('other_packages'); + final RepositoryPackage subpackage1 = + createFakePackage('subpackage1', otherPackagesDir); + final RepositoryPackage subpackage2 = + createFakePackage('subpackage2', otherPackagesDir); await runCapturingPrint(runner, ['analyze']); @@ -83,36 +83,36 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'flutter', const ['pub', 'get'], mainPackageDir.path), + 'flutter', const ['pub', 'get'], mainPackage.path), ProcessCall( 'flutter', const ['pub', 'get'], subpackage1.path), ProcessCall( 'flutter', const ['pub', 'get'], subpackage2.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], - mainPackageDir.path), + mainPackage.path), ])); }); test('don\'t elide a non-contained example package', () async { - final Directory plugin1Dir = createFakePlugin('a', packagesDir); - final Directory plugin2Dir = createFakePlugin('example', packagesDir); + final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); + final RepositoryPackage plugin2 = createFakePlugin('example', packagesDir); await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin1Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin1Dir.path), - ProcessCall('flutter', const ['pub', 'get'], plugin2Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin2Dir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin1.path), + ProcessCall( + 'dart', const ['analyze', '--fatal-infos'], plugin1.path), + ProcessCall('flutter', const ['pub', 'get'], plugin2.path), + ProcessCall( + 'dart', const ['analyze', '--fatal-infos'], plugin2.path), ])); }); test('uses a separate analysis sdk', () async { - final Directory pluginDir = createFakePlugin('a', packagesDir); + final RepositoryPackage plugin = createFakePlugin('a', packagesDir); await runCapturingPrint( runner, ['analyze', '--analysis-sdk', 'foo/bar/baz']); @@ -123,12 +123,12 @@ void main() { ProcessCall( 'flutter', const ['pub', 'get'], - pluginDir.path, + plugin.path, ), ProcessCall( 'foo/bar/baz/bin/dart', const ['analyze', '--fatal-infos'], - pluginDir.path, + plugin.path, ), ]), ); @@ -180,7 +180,7 @@ void main() { }); test('takes an allow list', () async { - final Directory pluginDir = createFakePlugin('foo', packagesDir, + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); await runCapturingPrint( @@ -189,15 +189,14 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['pub', 'get'], pluginDir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], - pluginDir.path), + plugin.path), ])); }); test('takes an allow config file', () async { - final Directory pluginDir = createFakePlugin('foo', packagesDir, + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); final File allowFile = packagesDir.childFile('custom.yaml'); allowFile.writeAsStringSync('- foo'); @@ -208,10 +207,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['pub', 'get'], pluginDir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], - pluginDir.path), + plugin.path), ])); }); @@ -280,7 +278,7 @@ void main() { // modify the script above, as it is run from source, but out-of-repo. // Contact stuartmorgan or devoncarew for assistance. test('Dart repo analyze command works', () async { - final Directory pluginDir = createFakePlugin('foo', packagesDir, + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); final File allowFile = packagesDir.childFile('custom.yaml'); allowFile.writeAsStringSync('- foo'); @@ -300,12 +298,12 @@ void main() { ProcessCall( 'flutter', const ['pub', 'get'], - pluginDir.path, + plugin.path, ), ProcessCall( 'foo/bar/baz/bin/dart', const ['analyze', '--fatal-infos'], - pluginDir.path, + plugin.path, ), ]), ); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 2bdb1bc0c2b..420b3b1161d 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -134,13 +134,12 @@ void main() { test('building for iOS', () async { mockPlatform.isMacOS = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformIOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, ['build-examples', '--ios', '--enable-experiment=exp1']); @@ -191,13 +190,12 @@ void main() { test('building for Linux', () async { mockPlatform.isLinux = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint( runner, ['build-examples', '--linux']); @@ -240,13 +238,12 @@ void main() { test('building for macOS', () async { mockPlatform.isMacOS = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint( runner, ['build-examples', '--macos']); @@ -286,13 +283,12 @@ void main() { }); test('building for web', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, ['build-examples', '--web']); @@ -336,13 +332,12 @@ void main() { test('building for Windows', () async { mockPlatform.isWindows = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint( runner, ['build-examples', '--windows']); @@ -386,13 +381,12 @@ void main() { }); test('building for Android', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'build-examples', @@ -415,13 +409,12 @@ void main() { }); test('enable-experiment flag for Android', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); await runCapturingPrint(runner, ['build-examples', '--apk', '--enable-experiment=exp1']); @@ -437,13 +430,12 @@ void main() { }); test('enable-experiment flag for ios', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformIOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); await runCapturingPrint(runner, ['build-examples', '--ios', '--enable-experiment=exp1']); @@ -481,7 +473,7 @@ void main() { group('packages', () { test('builds when requested platform is supported by example', () async { - final Directory packageDirectory = createFakePackage( + final RepositoryPackage package = createFakePackage( 'package', packagesDir, isFlutter: true, extraFiles: [ 'example/ios/Runner.xcodeproj/project.pbxproj' ]); @@ -507,7 +499,7 @@ void main() { 'ios', '--no-codesign', ], - packageDirectory.childDirectory('example').path), + getExampleDir(package).path), ])); }); @@ -567,7 +559,7 @@ void main() { }); test('logs skipped platforms when only some are supported', () async { - final Directory packageDirectory = createFakePackage( + final RepositoryPackage package = createFakePackage( 'package', packagesDir, isFlutter: true, extraFiles: ['example/linux/CMakeLists.txt']); @@ -590,21 +582,20 @@ void main() { ProcessCall( getFlutterCommand(mockPlatform), const ['build', 'linux'], - packageDirectory.childDirectory('example').path), + getExampleDir(package).path), ])); }); }); test('The .pluginToolsConfig.yaml file', () async { mockPlatform.isLinux = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final File pluginExampleConfigFile = pluginExampleDirectory.childFile('.pluginToolsConfig.yaml'); diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart index 3eac60baf3c..8df4a65b93a 100644 --- a/script/tool/test/common/gradle_test.dart +++ b/script/tool/test/common/gradle_test.dart @@ -23,7 +23,7 @@ void main() { group('isConfigured', () { test('reports true when configured on Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew.bat']); final GradleProject project = GradleProject( @@ -36,7 +36,7 @@ void main() { }); test('reports true when configured on non-Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew']); final GradleProject project = GradleProject( @@ -49,7 +49,7 @@ void main() { }); test('reports false when not configured on Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/foo']); final GradleProject project = GradleProject( @@ -62,7 +62,7 @@ void main() { }); test('reports true when configured on non-Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/foo']); final GradleProject project = GradleProject( @@ -75,9 +75,9 @@ void main() { }); }); - group('runXcodeBuild', () { + group('runCommand', () { test('runs without arguments', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew']); final GradleProject project = GradleProject( @@ -93,16 +93,19 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - plugin.childDirectory('android').childFile('gradlew').path, + plugin + .platformDirectory(FlutterPlatform.android) + .childFile('gradlew') + .path, const [ 'foo', ], - plugin.childDirectory('android').path), + plugin.platformDirectory(FlutterPlatform.android).path), ])); }); test('runs with arguments', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew']); final GradleProject project = GradleProject( @@ -121,18 +124,21 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - plugin.childDirectory('android').childFile('gradlew').path, + plugin + .platformDirectory(FlutterPlatform.android) + .childFile('gradlew') + .path, const [ 'foo', '--bar', '--baz', ], - plugin.childDirectory('android').path), + plugin.platformDirectory(FlutterPlatform.android).path), ])); }); test('runs with the correct wrapper on Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew.bat']); final GradleProject project = GradleProject( @@ -148,16 +154,19 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - plugin.childDirectory('android').childFile('gradlew.bat').path, + plugin + .platformDirectory(FlutterPlatform.android) + .childFile('gradlew.bat') + .path, const [ 'foo', ], - plugin.childDirectory('android').path), + plugin.platformDirectory(FlutterPlatform.android).path), ])); }); test('returns error codes', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew.bat']); final GradleProject project = GradleProject( diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index ea02bd4ea38..3dd6a47ae2f 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -11,7 +11,6 @@ import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; @@ -31,6 +30,21 @@ const String _startSuccessColor = '\x1B[32m'; const String _startWarningColor = '\x1B[33m'; const String _endColor = '\x1B[0m'; +// The filename within a package containing warnings to log during runForPackage. +enum _ResultFileType { + /// A file containing errors to return. + errors, + + /// A file containing warnings that should be logged. + warns, + + /// A file indicating that the package should be skipped, and why. + skips, + + /// A file indicating that the package should throw. + throws, +} + // The filename within a package containing errors to return from runForPackage. const String _errorFile = 'errors'; // The filename within a package indicating that it should be skipped. @@ -40,6 +54,30 @@ const String _warningFile = 'warnings'; // The filename within a package indicating that it should throw. const String _throwFile = 'throw'; +/// Writes a file to [package] to control the behavior of +/// [TestPackageLoopingCommand] for that package. +void _addResultFile(RepositoryPackage package, _ResultFileType type, + {String? contents}) { + final File file = package.directory.childFile(_filenameForType(type)); + file.createSync(); + if (contents != null) { + file.writeAsStringSync(contents); + } +} + +String _filenameForType(_ResultFileType type) { + switch (type) { + case _ResultFileType.errors: + return _errorFile; + case _ResultFileType.warns: + return _warningFile; + case _ResultFileType.skips: + return _skipFile; + case _ResultFileType.throws: + return _throwFile; + } +} + void main() { late FileSystem fileSystem; late MockPlatform mockPlatform; @@ -122,10 +160,10 @@ void main() { test('does not stop looping on error', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage = + final RepositoryPackage failingPackage = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - failingPackage.childFile(_errorFile).createSync(); + _addResultFile(failingPackage, _ResultFileType.errors); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -147,10 +185,10 @@ void main() { test('does not stop looping on exceptions', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage = + final RepositoryPackage failingPackage = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - failingPackage.childFile(_throwFile).createSync(); + _addResultFile(failingPackage, _ResultFileType.throws); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -173,8 +211,10 @@ void main() { group('package iteration', () { test('includes plugins and packages', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir); - final Directory package = createFakePackage('a_package', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); final TestPackageLoopingCommand command = createTestCommand(); await runCommand(command); @@ -184,8 +224,9 @@ void main() { }); test('includes third_party/packages', () async { - final Directory package1 = createFakePackage('a_package', packagesDir); - final Directory package2 = + final RepositoryPackage package1 = + createFakePackage('a_package', packagesDir); + final RepositoryPackage package2 = createFakePackage('another_package', thirdPartyPackagesDir); final TestPackageLoopingCommand command = createTestCommand(); @@ -196,9 +237,10 @@ void main() { }); test('includes subpackages when requested', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, examples: ['example1', 'example2']); - final Directory package = createFakePackage('a_package', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); final TestPackageLoopingCommand command = createTestCommand(includeSubpackages: true); @@ -208,39 +250,45 @@ void main() { command.checkedPackages, unorderedEquals([ plugin.path, - plugin.childDirectory('example').childDirectory('example1').path, - plugin.childDirectory('example').childDirectory('example2').path, + getExampleDir(plugin).childDirectory('example1').path, + getExampleDir(plugin).childDirectory('example2').path, package.path, - package.childDirectory('example').path, + getExampleDir(package).path, ])); }); test('excludes subpackages when main package is excluded', () async { - final Directory excluded = createFakePlugin('a_plugin', packagesDir, + final RepositoryPackage excluded = createFakePlugin( + 'a_plugin', packagesDir, examples: ['example1', 'example2']); - final Directory included = createFakePackage('a_package', packagesDir); + final RepositoryPackage included = + createFakePackage('a_package', packagesDir); final TestPackageLoopingCommand command = createTestCommand(includeSubpackages: true); await runCommand(command, arguments: ['--exclude=a_plugin']); + final Iterable examples = excluded.getExamples(); + expect( command.checkedPackages, unorderedEquals([ included.path, - included.childDirectory('example').path, + getExampleDir(included).path, ])); expect(command.checkedPackages, isNot(contains(excluded.path))); - expect(command.checkedPackages, - isNot(contains(excluded.childDirectory('example1').path))); - expect(command.checkedPackages, - isNot(contains(excluded.childDirectory('example2').path))); + expect(examples.length, 2); + for (final RepositoryPackage example in examples) { + expect(command.checkedPackages, isNot(contains(example.path))); + } }); test('skips unsupported versions when requested', () async { - final Directory excluded = createFakePlugin('a_plugin', packagesDir, + final RepositoryPackage excluded = createFakePlugin( + 'a_plugin', packagesDir, flutterConstraint: '>=2.10.0'); - final Directory included = createFakePackage('a_package', packagesDir); + final RepositoryPackage included = + createFakePackage('a_package', packagesDir); final TestPackageLoopingCommand command = createTestCommand(includeSubpackages: true, hasLongOutput: false); @@ -252,7 +300,7 @@ void main() { command.checkedPackages, unorderedEquals([ included.path, - included.childDirectory('example').path, + getExampleDir(included).path, ])); expect(command.checkedPackages, isNot(contains(excluded.path))); @@ -360,13 +408,13 @@ void main() { test('shows failure summaries when something fails without extra details', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage1 = + final RepositoryPackage failingPackage1 = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - final Directory failingPackage2 = + final RepositoryPackage failingPackage2 = createFakePlugin('package_d', packagesDir); - failingPackage1.childFile(_errorFile).createSync(); - failingPackage2.childFile(_errorFile).createSync(); + _addResultFile(failingPackage1, _ResultFileType.errors); + _addResultFile(failingPackage2, _ResultFileType.errors); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -390,13 +438,13 @@ void main() { test('uses custom summary header and footer if provided', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage1 = + final RepositoryPackage failingPackage1 = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - final Directory failingPackage2 = + final RepositoryPackage failingPackage2 = createFakePlugin('package_d', packagesDir); - failingPackage1.childFile(_errorFile).createSync(); - failingPackage2.childFile(_errorFile).createSync(); + _addResultFile(failingPackage1, _ResultFileType.errors); + _addResultFile(failingPackage2, _ResultFileType.errors); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, @@ -423,17 +471,15 @@ void main() { test('shows failure summaries when something fails with extra details', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage1 = + final RepositoryPackage failingPackage1 = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - final Directory failingPackage2 = + final RepositoryPackage failingPackage2 = createFakePlugin('package_d', packagesDir); - final File errorFile1 = failingPackage1.childFile(_errorFile); - errorFile1.createSync(); - errorFile1.writeAsStringSync('just one detail'); - final File errorFile2 = failingPackage2.childFile(_errorFile); - errorFile2.createSync(); - errorFile2.writeAsStringSync('first detail\nsecond detail'); + _addResultFile(failingPackage1, _ResultFileType.errors, + contents: 'just one detail'); + _addResultFile(failingPackage2, _ResultFileType.errors, + contents: 'first detail\nsecond detail'); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -479,8 +525,10 @@ void main() { test('logs skips', () async { createFakePackage('package_a', packagesDir); - final Directory skipPackage = createFakePackage('package_b', packagesDir); - skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); + final RepositoryPackage skipPackage = + createFakePackage('package_b', packagesDir); + _addResultFile(skipPackage, _ResultFileType.skips, + contents: 'For a reason'); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -513,10 +561,10 @@ void main() { }); test('logs warnings', () async { - final Directory warnPackage = createFakePackage('package_a', packagesDir); - warnPackage - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + final RepositoryPackage warnPackage = + createFakePackage('package_a', packagesDir); + _addResultFile(warnPackage, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = @@ -535,10 +583,10 @@ void main() { test('logs unhandled exceptions as errors', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage = + final RepositoryPackage failingPackage = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - failingPackage.childFile(_throwFile).createSync(); + _addResultFile(failingPackage, _ResultFileType.throws); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -559,23 +607,30 @@ void main() { }); test('prints run summary on success', () async { - final Directory warnPackage1 = + final RepositoryPackage warnPackage1 = createFakePackage('package_a', packagesDir); - warnPackage1 - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + _addResultFile(warnPackage1, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); - final Directory skipPackage = createFakePackage('package_c', packagesDir); - skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); - final Directory skipAndWarnPackage = + + final RepositoryPackage skipPackage = + createFakePackage('package_c', packagesDir); + _addResultFile(skipPackage, _ResultFileType.skips, + contents: 'For a reason'); + + final RepositoryPackage skipAndWarnPackage = createFakePackage('package_d', packagesDir); - skipAndWarnPackage.childFile(_warningFile).writeAsStringSync('Warning'); - skipAndWarnPackage.childFile(_skipFile).writeAsStringSync('See warning'); - final Directory warnPackage2 = + _addResultFile(skipAndWarnPackage, _ResultFileType.warns, + contents: 'Warning'); + _addResultFile(skipAndWarnPackage, _ResultFileType.skips, + contents: 'See warning'); + + final RepositoryPackage warnPackage2 = createFakePackage('package_e', packagesDir); - warnPackage2 - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + _addResultFile(warnPackage2, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); + createFakePackage('package_f', packagesDir); final TestPackageLoopingCommand command = @@ -615,23 +670,30 @@ void main() { }); test('prints long-form run summary for long-output commands', () async { - final Directory warnPackage1 = + final RepositoryPackage warnPackage1 = createFakePackage('package_a', packagesDir); - warnPackage1 - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + _addResultFile(warnPackage1, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); - final Directory skipPackage = createFakePackage('package_c', packagesDir); - skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); - final Directory skipAndWarnPackage = + + final RepositoryPackage skipPackage = + createFakePackage('package_c', packagesDir); + _addResultFile(skipPackage, _ResultFileType.skips, + contents: 'For a reason'); + + final RepositoryPackage skipAndWarnPackage = createFakePackage('package_d', packagesDir); - skipAndWarnPackage.childFile(_warningFile).writeAsStringSync('Warning'); - skipAndWarnPackage.childFile(_skipFile).writeAsStringSync('See warning'); - final Directory warnPackage2 = + _addResultFile(skipAndWarnPackage, _ResultFileType.warns, + contents: 'Warning'); + _addResultFile(skipAndWarnPackage, _ResultFileType.skips, + contents: 'See warning'); + + final RepositoryPackage warnPackage2 = createFakePackage('package_e', packagesDir); - warnPackage2 - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + _addResultFile(warnPackage2, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); + createFakePackage('package_f', packagesDir); final TestPackageLoopingCommand command = diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 782ea7267ae..7ed3d239b2a 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -62,18 +62,24 @@ void main() { group('plugin iteration', () { test('all plugins from file system', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); }); test('includes both plugins and packages', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - final Directory package3 = createFakePackage('package3', packagesDir); - final Directory package4 = createFakePackage('package4', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + final RepositoryPackage package3 = + createFakePackage('package3', packagesDir); + final RepositoryPackage package4 = + createFakePackage('package4', packagesDir); await runCapturingPrint(runner, ['sample']); expect( command.plugins, @@ -85,10 +91,25 @@ void main() { ])); }); + test('includes packages without source', () async { + final RepositoryPackage package = + createFakePackage('package', packagesDir); + package.libDirectory.deleteSync(recursive: true); + + await runCapturingPrint(runner, ['sample']); + expect( + command.plugins, + unorderedEquals([ + package.path, + ])); + }); + test('all plugins includes third_party/packages', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - final Directory plugin3 = + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin3 = createFakePlugin('plugin3', thirdPartyPackagesDir); await runCapturingPrint(runner, ['sample']); expect(command.plugins, @@ -96,10 +117,12 @@ void main() { }); test('--packages limits packages', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); createFakePackage('package3', packagesDir); - final Directory package4 = createFakePackage('package4', packagesDir); + final RepositoryPackage package4 = + createFakePackage('package4', packagesDir); await runCapturingPrint( runner, ['sample', '--packages=plugin1,package4']); expect( @@ -111,10 +134,12 @@ void main() { }); test('--plugins acts as an alias to --packages', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); createFakePackage('package3', packagesDir); - final Directory package4 = createFakePackage('package4', packagesDir); + final RepositoryPackage package4 = + createFakePackage('package4', packagesDir); await runCapturingPrint( runner, ['sample', '--plugins=plugin1,package4']); expect( @@ -127,7 +152,8 @@ void main() { test('exclude packages when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ 'sample', '--packages=plugin1,plugin2', @@ -146,7 +172,8 @@ void main() { test('exclude federated plugins when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir.childDirectory('federated')); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ 'sample', '--packages=federated/plugin1,plugin2', @@ -158,7 +185,8 @@ void main() { test('exclude entire federated plugins when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir.childDirectory('federated')); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ 'sample', '--packages=federated/plugin1,plugin2', @@ -189,11 +217,11 @@ packages/plugin1/plugin1/plugin1.dart '''), ]; final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - final Directory appFacingPackage = + final RepositoryPackage appFacingPackage = createFakePlugin('plugin1', pluginGroup); - final Directory platformInterfacePackage = + final RepositoryPackage platformInterfacePackage = createFakePlugin('plugin1_platform_interface', pluginGroup); - final Directory implementationPackage = + final RepositoryPackage implementationPackage = createFakePlugin('plugin1_web', pluginGroup); await runCapturingPrint( @@ -217,7 +245,7 @@ packages/plugin1/plugin1/plugin1.dart '''), ]; final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - final Directory appFacingPackage = + final RepositoryPackage appFacingPackage = createFakePlugin('plugin1', pluginGroup); createFakePlugin('plugin1_platform_interface', pluginGroup); createFakePlugin('plugin1_web', pluginGroup); @@ -239,7 +267,7 @@ packages/plugin1/plugin1/plugin1.dart final Directory pluginGroup = packagesDir.childDirectory('plugin1'); createFakePlugin('plugin1', pluginGroup); - final Directory platformInterfacePackage = + final RepositoryPackage platformInterfacePackage = createFakePlugin('plugin1_platform_interface', pluginGroup); createFakePlugin('plugin1_web', pluginGroup); @@ -265,14 +293,15 @@ packages/plugin1/plugin1/plugin1.dart CommandRunner('common_command', 'subpackage testing'); localRunner.addCommand(localCommand); - final Directory package = createFakePackage('apackage', packagesDir); + final RepositoryPackage package = + createFakePackage('apackage', packagesDir); await runCapturingPrint(localRunner, ['sample']); expect( localCommand.plugins, containsAllInOrder([ package.path, - package.childDirectory('example').path, + getExampleDir(package).path, ])); }); @@ -340,8 +369,10 @@ packages/plugin1/plugin1/plugin1.dart group('test run-on-changed-packages', () { test('all plugins should be tested if there are no changes.', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -355,8 +386,10 @@ packages/plugin1/plugin1/plugin1.dart processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'AUTHORS'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -371,8 +404,10 @@ packages/plugin1/plugin1/plugin1.dart packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -387,8 +422,10 @@ packages/plugin1/CHANGELOG packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -404,8 +441,10 @@ packages/plugin1/CHANGELOG packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -421,8 +460,10 @@ script/tool_runner.sh packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -438,8 +479,10 @@ analysis_options.yaml packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -455,8 +498,10 @@ packages/plugin1/CHANGELOG packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -468,7 +513,8 @@ packages/plugin1/CHANGELOG processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'packages/plugin1/plugin1.dart'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -491,7 +537,8 @@ packages/plugin1/plugin1.dart packages/plugin1/ios/plugin1.m '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -507,8 +554,10 @@ packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -527,7 +576,7 @@ packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart packages/plugin1/plugin1_web/plugin1_web.dart '''), ]; - final Directory plugin1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); @@ -545,7 +594,7 @@ packages/plugin1/plugin1_web/plugin1_web.dart packages/plugin1/plugin1/plugin1.dart '''), ]; - final Directory plugin1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin1_platform_interface', packagesDir.childDirectory('plugin1')); @@ -564,7 +613,7 @@ packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''), ]; - final Directory plugin1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); @@ -624,7 +673,8 @@ script/tool_runner.sh processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'packages/a_package/lib/a_package.dart'), ]; - final Directory packageA = createFakePackage('a_package', packagesDir); + final RepositoryPackage packageA = + createFakePackage('a_package', packagesDir); createFakePlugin('b_package', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--run-on-dirty-packages']); @@ -647,8 +697,10 @@ packages/a_package/lib/a_package.dart packages/b_package/lib/src/foo.dart '''), ]; - final Directory packageA = createFakePackage('a_package', packagesDir); - final Directory packageB = createFakePackage('b_package', packagesDir); + final RepositoryPackage packageA = + createFakePackage('a_package', packagesDir); + final RepositoryPackage packageB = + createFakePackage('b_package', packagesDir); createFakePackage('c_package', packagesDir); await runCapturingPrint( runner, ['sample', '--run-on-dirty-packages']); @@ -664,7 +716,8 @@ packages/a_package/lib/a_package.dart packages/b_package/lib/src/foo.dart '''), ]; - final Directory packageA = createFakePackage('a_package', packagesDir); + final RepositoryPackage packageA = + createFakePackage('a_package', packagesDir); createFakePackage('b_package', packagesDir); createFakePackage('c_package', packagesDir); await runCapturingPrint(runner, [ @@ -686,7 +739,8 @@ packages/b_package/lib/src/foo.dart processRunner.mockProcessesForExecutable['git-rev-parse'] = [ MockProcess(stdout: 'a-branch'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( @@ -707,8 +761,10 @@ packages/b_package/lib/src/foo.dart processRunner.mockProcessesForExecutable['git-rev-parse'] = [ MockProcess(stdout: 'main'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); @@ -729,8 +785,10 @@ packages/b_package/lib/src/foo.dart processRunner.mockProcessesForExecutable['git-rev-parse'] = [ MockProcess(stdout: 'master'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); @@ -770,18 +828,19 @@ packages/b_package/lib/src/foo.dart group('sharding', () { test('distributes evenly when evenly divisible', () async { - final List> expectedShards = >[ - [ + final List> expectedShards = + >[ + [ createFakePackage('package1', packagesDir), createFakePackage('package2', packagesDir), createFakePackage('package3', packagesDir), ], - [ + [ createFakePackage('package4', packagesDir), createFakePackage('package5', packagesDir), createFakePackage('package6', packagesDir), ], - [ + [ createFakePackage('package7', packagesDir), createFakePackage('package8', packagesDir), createFakePackage('package9', packagesDir), @@ -807,25 +866,26 @@ packages/b_package/lib/src/foo.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory packageDir) => packageDir.path) + .map((RepositoryPackage package) => package.path) .toList())); } }); test('distributes as evenly as possible when not evenly divisible', () async { - final List> expectedShards = >[ - [ + final List> expectedShards = + >[ + [ createFakePackage('package1', packagesDir), createFakePackage('package2', packagesDir), createFakePackage('package3', packagesDir), ], - [ + [ createFakePackage('package4', packagesDir), createFakePackage('package5', packagesDir), createFakePackage('package6', packagesDir), ], - [ + [ createFakePackage('package7', packagesDir), createFakePackage('package8', packagesDir), ], @@ -850,7 +910,7 @@ packages/b_package/lib/src/foo.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory packageDir) => packageDir.path) + .map((RepositoryPackage package) => package.path) .toList())); } }); @@ -864,18 +924,19 @@ packages/b_package/lib/src/foo.dart // excluding some plugins from the later step shouldn't change what's tested // in each shard, as it may no longer align with what was built. test('counts excluded plugins when sharding', () async { - final List> expectedShards = >[ - [ + final List> expectedShards = + >[ + [ createFakePackage('package1', packagesDir), createFakePackage('package2', packagesDir), createFakePackage('package3', packagesDir), ], - [ + [ createFakePackage('package4', packagesDir), createFakePackage('package5', packagesDir), createFakePackage('package6', packagesDir), ], - [ + [ createFakePackage('package7', packagesDir), ], ]; @@ -903,7 +964,7 @@ packages/b_package/lib/src/foo.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory packageDir) => packageDir.path) + .map((RepositoryPackage package) => package.path) .toList())); } }); diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index af5cb7dfe4c..9c5ddc3f85b 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -6,7 +6,6 @@ import 'package:file/file.dart'; import 'package:file/memory.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/common/repository_package.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -22,8 +21,7 @@ void main() { group('pluginSupportsPlatform', () { test('no platforms', () async { - final RepositoryPackage plugin = - RepositoryPackage(createFakePlugin('plugin', packagesDir)); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); expect(pluginSupportsPlatform(platformAndroid, plugin), isFalse); expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); @@ -34,8 +32,7 @@ void main() { }); test('all platforms', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), platformIOS: const PlatformDetails(PlatformSupport.inline), @@ -43,7 +40,7 @@ void main() { platformMacOS: const PlatformDetails(PlatformSupport.inline), platformWeb: const PlatformDetails(PlatformSupport.inline), platformWindows: const PlatformDetails(PlatformSupport.inline), - })); + }); expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(platformIOS, plugin), isTrue); @@ -54,13 +51,12 @@ void main() { }); test('some platforms', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), platformLinux: const PlatformDetails(PlatformSupport.inline), platformWeb: const PlatformDetails(PlatformSupport.inline), - })); + }); expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); @@ -71,8 +67,7 @@ void main() { }); test('inline plugins are only detected as inline', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), platformIOS: const PlatformDetails(PlatformSupport.inline), @@ -80,7 +75,7 @@ void main() { platformMacOS: const PlatformDetails(PlatformSupport.inline), platformWeb: const PlatformDetails(PlatformSupport.inline), platformWindows: const PlatformDetails(PlatformSupport.inline), - })); + }); expect( pluginSupportsPlatform(platformAndroid, plugin, @@ -133,8 +128,7 @@ void main() { }); test('federated plugins are only detected as federated', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.federated), platformIOS: const PlatformDetails(PlatformSupport.federated), @@ -142,7 +136,7 @@ void main() { platformMacOS: const PlatformDetails(PlatformSupport.federated), platformWeb: const PlatformDetails(PlatformSupport.federated), platformWindows: const PlatformDetails(PlatformSupport.federated), - })); + }); expect( pluginSupportsPlatform(platformAndroid, plugin, @@ -197,19 +191,19 @@ void main() { group('pluginHasNativeCodeForPlatform', () { test('returns false for web', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), }, - )); + ); expect(pluginHasNativeCodeForPlatform(platformWeb, plugin), isFalse); }); test('returns false for a native-only plugin', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -217,7 +211,7 @@ void main() { platformMacOS: const PlatformDetails(PlatformSupport.inline), platformWindows: const PlatformDetails(PlatformSupport.inline), }, - )); + ); expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); @@ -225,7 +219,7 @@ void main() { }); test('returns true for a native+Dart plugin', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -236,7 +230,7 @@ void main() { platformWindows: const PlatformDetails(PlatformSupport.inline, hasNativeCode: true, hasDartCode: true), }, - )); + ); expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); @@ -244,7 +238,7 @@ void main() { }); test('returns false for a Dart-only plugin', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -255,7 +249,7 @@ void main() { platformWindows: const PlatformDetails(PlatformSupport.inline, hasNativeCode: false, hasDartCode: true), }, - )); + ); expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isFalse); expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isFalse); diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index 2d0e11475c3..dadfc883299 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -4,7 +4,6 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -97,107 +96,109 @@ void main() { group('getExamples', () { test('handles a single Flutter example', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); - final List examples = - RepositoryPackage(plugin).getExamples().toList(); + final List examples = plugin.getExamples().toList(); expect(examples.length, 1); - expect(examples[0].path, plugin.childDirectory('example').path); + expect(examples[0].path, getExampleDir(plugin).path); }); test('handles multiple Flutter examples', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, examples: ['example1', 'example2']); - final List examples = - RepositoryPackage(plugin).getExamples().toList(); + final List examples = plugin.getExamples().toList(); expect(examples.length, 2); expect(examples[0].path, - plugin.childDirectory('example').childDirectory('example1').path); + getExampleDir(plugin).childDirectory('example1').path); expect(examples[1].path, - plugin.childDirectory('example').childDirectory('example2').path); + getExampleDir(plugin).childDirectory('example2').path); }); test('handles a single non-Flutter example', () async { - final Directory package = createFakePackage('a_package', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); - final List examples = - RepositoryPackage(package).getExamples().toList(); + final List examples = package.getExamples().toList(); expect(examples.length, 1); - expect(examples[0].path, package.childDirectory('example').path); + expect(examples[0].path, getExampleDir(package).path); }); test('handles multiple non-Flutter examples', () async { - final Directory package = createFakePackage('a_package', packagesDir, + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, examples: ['example1', 'example2']); - final List examples = - RepositoryPackage(package).getExamples().toList(); + final List examples = package.getExamples().toList(); expect(examples.length, 2); expect(examples[0].path, - package.childDirectory('example').childDirectory('example1').path); + getExampleDir(package).childDirectory('example1').path); expect(examples[1].path, - package.childDirectory('example').childDirectory('example2').path); + getExampleDir(package).childDirectory('example2').path); }); }); group('federated plugin queries', () { test('all return false for a simple plugin', () { - final Directory plugin = createFakePlugin('a_plugin', packagesDir); - expect(RepositoryPackage(plugin).isFederated, false); - expect(RepositoryPackage(plugin).isAppFacing, false); - expect(RepositoryPackage(plugin).isPlatformInterface, false); - expect(RepositoryPackage(plugin).isFederated, false); + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); + expect(plugin.isFederated, false); + expect(plugin.isAppFacing, false); + expect(plugin.isPlatformInterface, false); + expect(plugin.isFederated, false); }); test('handle app-facing packages', () { - final Directory plugin = + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); - expect(RepositoryPackage(plugin).isFederated, true); - expect(RepositoryPackage(plugin).isAppFacing, true); - expect(RepositoryPackage(plugin).isPlatformInterface, false); - expect(RepositoryPackage(plugin).isPlatformImplementation, false); + expect(plugin.isFederated, true); + expect(plugin.isAppFacing, true); + expect(plugin.isPlatformInterface, false); + expect(plugin.isPlatformImplementation, false); }); test('handle platform interface packages', () { - final Directory plugin = createFakePlugin('a_plugin_platform_interface', + final RepositoryPackage plugin = createFakePlugin( + 'a_plugin_platform_interface', packagesDir.childDirectory('a_plugin')); - expect(RepositoryPackage(plugin).isFederated, true); - expect(RepositoryPackage(plugin).isAppFacing, false); - expect(RepositoryPackage(plugin).isPlatformInterface, true); - expect(RepositoryPackage(plugin).isPlatformImplementation, false); + expect(plugin.isFederated, true); + expect(plugin.isAppFacing, false); + expect(plugin.isPlatformInterface, true); + expect(plugin.isPlatformImplementation, false); }); test('handle platform implementation packages', () { // A platform interface can end with anything, not just one of the known // platform names, because of cases like webview_flutter_wkwebview. - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); - expect(RepositoryPackage(plugin).isFederated, true); - expect(RepositoryPackage(plugin).isAppFacing, false); - expect(RepositoryPackage(plugin).isPlatformInterface, false); - expect(RepositoryPackage(plugin).isPlatformImplementation, true); + expect(plugin.isFederated, true); + expect(plugin.isAppFacing, false); + expect(plugin.isPlatformInterface, false); + expect(plugin.isPlatformImplementation, true); }); }); group('pubspec', () { test('file', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); - final File pubspecFile = RepositoryPackage(plugin).pubspecFile; + final File pubspecFile = plugin.pubspecFile; - expect(pubspecFile.path, plugin.childFile('pubspec.yaml').path); + expect(pubspecFile.path, plugin.directory.childFile('pubspec.yaml').path); }); test('parsing', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, examples: ['example1', 'example2']); - final Pubspec pubspec = RepositoryPackage(plugin).parsePubspec(); + final Pubspec pubspec = plugin.parsePubspec(); expect(pubspec.name, 'a_plugin'); }); @@ -205,15 +206,15 @@ void main() { group('requiresFlutter', () { test('returns true for Flutter package', () async { - final Directory package = + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); - expect(RepositoryPackage(package).requiresFlutter(), true); + expect(package.requiresFlutter(), true); }); test('returns false for non-Flutter package', () async { - final Directory package = + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: false); - expect(RepositoryPackage(package).requiresFlutter(), false); + expect(package.requiresFlutter(), false); }); }); } diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 9e2ee29326d..830dd59a8d4 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -7,7 +7,6 @@ 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/repository_package.dart'; import 'package:flutter_plugin_tools/src/create_all_plugins_app_command.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; @@ -49,8 +48,7 @@ void main() { createFakePlugin('pluginc', packagesDir); await runCapturingPrint(runner, ['all-plugins-app']); - final List pubspec = - command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); + final List pubspec = command.app.pubspecFile.readAsLinesSync(); expect( pubspec, @@ -67,8 +65,7 @@ void main() { createFakePlugin('pluginc', packagesDir); await runCapturingPrint(runner, ['all-plugins-app']); - final List pubspec = - command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); + final List pubspec = command.app.pubspecFile.readAsLinesSync(); expect( pubspec, @@ -99,8 +96,7 @@ void main() { createFakePlugin('plugina', packagesDir); await runCapturingPrint(runner, ['all-plugins-app']); - final Pubspec generatedPubspec = - RepositoryPackage(command.appDirectory).parsePubspec(); + final Pubspec generatedPubspec = command.app.parsePubspec(); const String dartSdkKey = 'sdk'; expect(generatedPubspec.environment?[dartSdkKey], @@ -115,8 +111,8 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app', '--output-dir=${customOutputDir.path}']); - expect(command.appDirectory.path, - customOutputDir.childDirectory('all_plugins').path); + expect( + command.app.path, customOutputDir.childDirectory('all_plugins').path); }); test('logs exclusions', () async { diff --git a/script/tool/test/custom_test_command_test.dart b/script/tool/test/custom_test_command_test.dart index bc30d9a1d2e..54a1acf8b82 100644 --- a/script/tool/test/custom_test_command_test.dart +++ b/script/tool/test/custom_test_command_test.dart @@ -39,7 +39,7 @@ void main() { }); test('runs both new and legacy when both are present', () async { - final Directory package = + final RepositoryPackage package = createFakePlugin('a_package', packagesDir, extraFiles: [ 'tool/run_tests.dart', 'run_tests.sh', @@ -51,7 +51,7 @@ void main() { expect( processRunner.recordedCalls, containsAll([ - ProcessCall(package.childFile('run_tests.sh').path, + ProcessCall(package.directory.childFile('run_tests.sh').path, const [], package.path), ProcessCall('dart', const ['run', 'tool/run_tests.dart'], package.path), @@ -65,7 +65,8 @@ void main() { }); test('runs when only new is present', () async { - final Directory package = createFakePlugin('a_package', packagesDir, + final RepositoryPackage package = createFakePlugin( + 'a_package', packagesDir, extraFiles: ['tool/run_tests.dart']); final List output = @@ -86,7 +87,8 @@ void main() { }); test('runs pub get before running Dart test script', () async { - final Directory package = createFakePlugin('a_package', packagesDir, + final RepositoryPackage package = createFakePlugin( + 'a_package', packagesDir, extraFiles: ['tool/run_tests.dart']); await runCapturingPrint(runner, ['custom-test']); @@ -101,7 +103,8 @@ void main() { }); test('runs when only legacy is present', () async { - final Directory package = createFakePlugin('a_package', packagesDir, + final RepositoryPackage package = createFakePlugin( + 'a_package', packagesDir, extraFiles: ['run_tests.sh']); final List output = @@ -110,7 +113,7 @@ void main() { expect( processRunner.recordedCalls, containsAll([ - ProcessCall(package.childFile('run_tests.sh').path, + ProcessCall(package.directory.childFile('run_tests.sh').path, const [], package.path), ])); @@ -189,14 +192,14 @@ void main() { }); test('fails if legacy fails', () async { - final Directory package = + final RepositoryPackage package = createFakePlugin('a_package', packagesDir, extraFiles: [ 'tool/run_tests.dart', 'run_tests.sh', ]); processRunner.mockProcessesForExecutable[ - package.childFile('run_tests.sh').path] = [ + package.directory.childFile('run_tests.sh').path] = [ MockProcess(exitCode: 1), ]; @@ -234,7 +237,7 @@ void main() { }); test('runs new and skips old when both are present', () async { - final Directory package = + final RepositoryPackage package = createFakePlugin('a_package', packagesDir, extraFiles: [ 'tool/run_tests.dart', 'run_tests.sh', @@ -258,7 +261,8 @@ void main() { }); test('runs when only new is present', () async { - final Directory package = createFakePlugin('a_package', packagesDir, + final RepositoryPackage package = createFakePlugin( + 'a_package', packagesDir, extraFiles: ['tool/run_tests.dart']); final List output = diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 214efb42022..23318f7cd60 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -188,7 +188,7 @@ void main() { }); test('driving under folder "test_driver"', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -203,8 +203,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); final List output = @@ -311,7 +310,7 @@ void main() { test( 'driving under folder "test_driver" when targets are under "integration_test"', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -328,8 +327,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); final List output = @@ -401,7 +399,7 @@ void main() { }); test('driving on a Linux plugin', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -414,8 +412,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -474,7 +471,7 @@ void main() { }); test('driving on a macOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -487,8 +484,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -546,7 +542,7 @@ void main() { }); test('driving a web plugin', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -559,8 +555,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -596,7 +591,7 @@ void main() { }); test('driving a web plugin with CHROME_EXECUTABLE', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -609,8 +604,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); mockPlatform.environment['CHROME_EXECUTABLE'] = '/path/to/chrome'; @@ -674,7 +668,7 @@ void main() { }); test('driving on a Windows plugin', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -687,8 +681,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -722,7 +715,7 @@ void main() { }); test('driving on an Android plugin', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -735,8 +728,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); final List output = await runCapturingPrint(runner, [ @@ -861,7 +853,7 @@ void main() { }); test('enable-experiment flag', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -876,8 +868,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); await runCapturingPrint(runner, [ @@ -1006,7 +997,7 @@ void main() { }); test('reports test failures', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -1048,8 +1039,7 @@ void main() { ]), ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); expect( processRunner.recordedCalls, orderedEquals([ @@ -1082,13 +1072,13 @@ void main() { group('packages', () { test('can be driven', () async { - final Directory package = + final RepositoryPackage package = createFakePackage('a_package', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/test_driver/integration_test.dart', 'example/web/index.html', ]); - final Directory exampleDirectory = package.childDirectory('example'); + final Directory exampleDirectory = getExampleDir(package); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -1150,7 +1140,8 @@ void main() { }); test('drive only supported examples if there is more than one', () async { - final Directory package = createFakePackage('a_package', packagesDir, + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, isFlutter: true, examples: [ 'with_web', @@ -1164,7 +1155,7 @@ void main() { 'example/without_web/test_driver/integration_test.dart', ]); final Directory supportedExampleDirectory = - package.childDirectory('example').childDirectory('with_web'); + getExampleDir(package).childDirectory('with_web'); final List output = await runCapturingPrint(runner, [ 'drive-examples', diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart index 126aa8b41c8..015a0eb634d 100644 --- a/script/tool/test/federation_safety_check_command_test.dart +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -8,7 +8,6 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:flutter_plugin_tools/src/federation_safety_check_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -55,10 +54,10 @@ void main() { }); test('skips non-plugin packages', () async { - final Directory package = createFakePackage('foo', packagesDir); + final RepositoryPackage package = createFakePackage('foo', packagesDir); final String changedFileOutput = [ - package.childDirectory('lib').childFile('foo.dart'), + package.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -78,10 +77,10 @@ void main() { }); test('skips unfederated plugins', () async { - final Directory package = createFakePlugin('foo', packagesDir); + final RepositoryPackage package = createFakePlugin('foo', packagesDir); final String changedFileOutput = [ - package.childDirectory('lib').childFile('foo.dart'), + package.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -102,11 +101,11 @@ void main() { test('skips interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - platformInterface.childDirectory('lib').childFile('foo.dart'), + platformInterface.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -127,15 +126,15 @@ void main() { test('allows changes to just an interface package', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); createFakePlugin('foo', pluginGroupDir); createFakePlugin('foo_ios', pluginGroupDir); createFakePlugin('foo_android', pluginGroupDir); final String changedFileOutput = [ - platformInterface.childDirectory('lib').childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), + platformInterface.libDirectory.childFile('foo.dart'), + platformInterface.pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -168,14 +167,14 @@ void main() { test('allows changes to multiple non-interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), + appFacing.libDirectory.childFile('foo.dart'), + implementation.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -199,17 +198,17 @@ void main() { 'fails on changes to interface and non-interface packages in the same plugin', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), - platformInterface.childDirectory('lib').childFile('foo.dart'), + appFacing.libDirectory.childFile('foo.dart'), + implementation.libDirectory.childFile('foo.dart'), + platformInterface.pubspecFile, + platformInterface.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -244,17 +243,17 @@ void main() { test('ignores test-only changes to interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), - platformInterface.childDirectory('test').childFile('foo.dart'), + appFacing.libDirectory.childFile('foo.dart'), + implementation.libDirectory.childFile('foo.dart'), + platformInterface.pubspecFile, + platformInterface.testDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -276,27 +275,24 @@ void main() { test('ignores unpublished changes to interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), - platformInterface.childDirectory('lib').childFile('foo.dart'), + appFacing.libDirectory.childFile('foo.dart'), + implementation.libDirectory.childFile('foo.dart'), + platformInterface.pubspecFile, + platformInterface.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; // Simulate no change to the version in the interface's pubspec.yaml. processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess( - stdout: RepositoryPackage(platformInterface) - .pubspecFile - .readAsStringSync()), + MockProcess(stdout: platformInterface.pubspecFile.readAsStringSync()), ]; final List output = @@ -315,22 +311,22 @@ void main() { test('allows things that look like mass changes, with warning', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); - final Directory otherPlugin1 = createFakePlugin('bar', packagesDir); - final Directory otherPlugin2 = createFakePlugin('baz', packagesDir); + final RepositoryPackage otherPlugin1 = createFakePlugin('bar', packagesDir); + final RepositoryPackage otherPlugin2 = createFakePlugin('baz', packagesDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), - platformInterface.childDirectory('lib').childFile('foo.dart'), - otherPlugin1.childFile('bar.dart'), - otherPlugin2.childFile('baz.dart'), + appFacing.libDirectory.childFile('foo.dart'), + implementation.libDirectory.childFile('foo.dart'), + platformInterface.pubspecFile, + platformInterface.libDirectory.childFile('foo.dart'), + otherPlugin1.libDirectory.childFile('bar.dart'), + otherPlugin2.libDirectory.childFile('baz.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -355,11 +351,11 @@ void main() { test('handles top-level files that match federated package heuristics', () async { - final Directory plugin = createFakePlugin('foo', packagesDir); + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); final String changedFileOutput = [ // This should be picked up as a change to 'foo', and not crash. - plugin.childFile('foo_bar.baz'), + plugin.directory.childFile('foo_bar.baz'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index db658e19b28..a4140939472 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -40,9 +40,10 @@ void main() { runner.addCommand(command); }); - void _writeJavaTestFile(Directory pluginDir, String relativeFilePath, + void _writeJavaTestFile(RepositoryPackage plugin, String relativeFilePath, {String runnerClass = 'FlutterTestRunner'}) { - childFileWithSubcomponents(pluginDir, p.posix.split(relativeFilePath)) + childFileWithSubcomponents( + plugin.directory, p.posix.split(relativeFilePath)) .writeAsStringSync(''' @DartIntegrationTest @RunWith($runnerClass.class) @@ -60,13 +61,13 @@ public class MainActivityTest { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -90,13 +91,13 @@ public class MainActivityTest { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, ['firebase-test-lab']); @@ -112,22 +113,22 @@ public class MainActivityTest { test('only runs gcloud configuration once', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory plugin1Dir = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin1Dir, javaTestFileRelativePath); - final Directory plugin2Dir = + _writeJavaTestFile(plugin1, javaTestFileRelativePath); + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/bar_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin2Dir, javaTestFileRelativePath); + _writeJavaTestFile(plugin2, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -197,7 +198,7 @@ public class MainActivityTest { test('runs integration tests', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/bar_test.dart', @@ -206,7 +207,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -275,7 +276,7 @@ public class MainActivityTest { const List examples = ['example1', 'example2']; const String javaTestFileExampleRelativePath = 'android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: examples, extraFiles: [ for (final String example in examples) ...[ @@ -286,7 +287,7 @@ public class MainActivityTest { ]); for (final String example in examples) { _writeJavaTestFile( - pluginDir, 'example/$example/$javaTestFileExampleRelativePath'); + plugin, 'example/$example/$javaTestFileExampleRelativePath'); } final List output = await runCapturingPrint(runner, [ @@ -339,14 +340,14 @@ public class MainActivityTest { test('fails if a test fails twice', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(), // auth @@ -385,14 +386,14 @@ public class MainActivityTest { () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(), // auth @@ -454,12 +455,12 @@ public class MainActivityTest { test('fails for packages with no integration test files', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -490,7 +491,7 @@ public class MainActivityTest { test('fails for packages with no integration_test runner', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/bar_test.dart', @@ -500,7 +501,7 @@ public class MainActivityTest { javaTestFileRelativePath, ]); // Use the wrong @RunWith annotation. - _writeJavaTestFile(pluginDir, javaTestFileRelativePath, + _writeJavaTestFile(plugin, javaTestFileRelativePath, runnerClass: 'AndroidJUnit4.class'); Error? commandError; @@ -559,12 +560,12 @@ public class MainActivityTest { test('builds if gradlew is missing', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -622,12 +623,12 @@ public class MainActivityTest { test('fails if building to generate gradlew fails', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['flutter'] = [ MockProcess(exitCode: 1) // flutter build @@ -657,16 +658,17 @@ public class MainActivityTest { test('fails if assembleAndroidTest fails', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -697,16 +699,17 @@ public class MainActivityTest { test('fails if assembleDebug fails', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -741,13 +744,13 @@ public class MainActivityTest { test('experimental flag', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + _writeJavaTestFile(plugin, javaTestFileRelativePath); await runCapturingPrint(runner, [ 'firebase-test-lab', diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index 6c10a7dc320..3fa7782245a 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -50,10 +50,10 @@ void main() { /// Returns a modified version of a list of [relativePaths] that are relative /// to [package] to instead be relative to [packagesDir]. List _getPackagesDirRelativePaths( - Directory packageDir, List relativePaths) { + RepositoryPackage package, List relativePaths) { final p.Context path = analyzeCommand.path; final String relativeBase = - path.relative(packageDir.path, from: packagesDir.path); + path.relative(package.path, from: packagesDir.path); return relativePaths .map((String relativePath) => path.join(relativeBase, relativePath)) .toList(); @@ -86,7 +86,7 @@ void main() { 'lib/src/b.dart', 'lib/src/c.dart', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -101,7 +101,7 @@ void main() { getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -114,7 +114,7 @@ void main() { 'lib/src/c.dart', ]; const String unformattedFile = 'lib/src/d.dart'; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: [ @@ -124,7 +124,8 @@ void main() { ); final p.Context posixContext = p.posix; - childFileWithSubcomponents(pluginDir, posixContext.split(unformattedFile)) + childFileWithSubcomponents( + plugin.directory, posixContext.split(unformattedFile)) .writeAsStringSync( '// copyright bla bla\n// This file is hand-formatted.\ncode...'); @@ -137,7 +138,7 @@ void main() { getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(pluginDir, formattedFiles) + ..._getPackagesDirRelativePaths(plugin, formattedFiles) ], packagesDir.path), ])); @@ -172,7 +173,7 @@ void main() { 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -190,7 +191,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -252,7 +253,7 @@ void main() { 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -270,7 +271,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -285,7 +286,7 @@ void main() { 'macos/Classes/Foo.mm', 'windows/foo_plugin.cpp', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -302,7 +303,7 @@ void main() { [ '-i', '--style=file', - ..._getPackagesDirRelativePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -339,7 +340,7 @@ void main() { const List files = [ 'windows/foo_plugin.cpp', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -358,7 +359,7 @@ void main() { [ '-i', '--style=file', - ..._getPackagesDirRelativePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -403,7 +404,7 @@ void main() { const List javaFiles = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java' ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: [ @@ -426,14 +427,14 @@ void main() { [ '-i', '--style=file', - ..._getPackagesDirRelativePaths(pluginDir, clangFiles) + ..._getPackagesDirRelativePaths(plugin, clangFiles) ], packagesDir.path), ProcessCall( getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(pluginDir, dartFiles) + ..._getPackagesDirRelativePaths(plugin, dartFiles) ], packagesDir.path), ProcessCall( @@ -442,7 +443,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(pluginDir, javaFiles) + ..._getPackagesDirRelativePaths(plugin, javaFiles) ], packagesDir.path), ])); diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index 91608d3785c..b072946ff95 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -40,7 +40,7 @@ void main() { }); test('runs gradle lint', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, extraFiles: [ 'example/android/gradlew', ], platformSupport: { @@ -48,7 +48,7 @@ void main() { }); final Directory androidDir = - pluginDir.childDirectory('example').childDirectory('android'); + plugin.getExamples().first.platformDirectory(FlutterPlatform.android); final List output = await runCapturingPrint(runner, ['lint-android']); @@ -74,7 +74,7 @@ void main() { test('runs on all examples', () async { final List examples = ['example1', 'example2']; - final Directory pluginDir = createFakePlugin('plugin1', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, examples: examples, extraFiles: [ 'example/example1/android/gradlew', @@ -84,11 +84,9 @@ void main() { platformAndroid: const PlatformDetails(PlatformSupport.inline) }); - final Iterable exampleAndroidDirs = examples.map( - (String example) => pluginDir - .childDirectory('example') - .childDirectory(example) - .childDirectory('android')); + final Iterable exampleAndroidDirs = plugin.getExamples().map( + (RepositoryPackage example) => + example.platformDirectory(FlutterPlatform.android)); final List output = await runCapturingPrint(runner, ['lint-android']); @@ -136,16 +134,17 @@ void main() { }); test('fails if linting finds issues', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, extraFiles: [ 'example/android/gradlew', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline) }); - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index bccbec67866..516a32fa692 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -65,7 +65,7 @@ void main() { }); test('runs pod lib lint on a podspec', () async { - final Directory plugin1Dir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin1', packagesDir, extraFiles: [ @@ -91,8 +91,8 @@ void main() { [ 'lib', 'lint', - plugin1Dir - .childDirectory('ios') + plugin + .platformDirectory(FlutterPlatform.ios) .childFile('plugin1.podspec') .path, '--configuration=Debug', @@ -106,8 +106,8 @@ void main() { [ 'lib', 'lint', - plugin1Dir - .childDirectory('ios') + plugin + .platformDirectory(FlutterPlatform.ios) .childFile('plugin1.podspec') .path, '--configuration=Debug', diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index 9e70f72e748..c2042c26638 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -119,17 +119,11 @@ void main() { // Create a federated plugin by creating a directory under the packages // directory with several packages underneath. - final Directory federatedPlugin = packagesDir.childDirectory('my_plugin') - ..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); + final Directory federatedPluginDir = + packagesDir.childDirectory('my_plugin')..createSync(); + createFakePlugin('my_plugin', federatedPluginDir); + createFakePlugin('my_plugin_web', federatedPluginDir); + createFakePlugin('my_plugin_macos', federatedPluginDir); // Test without specifying `--type`. final List plugins = @@ -151,17 +145,11 @@ void main() { // Create a federated plugin by creating a directory under the packages // directory with several packages underneath. - final Directory federatedPlugin = packagesDir.childDirectory('my_plugin') - ..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); + final Directory federatedPluginDir = + packagesDir.childDirectory('my_plugin')..createSync(); + createFakePlugin('my_plugin', federatedPluginDir); + createFakePlugin('my_plugin_web', federatedPluginDir); + createFakePlugin('my_plugin_macos', federatedPluginDir); List plugins = await runCapturingPrint( runner, ['list', '--packages=plugin1']); diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index da241c3d83f..2644e814f57 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -7,7 +7,6 @@ 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/repository_package.dart'; import 'package:flutter_plugin_tools/src/make_deps_path_based_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -62,9 +61,9 @@ void main() { } test('no-ops for no plugins', () async { - RepositoryPackage(createFakePackage('foo', packagesDir, isFlutter: true)); - final RepositoryPackage packageBar = RepositoryPackage( - createFakePackage('bar', packagesDir, isFlutter: true)); + createFakePackage('foo', packagesDir, isFlutter: true); + final RepositoryPackage packageBar = + createFakePackage('bar', packagesDir, isFlutter: true); _addDependencies(packageBar, ['foo']); final String originalPubspecContents = packageBar.pubspecFile.readAsStringSync(); @@ -83,16 +82,15 @@ void main() { }); test('rewrites references', () async { - final RepositoryPackage simplePackage = RepositoryPackage( - createFakePackage('foo', packagesDir, isFlutter: true)); + final RepositoryPackage simplePackage = + createFakePackage('foo', packagesDir, isFlutter: true); final Directory pluginGroup = packagesDir.childDirectory('bar'); - RepositoryPackage(createFakePackage('bar_platform_interface', pluginGroup, - isFlutter: true)); + createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); final RepositoryPackage pluginImplementation = - RepositoryPackage(createFakePlugin('bar_android', pluginGroup)); + createFakePlugin('bar_android', pluginGroup); final RepositoryPackage pluginAppFacing = - RepositoryPackage(createFakePlugin('bar', pluginGroup)); + createFakePlugin('bar', pluginGroup); _addDependencies(simplePackage, [ 'bar', @@ -147,16 +145,15 @@ void main() { // This test case ensures that running CI using this command on an interim // PR that itself used this command won't fail on the rewrite step. test('running a second time no-ops without failing', () async { - final RepositoryPackage simplePackage = RepositoryPackage( - createFakePackage('foo', packagesDir, isFlutter: true)); + final RepositoryPackage simplePackage = + createFakePackage('foo', packagesDir, isFlutter: true); final Directory pluginGroup = packagesDir.childDirectory('bar'); - RepositoryPackage(createFakePackage('bar_platform_interface', pluginGroup, - isFlutter: true)); + createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); final RepositoryPackage pluginImplementation = - RepositoryPackage(createFakePlugin('bar_android', pluginGroup)); + createFakePlugin('bar_android', pluginGroup); final RepositoryPackage pluginAppFacing = - RepositoryPackage(createFakePlugin('bar', pluginGroup)); + createFakePlugin('bar', pluginGroup); _addDependencies(simplePackage, [ 'bar', @@ -192,18 +189,17 @@ void main() { group('target-dependencies-with-non-breaking-updates', () { test('no-ops for no published changes', () async { - final Directory package = createFakePackage('foo', packagesDir); + final RepositoryPackage package = createFakePackage('foo', packagesDir); final String changedFileOutput = [ - package.childFile('pubspec.yaml'), + package.pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), ]; // Simulate no change to the version in the interface's pubspec.yaml. processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess( - stdout: RepositoryPackage(package).pubspecFile.readAsStringSync()), + MockProcess(stdout: package.pubspecFile.readAsStringSync()), ]; final List output = await runCapturingPrint(runner, [ @@ -244,10 +240,10 @@ void main() { test('includes bugfix version changes as targets', () async { const String newVersion = '1.0.1'; - final Directory package = + final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); - final File pubspecFile = RepositoryPackage(package).pubspecFile; + final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); @@ -276,10 +272,10 @@ void main() { test('includes minor version changes to 1.0+ as targets', () async { const String newVersion = '1.1.0'; - final Directory package = + final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); - final File pubspecFile = RepositoryPackage(package).pubspecFile; + final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); @@ -308,10 +304,10 @@ void main() { test('does not include major version changes as targets', () async { const String newVersion = '2.0.0'; - final Directory package = + final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); - final File pubspecFile = RepositoryPackage(package).pubspecFile; + final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); @@ -340,10 +336,10 @@ void main() { test('does not include minor version changes to 0.x as targets', () async { const String newVersion = '0.8.0'; - final Directory package = + final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); - final File pubspecFile = RepositoryPackage(package).pubspecFile; + final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); @@ -373,12 +369,12 @@ void main() { test('skips anything outside of the packages directory', () async { final Directory toolDir = packagesDir.parent.childDirectory('tool'); const String newVersion = '1.1.0'; - final Directory package = createFakePackage( + final RepositoryPackage package = createFakePackage( 'flutter_plugin_tools', toolDir, version: newVersion); // Simulate a minor version change so it would be a target. - final File pubspecFile = RepositoryPackage(package).pubspecFile; + final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 1069a68107c..d420184b612 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -57,8 +57,8 @@ final Map _kDeviceListMap = { const String _fakeCmakeCommand = 'path/to/cmake'; -void _createFakeCMakeCache(Directory pluginDir, Platform platform) { - final CMakeProject project = CMakeProject(pluginDir.childDirectory('example'), +void _createFakeCMakeCache(RepositoryPackage plugin, Platform platform) { + final CMakeProject project = CMakeProject(getExampleDir(plugin), platform: platform, buildMode: 'Release'); final File cache = project.buildDirectory.childFile('CMakeCache.txt'); cache.createSync(recursive: true); @@ -150,13 +150,12 @@ void main() { // Returns the ProcessCall to expect for build the Linux unit tests for the // given plugin. - ProcessCall _getLinuxBuildCall(Directory pluginDir) { + ProcessCall _getLinuxBuildCall(RepositoryPackage plugin) { return ProcessCall( 'cmake', [ '--build', - pluginDir - .childDirectory('example') + getExampleDir(plugin) .childDirectory('build') .childDirectory('linux') .childDirectory('x64') @@ -205,13 +204,12 @@ void main() { }); test('reports skips with no tests', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), @@ -273,13 +271,12 @@ void main() { }); test('running with correct destination', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -311,12 +308,11 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl @@ -382,14 +378,12 @@ void main() { }); test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -417,7 +411,7 @@ void main() { group('Android', () { test('runs Java unit tests in Android implementation folder', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -431,8 +425,10 @@ void main() { await runCapturingPrint(runner, ['native-test', '--android']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -447,7 +443,7 @@ void main() { }); test('runs Java unit tests in example folder', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -461,8 +457,10 @@ void main() { await runCapturingPrint(runner, ['native-test', '--android']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -477,7 +475,7 @@ void main() { }); test('runs Java integration tests', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -492,8 +490,10 @@ void main() { await runCapturingPrint( runner, ['native-test', '--android', '--no-unit']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -539,7 +539,7 @@ void main() { }); test('runs all tests when present', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -554,8 +554,10 @@ void main() { await runCapturingPrint(runner, ['native-test', '--android']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -578,7 +580,7 @@ void main() { }); test('honors --no-unit', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -594,8 +596,10 @@ void main() { await runCapturingPrint( runner, ['native-test', '--android', '--no-unit']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -613,7 +617,7 @@ void main() { }); test('honors --no-integration', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -629,8 +633,10 @@ void main() { await runCapturingPrint( runner, ['native-test', '--android', '--no-integration']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -720,7 +726,7 @@ void main() { }); test('fails when a unit test fails', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -732,9 +738,10 @@ void main() { ], ); - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -761,7 +768,7 @@ void main() { }); test('fails when an integration test fails', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -774,9 +781,10 @@ void main() { ], ); - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -882,15 +890,15 @@ void main() { test('builds and runs unit tests', () async { const String testBinaryRelativePath = 'build/linux/x64/release/bar/plugin_test'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -910,7 +918,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(pluginDirectory), + _getLinuxBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -920,17 +928,17 @@ void main() { 'build/linux/x64/debug/bar/plugin_test'; const String releaseTestBinaryRelativePath = 'build/linux/x64/release/bar/plugin_test'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', 'example/$releaseTestBinaryRelativePath' ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); final File releaseTestBinary = childFileWithSubcomponents( - pluginDirectory, + plugin.directory, ['example', ...releaseTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -950,7 +958,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(pluginDirectory), + _getLinuxBuildCall(plugin), ProcessCall(releaseTestBinary.path, const [], null), ])); }); @@ -983,12 +991,11 @@ void main() { }); test('fails if there are no unit tests', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -1010,22 +1017,22 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(pluginDirectory), + _getLinuxBuildCall(plugin), ])); }); test('fails if a unit test fails', () async { const String testBinaryRelativePath = 'build/linux/x64/release/bar/plugin_test'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); processRunner.mockProcessesForExecutable[testBinary.path] = @@ -1051,7 +1058,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(pluginDirectory), + _getLinuxBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -1087,14 +1094,12 @@ void main() { }); test('honors unit-only', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -1123,14 +1128,13 @@ void main() { }); test('honors integration-only', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -1159,14 +1163,13 @@ void main() { }); test('skips when the requested target is not present', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); // Simulate a project with unit tests but no integration tests... processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -1195,14 +1198,13 @@ void main() { }); test('fails if there are no unit tests', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess(['RunnerUITests']), @@ -1235,14 +1237,13 @@ void main() { }); test('fails if unable to check for requested target', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess(exitCode: 1), // xcodebuild -list @@ -1275,7 +1276,7 @@ void main() { group('multiplatform', () { test('runs all platfroms when supported', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -1289,8 +1290,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final Directory androidFolder = pluginExampleDirectory.childDirectory('android'); @@ -1334,14 +1334,12 @@ void main() { }); test('runs only macOS for a macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -1372,13 +1370,12 @@ void main() { }); test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -1466,7 +1463,7 @@ void main() { }); test('failing one platform does not stop the tests', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -1485,9 +1482,10 @@ void main() { ]; // Simulate failing Android, but not iOS. - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -1522,7 +1520,7 @@ void main() { }); test('failing multiple platforms reports multiple failures', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -1536,9 +1534,10 @@ void main() { ); // Simulate failing Android. - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -1599,13 +1598,12 @@ void main() { // Returns the ProcessCall to expect for build the Windows unit tests for // the given plugin. - ProcessCall _getWindowsBuildCall(Directory pluginDir) { + ProcessCall _getWindowsBuildCall(RepositoryPackage plugin) { return ProcessCall( _fakeCmakeCommand, [ '--build', - pluginDir - .childDirectory('example') + getExampleDir(plugin) .childDirectory('build') .childDirectory('windows') .path, @@ -1621,15 +1619,15 @@ void main() { test('runs unit tests', () async { const String testBinaryRelativePath = 'build/windows/Debug/bar/plugin_test.exe'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -1649,7 +1647,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(pluginDirectory), + _getWindowsBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -1659,16 +1657,17 @@ void main() { 'build/windows/Debug/bar/plugin_test.exe'; const String releaseTestBinaryRelativePath = 'build/windows/Release/bar/plugin_test.exe'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', 'example/$releaseTestBinaryRelativePath' ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File debugTestBinary = childFileWithSubcomponents(pluginDirectory, + final File debugTestBinary = childFileWithSubcomponents( + plugin.directory, ['example', ...debugTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -1688,7 +1687,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(pluginDirectory), + _getWindowsBuildCall(plugin), ProcessCall(debugTestBinary.path, const [], null), ])); }); @@ -1721,12 +1720,11 @@ void main() { }); test('fails if there are no unit tests', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -1748,22 +1746,22 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(pluginDirectory), + _getWindowsBuildCall(plugin), ])); }); test('fails if a unit test fails', () async { const String testBinaryRelativePath = 'build/windows/Debug/bar/plugin_test.exe'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); processRunner.mockProcessesForExecutable[testBinary.path] = @@ -1789,7 +1787,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(pluginDirectory), + _getWindowsBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index c5527af2173..e6c5b9cdebc 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -44,9 +44,9 @@ void main() { }); test('publish check all packages', () async { - final Directory plugin1Dir = + final RepositoryPackage plugin1 = createFakePlugin('plugin_tools_test_package_a', packagesDir); - final Directory plugin2Dir = + final RepositoryPackage plugin2 = createFakePlugin('plugin_tools_test_package_b', packagesDir); await runCapturingPrint(runner, ['publish-check']); @@ -57,11 +57,11 @@ void main() { ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], - plugin1Dir.path), + plugin1.path), ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], - plugin2Dir.path), + plugin2.path), ])); }); @@ -89,8 +89,8 @@ void main() { }); test('fail on bad pubspec', () async { - final Directory dir = createFakePlugin('c', packagesDir); - await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + final RepositoryPackage package = createFakePlugin('c', packagesDir); + await package.pubspecFile.writeAsString('bad-yaml'); Error? commandError; final List output = await runCapturingPrint( @@ -108,8 +108,9 @@ void main() { }); test('fails if AUTHORS is missing', () async { - final Directory package = createFakePackage('a_package', packagesDir); - package.childFile('AUTHORS').delete(); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + package.authorsFile.delete(); Error? commandError; final List output = await runCapturingPrint( @@ -128,12 +129,12 @@ void main() { }); test('does not require AUTHORS for third-party', () async { - final Directory package = createFakePackage( + final RepositoryPackage package = createFakePackage( 'a_package', packagesDir.parent .childDirectory('third_party') .childDirectory('packages')); - package.childFile('AUTHORS').delete(); + package.authorsFile.delete(); final List output = await runCapturingPrint(runner, ['publish-check']); @@ -372,11 +373,11 @@ void main() { ); runner.addCommand(command); - final Directory plugin1Dir = + final RepositoryPackage plugin = createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + await plugin.pubspecFile.writeAsString('bad-yaml'); bool hasError = false; final List output = await runCapturingPrint( diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 2cb3fc25af2..857828ab930 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -83,11 +83,11 @@ void main() { group('Initial validation', () { test('refuses to proceed with dirty files', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); processRunner.mockProcessesForExecutable['git-status'] = [ - MockProcess(stdout: '?? ${pluginDir.childFile('tmp').path}\n') + MockProcess(stdout: '?? ${plugin.directory.childFile('tmp').path}\n') ]; Error? commandError; @@ -183,7 +183,7 @@ void main() { }); test('forwards --pub-publish-flags to pub publish', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ @@ -198,14 +198,14 @@ void main() { contains(ProcessCall( flutterCommand, const ['pub', 'publish', '--dry-run', '--server=bar'], - pluginDir.path))); + plugin.path))); }); test( '--skip-confirmation flag automatically adds --force to --pub-publish-flags', () async { _createMockCredentialFile(); - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ @@ -221,7 +221,7 @@ void main() { contains(ProcessCall( flutterCommand, const ['pub', 'publish', '--server=bar', '--force'], - pluginDir.path))); + plugin.path))); }); test('throws if pub publish fails', () async { @@ -249,7 +249,7 @@ void main() { }); test('publish, dry run', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); final List output = @@ -268,7 +268,7 @@ void main() { containsAllInOrder([ contains('=============== DRY RUN ==============='), contains('Running for foo'), - contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Running `pub publish ` in ${plugin.path}...'), contains('Tagging release foo-v0.0.1...'), contains('Pushing tag to upstream...'), contains('Published foo successfully!'), @@ -386,7 +386,7 @@ void main() { }); test('to upstream by default, dry run', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); mockStdin.readLineOutput = 'y'; @@ -402,7 +402,7 @@ void main() { output, containsAllInOrder([ contains('=============== DRY RUN ==============='), - contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Running `pub publish ` in ${plugin.path}...'), contains('Tagging release foo-v0.0.1...'), contains('Pushing tag to upstream...'), contains('Published foo successfully!'), @@ -447,16 +447,17 @@ void main() { }; // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = createFakePlugin( + final RepositoryPackage plugin2 = createFakePlugin( 'plugin2', packagesDir.childDirectory('plugin2'), ); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; @@ -468,8 +469,8 @@ void main() { containsAllInOrder([ contains( 'Publishing all packages that have changed relative to "HEAD~"'), - contains('Running `pub publish ` in ${pluginDir1.path}...'), - contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Running `pub publish ` in ${plugin1.path}...'), + contains('Running `pub publish ` in ${plugin2.path}...'), contains('plugin1 - \x1B[32mpublished\x1B[0m'), contains('plugin2/plugin2 - \x1B[32mpublished\x1B[0m'), ])); @@ -503,9 +504,10 @@ void main() { // The existing plugin. createFakePlugin('plugin0', packagesDir); // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); // Git results for plugin0 having been released already, and plugin1 and @@ -515,8 +517,8 @@ void main() { ]; processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; @@ -527,8 +529,8 @@ void main() { expect( output, containsAllInOrder([ - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Running `pub publish ` in ${plugin1.path}...\n', + 'Running `pub publish ` in ${plugin2.path}...\n', ])); expect( processRunner.recordedCalls, @@ -552,15 +554,16 @@ void main() { }; // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; @@ -576,11 +579,11 @@ void main() { output, containsAllInOrder([ contains('=============== DRY RUN ==============='), - contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Running `pub publish ` in ${plugin1.path}...'), contains('Tagging release plugin1-v0.0.1...'), contains('Pushing tag to upstream...'), contains('Published plugin1 successfully!'), - contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Running `pub publish ` in ${plugin2.path}...'), contains('Tagging release plugin2-v0.0.1...'), contains('Pushing tag to upstream...'), contains('Published plugin2 successfully!'), @@ -603,17 +606,17 @@ void main() { }; // Non-federated - final Directory pluginDir1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = createFakePlugin( + final RepositoryPackage plugin2 = createFakePlugin( 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; @@ -623,9 +626,9 @@ void main() { expect( output2, containsAllInOrder([ - contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Running `pub publish ` in ${plugin1.path}...'), contains('Published plugin1 successfully!'), - contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Running `pub publish ` in ${plugin2.path}...'), contains('Published plugin2 successfully!'), ])); expect( @@ -652,17 +655,17 @@ void main() { }; // Non-federated - final Directory pluginDir1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - pluginDir2.deleteSync(recursive: true); + plugin2.directory.deleteSync(recursive: true); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; @@ -672,7 +675,7 @@ void main() { expect( output2, containsAllInOrder([ - contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Running `pub publish ` in ${plugin1.path}...'), contains('Published plugin1 successfully!'), contains( 'The pubspec file for plugin2/plugin2 does not exist, so no publishing will happen.\nSafe to ignore if the package is deleted in this commit.\n'), @@ -698,17 +701,17 @@ void main() { }; // Non-federated - final Directory pluginDir1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = createFakePlugin( + final RepositoryPackage plugin2 = createFakePlugin( 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; processRunner.mockProcessesForExecutable['git-tag'] = [ MockProcess( @@ -748,17 +751,17 @@ void main() { }; // Non-federated - final Directory pluginDir1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = createFakePlugin( + final RepositoryPackage plugin2 = createFakePlugin( 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; Error? commandError; @@ -785,15 +788,16 @@ void main() { test('No version change does not release any plugins', () async { // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('plugin1.dart').path}\n' - '${pluginDir2.childFile('plugin2.dart').path}\n') + stdout: '${plugin1.libDirectory.childFile('plugin1.dart').path}\n' + '${plugin2.libDirectory.childFile('plugin2.dart').path}\n') ]; final List output = await runCapturingPrint(commandRunner, @@ -812,10 +816,10 @@ void main() { 'versions': [], }; - final Directory flutterPluginTools = + final RepositoryPackage flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: flutterPluginTools.childFile('pubspec.yaml').path) + MockProcess(stdout: flutterPluginTools.pubspecFile.path) ]; final List output = await runCapturingPrint(commandRunner, diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 30b6ab6004e..89bb98abd80 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -146,9 +146,9 @@ void main() { }); test('passes for a plugin following conventions', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -157,10 +157,7 @@ ${_devDependenciesSection()} ${_falseSecretsSection()} '''); - pluginDirectory - .childDirectory('example') - .childFile('pubspec.yaml') - .writeAsStringSync(''' + plugin.getExamples().first.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_example', publishable: false, @@ -187,10 +184,10 @@ ${_flutterSection()} }); test('passes for a Flutter package following conventions', () async { - final Directory packageDirectory = + final RepositoryPackage package = createFakePackage('a_package', packagesDir); - packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + package.pubspecFile.writeAsStringSync(''' ${_headerSection('a_package')} ${_environmentSection()} ${_dependenciesSection()} @@ -199,10 +196,7 @@ ${_flutterSection()} ${_falseSecretsSection()} '''); - packageDirectory - .childDirectory('example') - .childFile('pubspec.yaml') - .writeAsStringSync(''' + package.getExamples().first.pubspecFile.writeAsStringSync(''' ${_headerSection( 'a_package', publishable: false, @@ -229,10 +223,10 @@ ${_flutterSection()} }); test('passes for a minimal package following conventions', () async { - final Directory packageDirectory = packagesDir.childDirectory('package'); - packageDirectory.createSync(recursive: true); + final RepositoryPackage package = + createFakePackage('package', packagesDir, examples: []); - packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + package.pubspecFile.writeAsStringSync(''' ${_headerSection('package')} ${_environmentSection()} ${_dependenciesSection()} @@ -252,10 +246,10 @@ ${_dependenciesSection()} }); test('fails when homepage is included', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeHomepage: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -280,10 +274,10 @@ ${_devDependenciesSection()} }); test('fails when repository is missing', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeRepository: false)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -307,10 +301,10 @@ ${_devDependenciesSection()} }); test('fails when homepage is given instead of repository', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -335,10 +329,10 @@ ${_devDependenciesSection()} }); test('fails when repository is incorrect', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -362,10 +356,10 @@ ${_devDependenciesSection()} }); test('fails when issue tracker is missing', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeIssueTracker: false)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -389,11 +383,11 @@ ${_devDependenciesSection()} }); test('fails when description is too short', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir.childDirectory('a_plugin'), examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: 'Too short')} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -420,10 +414,10 @@ ${_devDependenciesSection()} test( 'allows short descriptions for non-app-facing parts of federated plugins', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: 'Too short')} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -448,7 +442,7 @@ ${_devDependenciesSection()} }); test('fails when description is too long', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); const String description = 'This description is too long. It just goes ' @@ -456,7 +450,7 @@ ${_devDependenciesSection()} 'there is just too much here. Someone shoul really cut this down to just ' 'the core description so that search results are more useful and the ' 'package does not lose pub points.'; - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: description)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -481,10 +475,10 @@ ${_devDependenciesSection()} }); test('fails when environment section is out of order', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_flutterSection(isPlugin: true)} ${_dependenciesSection()} @@ -509,10 +503,10 @@ ${_environmentSection()} }); test('fails when flutter section is out of order', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_flutterSection(isPlugin: true)} ${_environmentSection()} @@ -537,10 +531,10 @@ ${_devDependenciesSection()} }); test('fails when dependencies section is out of order', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -565,9 +559,9 @@ ${_dependenciesSection()} }); test('fails when dev_dependencies section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_environmentSection()} ${_devDependenciesSection()} @@ -592,10 +586,10 @@ ${_dependenciesSection()} }); test('fails when false_secrets section is out of order', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -622,11 +616,11 @@ ${_devDependenciesSection()} test('fails when an implemenation package is missing "implements"', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -651,11 +645,11 @@ ${_devDependenciesSection()} test('fails when an implemenation package has the wrong "implements"', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} @@ -680,11 +674,11 @@ ${_devDependenciesSection()} }); test('passes for a correct implemenation package', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a_foo', isPlugin: true, @@ -709,11 +703,11 @@ ${_devDependenciesSection()} }); test('fails when a "default_package" looks incorrect', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin_a', packagesDir.childDirectory('plugin_a'), examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a', isPlugin: true, @@ -749,11 +743,11 @@ ${_devDependenciesSection()} test( 'fails when a "default_package" does not have a corresponding dependency', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin_a', packagesDir.childDirectory('plugin_a'), examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a', isPlugin: true, @@ -787,11 +781,11 @@ ${_devDependenciesSection()} }); test('passes for an app-facing package without "implements"', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin_a', packagesDir.childDirectory('plugin_a'), examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a', isPlugin: true, @@ -817,11 +811,11 @@ ${_devDependenciesSection()} test('passes for a platform interface package without "implements"', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin_a_platform_interface', packagesDir.childDirectory('plugin_a'), examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a_platform_interface', isPlugin: true, @@ -847,13 +841,13 @@ ${_devDependenciesSection()} }); test('validates some properties even for unpublished packages', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), examples: []); // Environment section is in the wrong location. // Missing 'implements'. - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true, publishable: false)} ${_flutterSection(isPlugin: true)} ${_dependenciesSection()} @@ -879,12 +873,12 @@ ${_environmentSection()} }); test('ignores some checks for unpublished packages', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); // Missing metadata that is only useful for published packages, such as // repository and issue tracker. - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin', isPlugin: true, @@ -936,10 +930,10 @@ ${_devDependenciesSection()} }); test('repository check works', () async { - final Directory packageDirectory = + final RepositoryPackage package = createFakePackage('package', packagesDir, examples: []); - packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + package.pubspecFile.writeAsStringSync(''' ${_headerSection('package')} ${_environmentSection()} ${_dependenciesSection()} diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart index b6e016dccab..f53fa06a813 100644 --- a/script/tool/test/readme_check_command_test.dart +++ b/script/tool/test/readme_check_command_test.dart @@ -62,7 +62,7 @@ void main() { const String federatedPluginName = 'a_federated_plugin'; final Directory federatedDir = packagesDir.childDirectory(federatedPluginName); - final List packageDirectories = [ + final List packages = [ // A non-plugin package. createFakePackage('a_package', packagesDir), // Non-app-facing parts of a federated plugin. @@ -71,8 +71,8 @@ void main() { createFakePlugin('${federatedPluginName}_android', federatedDir), ]; - for (final Directory package in packageDirectories) { - package.childFile('README.md').writeAsStringSync(''' + for (final RepositoryPackage package in packages) { + package.readmeFile.writeAsStringSync(''' A very useful package. '''); } @@ -94,9 +94,10 @@ A very useful package. test('fails when non-federated plugin is missing an OS support table', () async { - final Directory pluginDir = createFakePlugin('a_plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); - pluginDir.childFile('README.md').writeAsStringSync(''' + plugin.readmeFile.writeAsStringSync(''' A very useful plugin. '''); @@ -118,10 +119,10 @@ A very useful plugin. test( 'fails when app-facing part of a federated plugin is missing an OS support table', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); - pluginDir.childFile('README.md').writeAsStringSync(''' + plugin.readmeFile.writeAsStringSync(''' A very useful plugin. '''); @@ -141,9 +142,10 @@ A very useful plugin. }); test('fails the OS support table is missing the header', () async { - final Directory pluginDir = createFakePlugin('a_plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); - pluginDir.childFile('README.md').writeAsStringSync(''' + plugin.readmeFile.writeAsStringSync(''' A very useful plugin. | **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | @@ -165,7 +167,7 @@ A very useful plugin. }); test('fails if the OS support table is missing a supported OS', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, platformSupport: { @@ -175,7 +177,7 @@ A very useful plugin. }, ); - pluginDir.childFile('README.md').writeAsStringSync(''' + plugin.readmeFile.writeAsStringSync(''' A very useful plugin. | | Android | iOS | @@ -202,7 +204,7 @@ A very useful plugin. }); test('fails if the OS support table lists an extra OS', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, platformSupport: { @@ -211,7 +213,7 @@ A very useful plugin. }, ); - pluginDir.childFile('README.md').writeAsStringSync(''' + plugin.readmeFile.writeAsStringSync(''' A very useful plugin. | | Android | iOS | Web | @@ -239,7 +241,7 @@ A very useful plugin. test('fails if the OS support table has unexpected OS formatting', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, platformSupport: { @@ -250,7 +252,7 @@ A very useful plugin. }, ); - pluginDir.childFile('README.md').writeAsStringSync(''' + plugin.readmeFile.writeAsStringSync(''' A very useful plugin. | | android | ios | MacOS | web | @@ -278,9 +280,10 @@ A very useful plugin. group('code blocks', () { test('fails on missing info string', () async { - final Directory packageDir = createFakePackage('a_package', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); - packageDir.childFile('README.md').writeAsStringSync(''' + package.readmeFile.writeAsStringSync(''' Example: ``` @@ -307,9 +310,10 @@ void main() { }); test('allows unknown info strings', () async { - final Directory packageDir = createFakePackage('a_package', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); - packageDir.childFile('README.md').writeAsStringSync(''' + package.readmeFile.writeAsStringSync(''' Example: ```someunknowninfotag @@ -331,9 +335,10 @@ A B C }); test('allows space around info strings', () async { - final Directory packageDir = createFakePackage('a_package', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); - packageDir.childFile('README.md').writeAsStringSync(''' + package.readmeFile.writeAsStringSync(''' Example: ``` dart @@ -355,9 +360,10 @@ A B C }); test('passes when excerpt requirement is met', () async { - final Directory packageDir = createFakePackage('a_package', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); - packageDir.childFile('README.md').writeAsStringSync(''' + package.readmeFile.writeAsStringSync(''' Example: @@ -379,9 +385,10 @@ A B C }); test('fails on missing excerpt tag when requested', () async { - final Directory packageDir = createFakePackage('a_package', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); - packageDir.childFile('README.md').writeAsStringSync(''' + package.readmeFile.writeAsStringSync(''' Example: ```dart diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 386eaf0d345..14a1e4a67c1 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -40,9 +40,9 @@ void main() { }); test('runs flutter test on each plugin', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, extraFiles: ['test/empty_test.dart']); - final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); await runCapturingPrint(runner, ['test']); @@ -51,15 +51,15 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin1Dir.path), + const ['test', '--color'], plugin1.path), ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin2Dir.path), + const ['test', '--color'], plugin2.path), ]), ); }); test('runs flutter test on Flutter package example tests', () async { - final Directory pluginDir = createFakePlugin('a_plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: [ 'test/empty_test.dart', 'example/test/an_example_test.dart' @@ -71,11 +71,9 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], pluginDir.path), - ProcessCall( - getFlutterCommand(mockPlatform), - const ['test', '--color'], - pluginDir.childDirectory('example').path), + const ['test', '--color'], plugin.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], getExampleDir(plugin).path), ]), ); }); @@ -110,7 +108,7 @@ void main() { test('skips testing plugins without test directory', () async { createFakePlugin('plugin1', packagesDir); - final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); await runCapturingPrint(runner, ['test']); @@ -119,15 +117,15 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin2Dir.path), + const ['test', '--color'], plugin2.path), ]), ); }); test('runs dart run test on non-Flutter packages', () async { - final Directory pluginDir = createFakePlugin('a', packagesDir, + final RepositoryPackage plugin = createFakePlugin('a', packagesDir, extraFiles: ['test/empty_test.dart']); - final Directory packageDir = createFakePackage('b', packagesDir, + final RepositoryPackage package = createFakePackage('b', packagesDir, extraFiles: ['test/empty_test.dart']); await runCapturingPrint( @@ -139,34 +137,34 @@ void main() { ProcessCall( getFlutterCommand(mockPlatform), const ['test', '--color', '--enable-experiment=exp1'], - pluginDir.path), - ProcessCall('dart', const ['pub', 'get'], packageDir.path), + plugin.path), + ProcessCall('dart', const ['pub', 'get'], package.path), ProcessCall( 'dart', const ['run', '--enable-experiment=exp1', 'test'], - packageDir.path), + package.path), ]), ); }); test('runs dart run test on non-Flutter package examples', () async { - final Directory packageDir = createFakePackage('a_package', packagesDir, - extraFiles: [ - 'test/empty_test.dart', - 'example/test/an_example_test.dart' - ]); + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, extraFiles: [ + 'test/empty_test.dart', + 'example/test/an_example_test.dart' + ]); await runCapturingPrint(runner, ['test']); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('dart', const ['pub', 'get'], packageDir.path), - ProcessCall('dart', const ['run', 'test'], packageDir.path), + ProcessCall('dart', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['run', 'test'], package.path), ProcessCall('dart', const ['pub', 'get'], - packageDir.childDirectory('example').path), + getExampleDir(package).path), ProcessCall('dart', const ['run', 'test'], - packageDir.childDirectory('example').path), + getExampleDir(package).path), ]), ); }); @@ -220,7 +218,7 @@ void main() { }); test('runs on Chrome for web plugins', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: ['test/empty_test.dart'], @@ -237,15 +235,15 @@ void main() { ProcessCall( getFlutterCommand(mockPlatform), const ['test', '--color', '--platform=chrome'], - pluginDir.path), + plugin.path), ]), ); }); test('enable-experiment flag', () async { - final Directory pluginDir = createFakePlugin('a', packagesDir, + final RepositoryPackage plugin = createFakePlugin('a', packagesDir, extraFiles: ['test/empty_test.dart']); - final Directory packageDir = createFakePackage('b', packagesDir, + final RepositoryPackage package = createFakePackage('b', packagesDir, extraFiles: ['test/empty_test.dart']); await runCapturingPrint( @@ -257,12 +255,12 @@ void main() { ProcessCall( getFlutterCommand(mockPlatform), const ['test', '--color', '--enable-experiment=exp1'], - pluginDir.path), - ProcessCall('dart', const ['pub', 'get'], packageDir.path), + plugin.path), + ProcessCall('dart', const ['pub', 'get'], package.path), ProcessCall( 'dart', const ['run', '--enable-experiment=exp1', 'test'], - packageDir.path), + package.path), ]), ); }); diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart index 30189cf23a0..5c1d74444ee 100644 --- a/script/tool/test/update_excerpts_command_test.dart +++ b/script/tool/test/update_excerpts_command_test.dart @@ -8,7 +8,6 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:flutter_plugin_tools/src/update_excerpts_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -42,9 +41,9 @@ void main() { }); test('runs pub get before running scripts', () async { - final Directory package = createFakePlugin('a_package', packagesDir, + final RepositoryPackage package = createFakePlugin('a_package', packagesDir, extraFiles: ['example/build.excerpt.yaml']); - final Directory example = package.childDirectory('example'); + final Directory example = getExampleDir(package); await runCapturingPrint(runner, ['update-excerpts']); @@ -69,9 +68,9 @@ void main() { }); test('runs when config is present', () async { - final Directory package = createFakePlugin('a_package', packagesDir, + final RepositoryPackage package = createFakePlugin('a_package', packagesDir, extraFiles: ['example/build.excerpt.yaml']); - final Directory example = package.childDirectory('example'); + final Directory example = getExampleDir(package); final List output = await runCapturingPrint(runner, ['update-excerpts']); @@ -128,7 +127,7 @@ void main() { }); test('restores pubspec even if running the script fails', () async { - final Directory package = createFakePlugin('a_package', packagesDir, + final RepositoryPackage package = createFakePlugin('a_package', packagesDir, extraFiles: ['example/build.excerpt.yaml']); processRunner.mockProcessesForExecutable['dart'] = [ @@ -152,11 +151,8 @@ void main() { ' Unable to get script dependencies') ])); - final String examplePubspecContent = RepositoryPackage(package) - .getExamples() - .first - .pubspecFile - .readAsStringSync(); + final String examplePubspecContent = + package.getExamples().first.pubspecFile.readAsStringSync(); expect(examplePubspecContent, isNot(contains('code_excerpter'))); expect(examplePubspecContent, isNot(contains('code_excerpt_updater'))); }); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 8a2bf099cc8..5c38bd5f703 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -13,6 +13,7 @@ import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; @@ -20,6 +21,8 @@ import 'package:quiver/collection.dart'; import 'mocks.dart'; +export 'package:flutter_plugin_tools/src/common/repository_package.dart'; + /// Returns the exe name that command will use when running Flutter on /// [platform]. String getFlutterCommand(Platform platform) => @@ -65,6 +68,20 @@ class PlatformDetails { final bool hasDartCode; } +/// Returns the 'example' directory for [package]. +/// +/// This is deliberately not a method on [RepositoryPackage] since actual tool +/// code should essentially never need this, and instead be using +/// [RepositoryPackage.getExamples] to avoid assuming there's a single example +/// directory. However, needing to construct paths with the example directory +/// is very common in test code. +/// +/// This returns a Directory rather than a RepositoryPackage because there is no +/// guarantee that the returned directory is a package. +Directory getExampleDir(RepositoryPackage package) { + return package.directory.childDirectory('example'); +} + /// Creates a plugin package with the given [name] in [packagesDirectory]. /// /// [platformSupport] is a map of platform string to the support details for @@ -72,8 +89,7 @@ class PlatformDetails { /// /// [extraFiles] is an optional list of plugin-relative paths, using Posix /// separators, of extra files to create in the plugin. -// TODO(stuartmorgan): Convert the return to a RepositoryPackage. -Directory createFakePlugin( +RepositoryPackage createFakePlugin( String name, Directory parentDirectory, { List examples = const ['example'], @@ -83,7 +99,7 @@ Directory createFakePlugin( String? version = '0.0.1', String flutterConstraint = '>=2.5.0', }) { - final Directory pluginDirectory = createFakePackage(name, parentDirectory, + final RepositoryPackage package = createFakePackage(name, parentDirectory, isFlutter: true, examples: examples, extraFiles: extraFiles, @@ -91,7 +107,7 @@ Directory createFakePlugin( flutterConstraint: flutterConstraint); createFakePubspec( - pluginDirectory, + package, name: name, isFlutter: true, isPlugin: true, @@ -100,7 +116,7 @@ Directory createFakePlugin( flutterConstraint: flutterConstraint, ); - return pluginDirectory; + return package; } /// Creates a plugin package with the given [name] in [packagesDirectory]. @@ -113,8 +129,7 @@ Directory createFakePlugin( /// /// If non-null, [directoryName] will be used for the directory instead of /// [name]. -// TODO(stuartmorgan): Convert the return to a RepositoryPackage. -Directory createFakePackage( +RepositoryPackage createFakePackage( String name, Directory parentDirectory, { List examples = const ['example'], @@ -126,26 +141,26 @@ Directory createFakePackage( String? directoryName, String? publishTo, }) { - final Directory packageDirectory = - parentDirectory.childDirectory(directoryName ?? name); - packageDirectory.createSync(recursive: true); + final RepositoryPackage package = + RepositoryPackage(parentDirectory.childDirectory(directoryName ?? name)); + package.directory.createSync(recursive: true); - packageDirectory.childDirectory('lib').createSync(); - createFakePubspec(packageDirectory, + package.libDirectory.createSync(); + createFakePubspec(package, name: name, isFlutter: isFlutter, version: version, flutterConstraint: flutterConstraint); if (includeCommonFiles) { - createFakeCHANGELOG(packageDirectory, ''' + createFakeCHANGELOG(package, ''' ## $version * Some changes. '''); - createFakeAuthors(packageDirectory); + createFakeAuthors(package); } if (examples.length == 1) { - createFakePackage('${name}_example', packageDirectory, + createFakePackage('${name}_example', package.directory, directoryName: examples.first, examples: [], includeCommonFiles: false, @@ -153,8 +168,7 @@ Directory createFakePackage( publishTo: 'none', flutterConstraint: flutterConstraint); } else if (examples.isNotEmpty) { - final Directory examplesDirectory = - packageDirectory.childDirectory('example')..createSync(); + final Directory examplesDirectory = getExampleDir(package)..createSync(); for (final String exampleName in examples) { createFakePackage(exampleName, examplesDirectory, examples: [], @@ -167,16 +181,16 @@ Directory createFakePackage( final p.Context posixContext = p.posix; for (final String file in extraFiles) { - childFileWithSubcomponents(packageDirectory, posixContext.split(file)) + childFileWithSubcomponents(package.directory, posixContext.split(file)) .createSync(recursive: true); } - return packageDirectory; + return package; } -void createFakeCHANGELOG(Directory parent, String texts) { - parent.childFile('CHANGELOG.md').createSync(); - parent.childFile('CHANGELOG.md').writeAsStringSync(texts); +void createFakeCHANGELOG(RepositoryPackage package, String texts) { + package.changelogFile.createSync(); + package.changelogFile.writeAsStringSync(texts); } /// Creates a `pubspec.yaml` file with a flutter dependency. @@ -185,7 +199,7 @@ void createFakeCHANGELOG(Directory parent, String texts) { /// that platform. If empty, no `plugin` entry will be created unless `isPlugin` /// is set to `true`. void createFakePubspec( - Directory parent, { + RepositoryPackage package, { String name = 'fake_package', bool isFlutter = true, bool isPlugin = false, @@ -249,14 +263,13 @@ $dependenciesSection $pluginSection '''; - parent.childFile('pubspec.yaml').createSync(); - parent.childFile('pubspec.yaml').writeAsStringSync(yaml); + package.pubspecFile.createSync(); + package.pubspecFile.writeAsStringSync(yaml); } -void createFakeAuthors(Directory parent) { - final File authorsFile = parent.childFile('AUTHORS'); - authorsFile.createSync(); - authorsFile.writeAsStringSync('Google Inc.'); +void createFakeAuthors(RepositoryPackage package) { + package.authorsFile.createSync(); + package.authorsFile.writeAsStringSync('Google Inc.'); } String _pluginPlatformSection( diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 5a5a0a108a3..aeacd77635d 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -414,14 +414,14 @@ This is necessary because of X, Y, and Z test('Allow empty lines in front of the first version in CHANGELOG', () async { const String version = '1.0.1'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' ## $version * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main']); expect( @@ -433,13 +433,13 @@ This is necessary because of X, Y, and Z }); test('Throws if versions in changelog and pubspec do not match', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.1'); const String changelog = ''' ## 1.0.2 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -458,14 +458,14 @@ This is necessary because of X, Y, and Z test('Success if CHANGELOG and pubspec versions match', () async { const String version = '1.0.1'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' ## $version * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main']); expect( @@ -479,7 +479,7 @@ This is necessary because of X, Y, and Z test( 'Fail if pubspec version only matches an older version listed in CHANGELOG', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' @@ -488,7 +488,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); bool hasError = false; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -509,7 +509,7 @@ This is necessary because of X, Y, and Z test('Allow NEXT as a placeholder for gathering CHANGELOG entries', () async { const String version = '1.0.0'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' @@ -518,7 +518,7 @@ This is necessary because of X, Y, and Z ## $version * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -536,7 +536,7 @@ This is necessary because of X, Y, and Z test('Fail if NEXT appears after a version', () async { const String version = '1.0.1'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' @@ -547,7 +547,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); bool hasError = false; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -569,7 +569,7 @@ This is necessary because of X, Y, and Z test('Fail if NEXT is left in the CHANGELOG when adding a version bump', () async { const String version = '1.0.1'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' @@ -580,7 +580,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); bool hasError = false; final List output = await runCapturingPrint( @@ -603,7 +603,7 @@ This is necessary because of X, Y, and Z }); test('Fail if the version changes without replacing NEXT', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.1'); const String changelog = ''' @@ -612,7 +612,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); bool hasError = false; final List output = await runCapturingPrint( @@ -635,7 +635,7 @@ This is necessary because of X, Y, and Z test( 'fails gracefully if the version headers are not found due to using the wrong style', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' @@ -644,7 +644,7 @@ This is necessary because of X, Y, and Z # 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -670,14 +670,14 @@ This is necessary because of X, Y, and Z }); test('fails gracefully if the version is unparseable', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## Alpha * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -715,14 +715,14 @@ This is necessary because of X, Y, and Z } test('passes for unchanged packages', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -744,14 +744,14 @@ This is necessary because of X, Y, and Z test( 'fails if a version change is missing from a change that does not ' 'pass the exemption check', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -779,14 +779,14 @@ packages/plugin/lib/plugin.dart }); test('passes version change requirement when version changes', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.1'); const String changelog = ''' ## 1.0.1 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -810,14 +810,14 @@ packages/plugin/pubspec.yaml }); test('version change check ignores files outside the package', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -840,14 +840,14 @@ tool/plugin/lib/plugin.dart }); test('allows missing version change for exempt changes', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -873,14 +873,14 @@ packages/plugin/CHANGELOG.md }); test('allows missing version change with justification', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -914,14 +914,14 @@ No version change: Code change is only to implementation comments. }); test('fails if a CHANGELOG change is missing', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -949,14 +949,14 @@ packages/plugin/example/lib/foo.dart }); test('passes CHANGELOG check when the CHANGELOG is changed', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -980,14 +980,14 @@ packages/plugin/CHANGELOG.md test('fails CHANGELOG check if only another package CHANGELOG chages', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -1014,14 +1014,14 @@ packages/another_plugin/CHANGELOG.md }); test('allows missing CHANGELOG change with justification', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + createFakeCHANGELOG(plugin, changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index 097d4d21cb1..51e8e328329 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -82,13 +82,12 @@ void main() { }); test('runs for iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', @@ -184,14 +183,12 @@ void main() { }); test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', @@ -251,15 +248,13 @@ void main() { group('combined', () { test('runs both iOS and macOS when supported', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformIOS: const PlatformDetails(PlatformSupport.inline), platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', @@ -311,14 +306,12 @@ void main() { }); test('runs only macOS for a macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', @@ -353,13 +346,12 @@ void main() { }); test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', From 5d92a4717a7a266f485974ff5403122ef2570935 Mon Sep 17 00:00:00 2001 From: Ahmed Ashour Date: Mon, 9 May 2022 20:54:11 +0200 Subject: [PATCH 184/249] Enable lints `library_private_types_in_public_api`, `sort_child_properties_last` and `use_key_in_widget_constructors` (#5428) --- script/tool/CHANGELOG.md | 2 +- script/tool/test/util.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index b2319c63dc4..5faa7c8201d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -322,7 +322,7 @@ and `firebase-test-lab`. ## v.0.0.36+2 -- Default to showing podspec lint warnings +- Default to showing podspec lint warnings. ## v.0.0.36+1 diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 5c38bd5f703..b0a8990e130 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -319,14 +319,14 @@ String _pluginPlatformSection( return entry; } -typedef _ErrorHandler = void Function(Error error); +typedef ErrorHandler = void Function(Error error); /// Run the command [runner] with the given [args] and return /// what was printed. /// A custom [errorHandler] can be used to handle the runner error as desired without throwing. Future> runCapturingPrint( CommandRunner runner, List args, - {_ErrorHandler? errorHandler}) async { + {ErrorHandler? errorHandler}) async { final List prints = []; final ZoneSpecification spec = ZoneSpecification( print: (_, __, ___, String message) { From 1a124b18d36ad666bade401dd4b4a87180a99090 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 10 May 2022 13:05:18 -0400 Subject: [PATCH 185/249] Revert "Enable lints `library_private_types_in_public_api`, `sort_child_properties_last` and `use_key_in_widget_constructors`" (#5691) This reverts commit 5d92a4717a7a266f485974ff5403122ef2570935. This includes a fix for a latent bug in the version-check repo tooling command that caused it to fail when reverting a package that previously had a NEXT section, so that tests will pass. --- script/tool/CHANGELOG.md | 6 +++- .../tool/lib/src/version_check_command.dart | 25 +++++++++------ script/tool/test/util.dart | 4 +-- .../tool/test/version_check_command_test.dart | 31 +++++++++++++++++-- 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 5faa7c8201d..9ed2a927865 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +- Fixes changelog validation when reverting to a `NEXT` state. + ## 0.8.5 - Updates `test` to inculde the Dart unit tests of examples, if any. @@ -322,7 +326,7 @@ and `firebase-test-lab`. ## v.0.0.36+2 -- Default to showing podspec lint warnings. +- Default to showing podspec lint warnings ## v.0.0.36+1 diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index fb768c19b52..c0e67764360 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -39,8 +39,11 @@ enum _CurrentVersionState { /// The version is unchanged. unchanged, - /// The version has changed, and the transition is valid. - validChange, + /// The version has increased, and the transition is valid. + validIncrease, + + /// The version has decrease, and the transition is a valid revert. + validRevert, /// The version has changed, and the transition is invalid. invalidChange, @@ -218,7 +221,8 @@ class VersionCheckCommand extends PackageLoopingCommand { case _CurrentVersionState.unchanged: versionChanged = false; break; - case _CurrentVersionState.validChange: + case _CurrentVersionState.validIncrease: + case _CurrentVersionState.validRevert: versionChanged = true; break; case _CurrentVersionState.invalidChange: @@ -232,7 +236,7 @@ class VersionCheckCommand extends PackageLoopingCommand { } if (!(await _validateChangelogVersion(package, - pubspec: pubspec, pubspecVersionChanged: versionChanged))) { + pubspec: pubspec, pubspecVersionState: versionState))) { errors.add('CHANGELOG.md failed validation.'); } @@ -322,7 +326,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); logWarning( '${indentation}If this plugin is not new, something has gone wrong.'); - return _CurrentVersionState.validChange; // Assume new, thus valid. + return _CurrentVersionState.validIncrease; // Assume new, thus valid. } if (previousVersion == currentVersion) { @@ -340,7 +344,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} if (possibleVersionsFromNewVersion.containsKey(previousVersion)) { logWarning('${indentation}New version is lower than previous version. ' 'This is assumed to be a revert.'); - return _CurrentVersionState.validChange; + return _CurrentVersionState.validRevert; } } @@ -367,7 +371,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} return _CurrentVersionState.invalidChange; } - return _CurrentVersionState.validChange; + return _CurrentVersionState.validIncrease; } /// Checks whether or not [package]'s CHANGELOG's versioning is correct, @@ -378,7 +382,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} Future _validateChangelogVersion( RepositoryPackage package, { required Pubspec pubspec, - required bool pubspecVersionChanged, + required _CurrentVersionState pubspecVersionState, }) async { // This method isn't called unless `version` is non-null. final Version fromPubspec = pubspec.version!; @@ -405,8 +409,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // changes that don't warrant publishing on their own. final bool hasNextSection = versionString == 'NEXT'; if (hasNextSection) { - // NEXT should not be present in a commit that changes the version. - if (pubspecVersionChanged) { + // NEXT should not be present in a commit that increases the version. + if (pubspecVersionState == _CurrentVersionState.validIncrease || + pubspecVersionState == _CurrentVersionState.invalidChange) { printError(badNextErrorMessage); return false; } diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index b0a8990e130..5c38bd5f703 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -319,14 +319,14 @@ String _pluginPlatformSection( return entry; } -typedef ErrorHandler = void Function(Error error); +typedef _ErrorHandler = void Function(Error error); /// Run the command [runner] with the given [args] and return /// what was printed. /// A custom [errorHandler] can be used to handle the runner error as desired without throwing. Future> runCapturingPrint( CommandRunner runner, List args, - {ErrorHandler? errorHandler}) async { + {_ErrorHandler? errorHandler}) async { final List prints = []; final ZoneSpecification spec = ZoneSpecification( print: (_, __, ___, String message) { diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index aeacd77635d..5b8ed97e20c 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -44,7 +44,7 @@ class MockProcessResult extends Mock implements io.ProcessResult {} void main() { const String indentation = ' '; - group('$VersionCheckCommand', () { + group('VersionCheckCommand', () { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; @@ -602,7 +602,7 @@ This is necessary because of X, Y, and Z ); }); - test('Fail if the version changes without replacing NEXT', () async { + test('fails if the version increases without replacing NEXT', () async { final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.1'); @@ -632,6 +632,33 @@ This is necessary because of X, Y, and Z ); }); + test('allows NEXT for a revert', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## NEXT +* Some changes that should be listed as part of 1.0.1. +## 1.0.0 +* Some other changes. +'''; + createFakeCHANGELOG(plugin, changelog); + createFakeCHANGELOG(plugin, changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.1'), + ]; + + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main']); + expect( + output, + containsAllInOrder([ + contains('New version is lower than previous version. ' + 'This is assumed to be a revert.'), + ]), + ); + }); + test( 'fails gracefully if the version headers are not found due to using the wrong style', () async { From 9544f04a50f4f192110699749fa1bdeaaef6965e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 10 May 2022 14:43:28 -0400 Subject: [PATCH 186/249] Re-land: Enable lints `library_private_types_in_public_api`, `sort_child_properties_last` and `use_key_in_widget_constructors` (#5692) Re-lands https://github.com/flutter/plugins/pull/5428 This is a revert of flutter/plugins#5691 (the revert of the above) with the following changes: - Excludes the repo tooling changes that had to be added to the revert, since we want those - Fixes local_auth: - Updates code for the new analysis failure - Fixes the bad version merge that dropped the version change - Reverts a version change in `file_selector_platform_interface`, which didn't otherwise change --- script/tool/test/util.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 5c38bd5f703..b0a8990e130 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -319,14 +319,14 @@ String _pluginPlatformSection( return entry; } -typedef _ErrorHandler = void Function(Error error); +typedef ErrorHandler = void Function(Error error); /// Run the command [runner] with the given [args] and return /// what was printed. /// A custom [errorHandler] can be used to handle the runner error as desired without throwing. Future> runCapturingPrint( CommandRunner runner, List args, - {_ErrorHandler? errorHandler}) async { + {ErrorHandler? errorHandler}) async { final List prints = []; final ZoneSpecification spec = ZoneSpecification( print: (_, __, ___, String message) { From e8b4147fcce385eb5b412fa9874494974c4617e5 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 11 May 2022 11:48:47 -0400 Subject: [PATCH 187/249] Re-sync analysis_options.yaml with flutter/flutter (#5695) The analysis options have gotten behind; this re-syncs to the current state of flutter/flutter. For options that are non-trivial to enable, either because they are non-trivial to fix, or touch a very large number of files, they are locally disabled with clear "LOCAL CHANGE" markers so that it's obvious where we are out of sync. For options that are simple to resolve, they are enabled in the PR. Part of https://github.com/flutter/flutter/issues/76229 --- script/tool/lib/src/create_all_plugins_app_command.dart | 2 +- script/tool/lib/src/custom_test_command.dart | 2 +- script/tool/lib/src/format_command.dart | 5 ++--- script/tool/lib/src/license_check_command.dart | 3 +-- script/tool/lib/src/make_deps_path_based_command.dart | 4 ++-- script/tool/lib/src/pubspec_check_command.dart | 8 ++++---- script/tool/lib/src/version_check_command.dart | 6 +++--- script/tool/test/analyze_command_test.dart | 2 +- script/tool/test/common/plugin_command_test.dart | 2 +- script/tool/test/format_command_test.dart | 5 ++--- script/tool/test/publish_plugin_command_test.dart | 8 ++++---- script/tool/test/pubspec_check_command_test.dart | 4 ++-- script/tool/test/util.dart | 2 +- script/tool/test/version_check_command_test.dart | 6 +++--- 14 files changed, 28 insertions(+), 31 deletions(-) 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 ad836e19d9c..595779b8be6 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -110,7 +110,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { newGradle.writeln(' multiDexEnabled true'); } else if (line.contains('dependencies {')) { newGradle.writeln( - ' implementation \'com.google.guava:guava:27.0.1-android\'\n', + " implementation 'com.google.guava:guava:27.0.1-android'\n", ); // Tests for https://github.com/flutter/flutter/issues/43383 newGradle.writeln( diff --git a/script/tool/lib/src/custom_test_command.dart b/script/tool/lib/src/custom_test_command.dart index cd9ac32606a..0ef6e602c07 100644 --- a/script/tool/lib/src/custom_test_command.dart +++ b/script/tool/lib/src/custom_test_command.dart @@ -30,7 +30,7 @@ class CustomTestCommand extends PackageLoopingCommand { @override final String description = 'Runs package-specific custom tests defined in ' - 'a package\'s tool/$_scriptName file.\n\n' + "a package's tool/$_scriptName file.\n\n" 'This command requires "dart" to be in your path.'; @override diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 10c0779de92..f640cbaa5f6 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -130,8 +130,7 @@ class FormatCommand extends PluginCommand { if (clangFiles.isNotEmpty) { final String clangFormat = getStringArg('clang-format'); if (!await _hasDependency(clangFormat)) { - printError( - 'Unable to run \'clang-format\'. Make sure that it is in your ' + printError('Unable to run "clang-format". Make sure that it is in your ' 'path, or provide a full path with --clang-format.'); throw ToolExit(_exitDependencyMissing); } @@ -156,7 +155,7 @@ class FormatCommand extends PluginCommand { final String java = getStringArg('java'); if (!await _hasDependency(java)) { printError( - 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'Unable to run "java". Make sure that it is in your path, or ' 'provide a full path with --java.'); throw ToolExit(_exitDependencyMissing); } diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 87e4c8b1486..5e74d846c13 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -241,8 +241,7 @@ class LicenseCheckCommand extends PluginCommand { } // Sort by path for more usable output. - final int Function(File, File) pathCompare = - (File a, File b) => a.path.compareTo(b.path); + int pathCompare(File a, File b) => a.path.compareTo(b.path); incorrectFirstPartyFiles.sort(pathCompare); unrecognizedThirdPartyFiles.sort(pathCompare); diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index 9b861c34ec9..4bbecb4d224 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -169,8 +169,8 @@ class MakeDepsPathBasedCommand extends PluginCommand { // then re-serialiazing so that it's a localized change, rather than // rewriting the whole file (e.g., destroying comments), which could be // more disruptive for local use. - String newPubspecContents = pubspecContents + - ''' + String newPubspecContents = ''' +$pubspecContents $_dependencyOverrideWarningComment dependency_overrides: diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 654675ebb85..23c9c00e33f 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -225,8 +225,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { bool _checkIssueLink(Pubspec pubspec) { return pubspec.issueTracker ?.toString() - .startsWith(_expectedIssueLinkFormat) == - true; + .startsWith(_expectedIssueLinkFormat) ?? + false; } // Validates the "implements" keyword for a plugin, returning an error @@ -287,8 +287,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { .where((String package) => !dependencies.contains(package)); if (missingPackages.isNotEmpty) { return 'The following default_packages are missing ' - 'corresponding dependencies:\n ' + - missingPackages.join('\n '); + 'corresponding dependencies:\n' + ' ${missingPackages.join('\n ')}'; } return null; diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index c0e67764360..b816ee56999 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -403,7 +403,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final String badNextErrorMessage = '${indentation}When bumping the version ' 'for release, the NEXT section should be incorporated into the new ' - 'version\'s release notes.'; + "version's release notes."; // Skip validation for the special NEXT version that's used to accumulate // changes that don't warrant publishing on their own. @@ -531,7 +531,7 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. final Directory gitRoot = packagesDir.fileSystem.directory((await gitDir).path); final String relativePackagePath = - getRelativePosixPath(package.directory, from: gitRoot) + '/'; + '${getRelativePosixPath(package.directory, from: gitRoot)}/'; bool hasChanges = false; bool needsVersionChange = false; bool hasChangelogChange = false; @@ -594,7 +594,7 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. 'change description.'); } else { printError( - 'No CHANGELOG change found. If this PR needs an exemption from' + 'No CHANGELOG change found. If this PR needs an exemption from ' 'the standard policy of listing all changes in the CHANGELOG, ' 'please add a line starting with\n' '$_missingChangelogChangeJustificationMarker\n' diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index e293e8b85e9..a9b83349306 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -93,7 +93,7 @@ void main() { ])); }); - test('don\'t elide a non-contained example package', () async { + test("don't elide a non-contained example package", () async { final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('example', packagesDir); diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 7ed3d239b2a..8c6b3868241 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -162,7 +162,7 @@ void main() { expect(command.plugins, unorderedEquals([plugin2.path])); }); - test('exclude packages when packages flag isn\'t specified', () async { + test("exclude packages when packages flag isn't specified", () async { createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint( diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index 3fa7782245a..5bd6f97832f 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -218,7 +218,7 @@ void main() { output, containsAllInOrder([ contains( - 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'Unable to run "java". Make sure that it is in your path, or ' 'provide a full path with --java.'), ])); }); @@ -330,8 +330,7 @@ void main() { expect( output, containsAllInOrder([ - contains( - 'Unable to run \'clang-format\'. Make sure that it is in your ' + contains('Unable to run "clang-format". Make sure that it is in your ' 'path, or provide a full path with --clang-format.'), ])); }); diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 857828ab930..d443f8ff017 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -103,7 +103,7 @@ void main() { expect( output, containsAllInOrder([ - contains('There are files in the package directory that haven\'t ' + contains("There are files in the package directory that haven't " 'been saved in git. Refusing to publish these files:\n\n' '?? /packages/foo/tmp\n\n' 'If the directory should be clean, you can run `git clean -xdf && ' @@ -113,7 +113,7 @@ void main() { ])); }); - test('fails immediately if the remote doesn\'t exist', () async { + test("fails immediately if the remote doesn't exist", () async { createFakePlugin('foo', packagesDir, examples: []); processRunner.mockProcessesForExecutable['git-remote'] = [ @@ -877,8 +877,8 @@ class MockStdin extends Mock implements io.Stdin { } @override - StreamSubscription> listen(void onData(List event)?, - {Function? onError, void onDone()?, bool? cancelOnError}) { + StreamSubscription> listen(void Function(List event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { return _controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 89bb98abd80..fbe31c72bc2 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -43,7 +43,7 @@ String _headerSection( repositoryPath, ]; final String repoLink = - 'https://github.com/' + repoLinkPathComponents.join('/'); + 'https://github.com/${repoLinkPathComponents.join('/')}'; final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; description ??= 'A test package for validating that the pubspec.yaml ' @@ -55,7 +55,7 @@ ${includeRepository ? 'repository: $repoLink' : ''} ${includeHomepage ? 'homepage: $repoLink' : ''} ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} version: 1.0.0 -${publishable ? '' : 'publish_to: \'none\''} +${publishable ? '' : "publish_to: 'none'"} '''; } diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index b0a8990e130..effdd03891d 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -313,7 +313,7 @@ String _pluginPlatformSection( assert(false, 'Unrecognized platform: $platform'); break; } - entry = lines.join('\n') + '\n'; + entry = '${lines.join('\n')}\n'; } return entry; diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 5b8ed97e20c..6af3c112f9e 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -561,7 +561,7 @@ This is necessary because of X, Y, and Z output, containsAllInOrder([ contains('When bumping the version for release, the NEXT section ' - 'should be incorporated into the new version\'s release notes.') + "should be incorporated into the new version's release notes.") ]), ); }); @@ -595,7 +595,7 @@ This is necessary because of X, Y, and Z output, containsAllInOrder([ contains('When bumping the version for release, the NEXT section ' - 'should be incorporated into the new version\'s release notes.'), + "should be incorporated into the new version's release notes."), contains('plugin:\n' ' CHANGELOG.md failed validation.'), ]), @@ -627,7 +627,7 @@ This is necessary because of X, Y, and Z output, containsAllInOrder([ contains('When bumping the version for release, the NEXT section ' - 'should be incorporated into the new version\'s release notes.') + "should be incorporated into the new version's release notes.") ]), ); }); From 5f2b2300efc098855de3c72942f3c43c44b92db2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 13 May 2022 20:34:12 -0400 Subject: [PATCH 188/249] [tools] Fix `publish` flag calculation (#5694) --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/common/plugin_command.dart | 4 ++- .../tool/lib/src/publish_plugin_command.dart | 17 ++++++----- .../test/publish_plugin_command_test.dart | 29 +++++++++++++++++++ 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 9ed2a927865..a8a8268c047 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT - Fixes changelog validation when reverting to a `NEXT` state. +- Fixes multiplication of `--force` flag when publishing multiple packages. ## 0.8.5 diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 0ec890368fb..be9fb23e57a 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -192,7 +192,9 @@ abstract class PluginCommand extends Command { /// Convenience accessor for List arguments. List getStringListArg(String key) { - return (argResults![key] as List?) ?? []; + // Clone the list so that if a caller modifies the result it won't change + // the actual arguments list for future queries. + return List.from(argResults![key] as List? ?? []); } /// If true, commands should log timing information that might be useful in diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 05f0afd0c06..7aa70bd4fd1 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -121,6 +121,8 @@ class PublishPluginCommand extends PackageLoopingCommand { List _existingGitTags = []; // The remote to push tags to. late _RemoteInfo _remote; + // Flags to pass to `pub publish`. + late List _publishFlags; @override String get successSummaryMessage => 'published'; @@ -149,6 +151,11 @@ class PublishPluginCommand extends PackageLoopingCommand { _existingGitTags = (existingTagsResult.stdout as String).split('\n') ..removeWhere((String element) => element.isEmpty); + _publishFlags = [ + ...getStringListArg(_pubFlagsOption), + if (getBoolArg(_skipConfirmationFlag)) '--force', + ]; + if (getBoolArg(_dryRunFlag)) { print('=============== DRY RUN ==============='); } @@ -333,22 +340,18 @@ Safe to ignore if the package is deleted in this commit. Future _publish(RepositoryPackage package) async { print('Publishing...'); - final List publishFlags = getStringListArg(_pubFlagsOption); - print('Running `pub publish ${publishFlags.join(' ')}` in ' + print('Running `pub publish ${_publishFlags.join(' ')}` in ' '${package.directory.absolute.path}...\n'); if (getBoolArg(_dryRunFlag)) { return true; } - if (getBoolArg(_skipConfirmationFlag)) { - publishFlags.add('--force'); - } - if (publishFlags.contains('--force')) { + if (_publishFlags.contains('--force')) { _ensureValidPubCredential(); } final io.Process publish = await processRunner.start( - flutterCommand, ['pub', 'publish'] + publishFlags, + flutterCommand, ['pub', 'publish', ..._publishFlags], workingDirectory: package.directory); publish.stdout.transform(utf8.decoder).listen((String data) => print(data)); publish.stderr.transform(utf8.decoder).listen((String data) => print(data)); diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index d443f8ff017..f3be3b48b1f 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -224,6 +224,35 @@ void main() { plugin.path))); }); + test('--force is only added once, regardless of plugin count', () async { + _createMockCredentialFile(); + final RepositoryPackage plugin1 = + createFakePlugin('plugin_a', packagesDir, examples: []); + final RepositoryPackage plugin2 = + createFakePlugin('plugin_b', packagesDir, examples: []); + + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=plugin_a,plugin_b', + '--skip-confirmation', + '--pub-publish-flags', + '--server=bar' + ]); + + expect( + processRunner.recordedCalls, + containsAllInOrder([ + ProcessCall( + flutterCommand, + const ['pub', 'publish', '--server=bar', '--force'], + plugin1.path), + ProcessCall( + flutterCommand, + const ['pub', 'publish', '--server=bar', '--force'], + plugin2.path), + ])); + }); + test('throws if pub publish fails', () async { createFakePlugin('foo', packagesDir, examples: []); From 517d3761f211154cdd836f3b71a7ea02d0019f39 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 17 May 2022 20:32:10 -0400 Subject: [PATCH 189/249] [tools] Validate example READMEs (#5775) --- script/tool/CHANGELOG.md | 2 + .../src/common/package_looping_command.dart | 43 +++- .../tool/lib/src/pubspec_check_command.dart | 3 +- script/tool/lib/src/readme_check_command.dart | 73 +++++-- script/tool/lib/src/test_command.dart | 3 +- .../common/package_looping_command_test.dart | 80 ++++++- script/tool/test/list_command_test.dart | 3 + .../tool/test/readme_check_command_test.dart | 200 +++++++++++++++--- script/tool/test/util.dart | 17 +- .../tool/test/version_check_command_test.dart | 44 ++-- 10 files changed, 372 insertions(+), 96 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a8a8268c047..0e2a33e15ea 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,8 @@ - Fixes changelog validation when reverting to a `NEXT` state. - Fixes multiplication of `--force` flag when publishing multiple packages. +- Checks for template boilerplate in `readme-check`. +- `readme-check` now validates example READMEs when present. ## 0.8.5 diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index b48743be317..a295215f362 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -16,6 +16,20 @@ import 'plugin_command.dart'; import 'process_runner.dart'; import 'repository_package.dart'; +/// Enumeration options for package looping commands. +enum PackageLoopingType { + /// Only enumerates the top level packages, without including any of their + /// subpackages. + topLevelOnly, + + /// Enumerates the top level packages and any example packages they contain. + includeExamples, + + /// Enumerates all packages recursively, including both example and + /// non-example subpackages. + includeAllSubpackages, +} + /// Possible outcomes of a command run for a package. enum RunState { /// The command succeeded for the package. @@ -109,9 +123,26 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Note: Consistent behavior across commands whenever possibel is a goal for /// this tool, so this should be overridden only in rare cases. Stream getPackagesToProcess() async* { - yield* includeSubpackages - ? getTargetPackagesAndSubpackages(filterExcluded: false) - : getTargetPackages(filterExcluded: false); + switch (packageLoopingType) { + case PackageLoopingType.topLevelOnly: + yield* getTargetPackages(filterExcluded: false); + break; + case PackageLoopingType.includeExamples: + await for (final PackageEnumerationEntry packageEntry + in getTargetPackages(filterExcluded: false)) { + yield packageEntry; + yield* Stream.fromIterable(packageEntry + .package + .getExamples() + .map((RepositoryPackage package) => PackageEnumerationEntry( + package, + excluded: packageEntry.excluded))); + } + break; + case PackageLoopingType.includeAllSubpackages: + yield* getTargetPackagesAndSubpackages(filterExcluded: false); + break; + } } /// Runs the command for [package], returning a list of errors. @@ -140,9 +171,9 @@ abstract class PackageLoopingCommand extends PluginCommand { /// to make the output structure easier to follow. bool get hasLongOutput => true; - /// Whether to loop over all packages (e.g., including example/), rather than - /// only top-level packages. - bool get includeSubpackages => false; + /// Whether to loop over top-level packages only, or some or all of their + /// sub-packages as well. + PackageLoopingType get packageLoopingType => PackageLoopingType.topLevelOnly; /// The text to output at the start when reporting one or more failures. /// This will be followed by a list of packages that reported errors, with diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 23c9c00e33f..3598a39da96 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -64,7 +64,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { bool get hasLongOutput => false; @override - bool get includeSubpackages => true; + PackageLoopingType get packageLoopingType => + PackageLoopingType.includeAllSubpackages; @override Future runForPackage(RepositoryPackage package) async { diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index 0cb64920dea..6e79b736781 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -54,34 +54,72 @@ class ReadmeCheckCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - final File readme = package.readmeFile; + final List errors = _validateReadme(package.readmeFile, + mainPackage: package, isExample: false); + for (final RepositoryPackage packageToCheck in package.getExamples()) { + errors.addAll(_validateReadme(packageToCheck.readmeFile, + mainPackage: package, isExample: true)); + } - if (!readme.existsSync()) { - return PackageResult.fail(['Missing README.md']); + // If there's an example/README.md for a multi-example package, validate + // that as well, as it will be shown on pub.dev. + final Directory exampleDir = package.directory.childDirectory('example'); + final File exampleDirReadme = exampleDir.childFile('README.md'); + if (exampleDir.existsSync() && !isPackage(exampleDir)) { + errors.addAll(_validateReadme(exampleDirReadme, + mainPackage: package, isExample: true)); } - final List errors = []; + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); + } + + List _validateReadme(File readme, + {required RepositoryPackage mainPackage, required bool isExample}) { + if (!readme.existsSync()) { + if (isExample) { + print('${indentation}No README for ' + '${getRelativePosixPath(readme.parent, from: mainPackage.directory)}'); + return []; + } else { + printError('${indentation}No README found at ' + '${getRelativePosixPath(readme, from: mainPackage.directory)}'); + return ['Missing README.md']; + } + } - final Pubspec pubspec = package.parsePubspec(); - final bool isPlugin = pubspec.flutter?['plugin'] != null; + print('${indentation}Checking ' + '${getRelativePosixPath(readme, from: mainPackage.directory)}...'); - final List readmeLines = package.readmeFile.readAsLinesSync(); + final List readmeLines = readme.readAsLinesSync(); + final List errors = []; final String? blockValidationError = _validateCodeBlocks(readmeLines); if (blockValidationError != null) { errors.add(blockValidationError); } - if (isPlugin && (!package.isFederated || package.isAppFacing)) { - final String? error = _validateSupportedPlatforms(readmeLines, pubspec); - if (error != null) { - errors.add(error); + if (_containsTemplateBoilerplate(readmeLines)) { + printError('${indentation}The boilerplate section about getting started ' + 'with Flutter should not be left in.'); + errors.add('Contains template boilerplate'); + } + + // Check if this is the main readme for a plugin, and if so enforce extra + // checks. + if (!isExample) { + final Pubspec pubspec = mainPackage.parsePubspec(); + final bool isPlugin = pubspec.flutter?['plugin'] != null; + if (isPlugin && (!mainPackage.isFederated || mainPackage.isAppFacing)) { + final String? error = _validateSupportedPlatforms(readmeLines, pubspec); + if (error != null) { + errors.add(error); + } } } - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); + return errors; } /// Validates that code blocks (``` ... ```) follow repository standards. @@ -223,4 +261,11 @@ ${indentation * 2}Please use standard capitalizations: ${sortedListString(expect // https://github.com/flutter/flutter/issues/84200 return null; } + + /// Returns true if the README still has the boilerplate from the + /// `flutter create` templates. + bool _containsTemplateBoilerplate(List readmeLines) { + return readmeLines.any((String line) => + line.contains('For help getting started with Flutter')); + } } diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 27a01c95e85..5101b8f19e7 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -37,7 +37,8 @@ class TestCommand extends PackageLoopingCommand { 'This command requires "flutter" to be in your path.'; @override - bool get includeSubpackages => true; + PackageLoopingType get packageLoopingType => + PackageLoopingType.includeAllSubpackages; @override Future runForPackage(RepositoryPackage package) async { diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 3dd6a47ae2f..a7e7dfdf6eb 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -98,7 +98,7 @@ void main() { TestPackageLoopingCommand createTestCommand({ String gitDiffResponse = '', bool hasLongOutput = true, - bool includeSubpackages = false, + PackageLoopingType packageLoopingType = PackageLoopingType.topLevelOnly, bool failsDuringInit = false, bool warnsDuringInit = false, bool warnsDuringCleanup = false, @@ -122,7 +122,7 @@ void main() { packagesDir, platform: mockPlatform, hasLongOutput: hasLongOutput, - includeSubpackages: includeSubpackages, + packageLoopingType: packageLoopingType, failsDuringInit: failsDuringInit, warnsDuringInit: warnsDuringInit, warnsDuringCleanup: warnsDuringCleanup, @@ -236,14 +236,17 @@ void main() { unorderedEquals([package1.path, package2.path])); }); - test('includes subpackages when requested', () async { + test('includes all subpackages when requested', () async { final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, examples: ['example1', 'example2']); final RepositoryPackage package = createFakePackage('a_package', packagesDir); + final RepositoryPackage subPackage = createFakePackage( + 'sub_package', package.directory, + examples: []); - final TestPackageLoopingCommand command = - createTestCommand(includeSubpackages: true); + final TestPackageLoopingCommand command = createTestCommand( + packageLoopingType: PackageLoopingType.includeAllSubpackages); await runCommand(command); expect( @@ -254,18 +257,72 @@ void main() { getExampleDir(plugin).childDirectory('example2').path, package.path, getExampleDir(package).path, + subPackage.path, ])); }); + test('includes examples when requested', () async { + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + final RepositoryPackage subPackage = + createFakePackage('sub_package', package.directory); + + final TestPackageLoopingCommand command = createTestCommand( + packageLoopingType: PackageLoopingType.includeExamples); + await runCommand(command); + + expect( + command.checkedPackages, + unorderedEquals([ + plugin.path, + getExampleDir(plugin).childDirectory('example1').path, + getExampleDir(plugin).childDirectory('example2').path, + package.path, + getExampleDir(package).path, + ])); + expect(command.checkedPackages, isNot(contains(subPackage.path))); + }); + test('excludes subpackages when main package is excluded', () async { final RepositoryPackage excluded = createFakePlugin( 'a_plugin', packagesDir, examples: ['example1', 'example2']); final RepositoryPackage included = createFakePackage('a_package', packagesDir); + final RepositoryPackage subpackage = + createFakePackage('sub_package', excluded.directory); - final TestPackageLoopingCommand command = - createTestCommand(includeSubpackages: true); + final TestPackageLoopingCommand command = createTestCommand( + packageLoopingType: PackageLoopingType.includeAllSubpackages); + await runCommand(command, arguments: ['--exclude=a_plugin']); + + final Iterable examples = excluded.getExamples(); + + expect( + command.checkedPackages, + unorderedEquals([ + included.path, + getExampleDir(included).path, + ])); + expect(command.checkedPackages, isNot(contains(excluded.path))); + expect(examples.length, 2); + for (final RepositoryPackage example in examples) { + expect(command.checkedPackages, isNot(contains(example.path))); + } + expect(command.checkedPackages, isNot(contains(subpackage.path))); + }); + + test('excludes examples when main package is excluded', () async { + final RepositoryPackage excluded = createFakePlugin( + 'a_plugin', packagesDir, + examples: ['example1', 'example2']); + final RepositoryPackage included = + createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = createTestCommand( + packageLoopingType: PackageLoopingType.includeExamples); await runCommand(command, arguments: ['--exclude=a_plugin']); final Iterable examples = excluded.getExamples(); @@ -290,8 +347,9 @@ void main() { final RepositoryPackage included = createFakePackage('a_package', packagesDir); - final TestPackageLoopingCommand command = - createTestCommand(includeSubpackages: true, hasLongOutput: false); + final TestPackageLoopingCommand command = createTestCommand( + packageLoopingType: PackageLoopingType.includeAllSubpackages, + hasLongOutput: false); final List output = await runCommand(command, arguments: [ '--skip-if-not-supporting-flutter-version=2.5.0' ]); @@ -769,7 +827,7 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { Directory packagesDir, { required Platform platform, this.hasLongOutput = true, - this.includeSubpackages = false, + this.packageLoopingType = PackageLoopingType.topLevelOnly, this.customFailureListHeader, this.customFailureListFooter, this.failsDuringInit = false, @@ -795,7 +853,7 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { bool hasLongOutput; @override - bool includeSubpackages; + PackageLoopingType packageLoopingType; @override String get failureListHeader => diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index c2042c26638..f74431c5cee 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -101,15 +101,18 @@ void main() { '/packages/plugin1/pubspec.yaml', '/packages/plugin1/AUTHORS', '/packages/plugin1/CHANGELOG.md', + '/packages/plugin1/README.md', '/packages/plugin1/example/pubspec.yaml', '/packages/plugin2/pubspec.yaml', '/packages/plugin2/AUTHORS', '/packages/plugin2/CHANGELOG.md', + '/packages/plugin2/README.md', '/packages/plugin2/example/example1/pubspec.yaml', '/packages/plugin2/example/example2/pubspec.yaml', '/packages/plugin3/pubspec.yaml', '/packages/plugin3/AUTHORS', '/packages/plugin3/CHANGELOG.md', + '/packages/plugin3/README.md', ]), ); }); diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart index f53fa06a813..fa4fc604dd7 100644 --- a/script/tool/test/readme_check_command_test.dart +++ b/script/tool/test/readme_check_command_test.dart @@ -37,8 +37,33 @@ void main() { runner.addCommand(command); }); - test('fails when README is missing', () async { - createFakePackage('a_package', packagesDir); + test('prints paths of checked READMEs', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, + examples: ['example1', 'example2']); + for (final RepositoryPackage example in package.getExamples()) { + example.readmeFile.writeAsStringSync('A readme'); + } + getExampleDir(package).childFile('README.md').writeAsStringSync('A readme'); + + final List output = + await runCapturingPrint(runner, ['readme-check']); + + expect( + output, + containsAll([ + contains(' Checking README.md...'), + contains(' Checking example/README.md...'), + contains(' Checking example/example1/README.md...'), + contains(' Checking example/example2/README.md...'), + ]), + ); + }); + + test('fails when package README is missing', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + package.readmeFile.deleteSync(); Error? commandError; final List output = await runCapturingPrint( @@ -55,6 +80,143 @@ void main() { ); }); + test('passes when example README is missing', () async { + createFakePackage('a_package', packagesDir); + + final List output = + await runCapturingPrint(runner, ['readme-check']); + + expect( + output, + containsAllInOrder([ + contains('No README for example'), + ]), + ); + }); + + test('does not inculde non-example subpackages', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + const String subpackageName = 'special_test'; + final RepositoryPackage miscSubpackage = + createFakePackage(subpackageName, package.directory); + miscSubpackage.readmeFile.delete(); + + final List output = + await runCapturingPrint(runner, ['readme-check']); + + expect(output, isNot(contains(subpackageName))); + }); + + test('fails when README still has plugin template boilerplate', () async { + final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir); + package.readmeFile.writeAsStringSync(''' +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The boilerplate section about getting started with Flutter ' + 'should not be left in.'), + contains('Contains template boilerplate'), + ]), + ); + }); + + test('fails when example README still has application template boilerplate', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + package.getExamples().first.readmeFile.writeAsStringSync(''' +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The boilerplate section about getting started with Flutter ' + 'should not be left in.'), + contains('Contains template boilerplate'), + ]), + ); + }); + + test( + 'fails when multi-example top-level example directory README still has ' + 'application template boilerplate', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, + examples: ['example1', 'example2']); + package.directory + .childDirectory('example') + .childFile('README.md') + .writeAsStringSync(''' +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The boilerplate section about getting started with Flutter ' + 'should not be left in.'), + contains('Contains template boilerplate'), + ]), + ); + }); + group('plugin OS support', () { test( 'does not check support table for anything other than app-facing plugin packages', @@ -62,20 +224,12 @@ void main() { const String federatedPluginName = 'a_federated_plugin'; final Directory federatedDir = packagesDir.childDirectory(federatedPluginName); - final List packages = [ - // A non-plugin package. - createFakePackage('a_package', packagesDir), - // Non-app-facing parts of a federated plugin. - createFakePlugin( - '${federatedPluginName}_platform_interface', federatedDir), - createFakePlugin('${federatedPluginName}_android', federatedDir), - ]; - - for (final RepositoryPackage package in packages) { - package.readmeFile.writeAsStringSync(''' -A very useful package. -'''); - } + // A non-plugin package. + createFakePackage('a_package', packagesDir); + // Non-app-facing parts of a federated plugin. + createFakePlugin( + '${federatedPluginName}_platform_interface', federatedDir); + createFakePlugin('${federatedPluginName}_android', federatedDir); final List output = await runCapturingPrint(runner, [ 'readme-check', @@ -94,12 +248,7 @@ A very useful package. test('fails when non-federated plugin is missing an OS support table', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. -'''); + createFakePlugin('a_plugin', packagesDir); Error? commandError; final List output = await runCapturingPrint( @@ -119,12 +268,7 @@ A very useful plugin. test( 'fails when app-facing part of a federated plugin is missing an OS support table', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. -'''); + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); Error? commandError; final List output = await runCapturingPrint( diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index effdd03891d..7255bf9a927 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -125,7 +125,7 @@ RepositoryPackage createFakePlugin( /// separators, of extra files to create in the package. /// /// If [includeCommonFiles] is true, common but non-critical files like -/// CHANGELOG.md and AUTHORS will be included. +/// CHANGELOG.md, README.md, and AUTHORS will be included. /// /// If non-null, [directoryName] will be used for the directory instead of /// [name]. @@ -152,11 +152,12 @@ RepositoryPackage createFakePackage( version: version, flutterConstraint: flutterConstraint); if (includeCommonFiles) { - createFakeCHANGELOG(package, ''' + package.changelogFile.writeAsStringSync(''' ## $version * Some changes. '''); - createFakeAuthors(package); + package.readmeFile.writeAsStringSync('A very useful package'); + package.authorsFile.writeAsStringSync('Google Inc.'); } if (examples.length == 1) { @@ -188,11 +189,6 @@ RepositoryPackage createFakePackage( return package; } -void createFakeCHANGELOG(RepositoryPackage package, String texts) { - package.changelogFile.createSync(); - package.changelogFile.writeAsStringSync(texts); -} - /// Creates a `pubspec.yaml` file with a flutter dependency. /// /// [platformSupport] is a map of platform string to the support details for @@ -267,11 +263,6 @@ $pluginSection package.pubspecFile.writeAsStringSync(yaml); } -void createFakeAuthors(RepositoryPackage package) { - package.authorsFile.createSync(); - package.authorsFile.writeAsStringSync('Google Inc.'); -} - String _pluginPlatformSection( String platform, PlatformDetails support, String packageName) { String entry = ''; diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 6af3c112f9e..a310f0f09fc 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -421,7 +421,7 @@ This is necessary because of X, Y, and Z ## $version * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main']); expect( @@ -439,7 +439,7 @@ This is necessary because of X, Y, and Z ## 1.0.2 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -465,7 +465,7 @@ This is necessary because of X, Y, and Z ## $version * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main']); expect( @@ -488,7 +488,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -518,7 +518,7 @@ This is necessary because of X, Y, and Z ## $version * Some other changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -547,7 +547,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -580,7 +580,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; final List output = await runCapturingPrint( @@ -612,7 +612,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; final List output = await runCapturingPrint( @@ -642,8 +642,8 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(plugin, changelog); - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.1'), ]; @@ -671,7 +671,7 @@ This is necessary because of X, Y, and Z # 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -704,7 +704,7 @@ This is necessary because of X, Y, and Z ## Alpha * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -749,7 +749,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -778,7 +778,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -813,7 +813,7 @@ packages/plugin/lib/plugin.dart ## 1.0.1 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -844,7 +844,7 @@ packages/plugin/pubspec.yaml ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -874,7 +874,7 @@ tool/plugin/lib/plugin.dart ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -907,7 +907,7 @@ packages/plugin/CHANGELOG.md ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -948,7 +948,7 @@ No version change: Code change is only to implementation comments. ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -983,7 +983,7 @@ packages/plugin/example/lib/foo.dart ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -1014,7 +1014,7 @@ packages/plugin/CHANGELOG.md ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -1048,7 +1048,7 @@ packages/another_plugin/CHANGELOG.md ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(plugin, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; From ca9a81d653035364343e1656966b0ee3240b4dbd Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 18 May 2022 11:47:12 -0400 Subject: [PATCH 190/249] [ci/tools] Add iOS/macOS analysis to catch deprecated code (#5778) --- script/tool/CHANGELOG.md | 3 + .../tool/lib/src/xcode_analyze_command.dart | 25 +++++- .../tool/test/xcode_analyze_command_test.dart | 76 +++++++++++++++++++ 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 0e2a33e15ea..04101c22879 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,9 @@ - Fixes changelog validation when reverting to a `NEXT` state. - Fixes multiplication of `--force` flag when publishing multiple packages. +- Adds minimum deployment target flags to `xcode-analyze` to allow + enforcing deprecation warning handling in advance of actually dropping + support for an OS version. - Checks for template boilerplate in `readme-check`. - `readme-check` now validates example READMEs when present. diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index 4298acb1c7e..a81bf15477a 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -23,8 +23,20 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addFlag(platformIOS, help: 'Analyze iOS'); argParser.addFlag(platformMacOS, help: 'Analyze macOS'); + argParser.addOption(_minIOSVersionArg, + help: 'Sets the minimum iOS deployment version to use when compiling, ' + 'overriding the default minimum version. This can be used to find ' + 'deprecation warnings that will affect the plugin in the future.'); + argParser.addOption(_minMacOSVersionArg, + help: + 'Sets the minimum macOS deployment version to use when compiling, ' + 'overriding the default minimum version. This can be used to find ' + 'deprecation warnings that will affect the plugin in the future.'); } + static const String _minIOSVersionArg = 'ios-min-version'; + static const String _minMacOSVersionArg = 'macos-min-version'; + final Xcode _xcode; @override @@ -57,15 +69,24 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { return PackageResult.skip('Not implemented for target platform(s).'); } + final String minIOSVersion = getStringArg(_minIOSVersionArg); + final String minMacOSVersion = getStringArg(_minMacOSVersionArg); + final List failures = []; if (testIOS && !await _analyzePlugin(package, 'iOS', extraFlags: [ '-destination', - 'generic/platform=iOS Simulator' + 'generic/platform=iOS Simulator', + if (minIOSVersion.isNotEmpty) + 'IPHONEOS_DEPLOYMENT_TARGET=$minIOSVersion', ])) { failures.add('iOS'); } - if (testMacOS && !await _analyzePlugin(package, 'macOS')) { + if (testMacOS && + !await _analyzePlugin(package, 'macOS', extraFlags: [ + if (minMacOSVersion.isNotEmpty) + 'MACOSX_DEPLOYMENT_TARGET=$minMacOSVersion', + ])) { failures.add('macOS'); } diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index 51e8e328329..418c695f295 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -123,6 +123,47 @@ void main() { ])); }); + test('passes min iOS deployment version when requested', () async { + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); + + final Directory pluginExampleDirectory = getExampleDir(plugin); + + final List output = await runCapturingPrint(runner, + ['xcode-analyze', '--ios', '--ios-min-version=14.0']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('plugin/example (iOS) passed analysis.') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'IPHONEOS_DEPLOYMENT_TARGET=14.0', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, platformSupport: { @@ -218,6 +259,41 @@ void main() { ])); }); + test('passes min macOS deployment version when requested', () async { + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformMacOS: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = getExampleDir(plugin); + + final List output = await runCapturingPrint(runner, + ['xcode-analyze', '--macos', '--macos-min-version=12.0']); + + expect(output, + contains(contains('plugin/example (macOS) passed analysis.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'MACOSX_DEPLOYMENT_TARGET=12.0', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, platformSupport: { From dd2fc61b1ca6dabeab217e54080454837dee336c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 18 May 2022 19:17:29 -0400 Subject: [PATCH 191/249] [tools] Add `update-release-info` (#5643) --- script/tool/CHANGELOG.md | 4 +- script/tool/README.md | 25 + .../lib/src/common/package_state_utils.dart | 95 +++ script/tool/lib/src/main.dart | 2 + .../lib/src/update_release_info_command.dart | 310 +++++++++ .../tool/lib/src/version_check_command.dart | 41 +- script/tool/pubspec.yaml | 2 +- .../test/common/package_state_utils_test.dart | 140 ++++ .../update_release_info_command_test.dart | 645 ++++++++++++++++++ script/tool/test/util.dart | 14 +- 10 files changed, 1238 insertions(+), 40 deletions(-) create mode 100644 script/tool/lib/src/common/package_state_utils.dart create mode 100644 script/tool/lib/src/update_release_info_command.dart create mode 100644 script/tool/test/common/package_state_utils_test.dart create mode 100644 script/tool/test/update_release_info_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 04101c22879..4b40aecefa9 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 0.8.6 +- Adds `update-release-info` to apply changelog and optional version changes + across multiple packages. - Fixes changelog validation when reverting to a `NEXT` state. - Fixes multiplication of `--force` flag when publishing multiple packages. - Adds minimum deployment target flags to `xcode-analyze` to allow diff --git a/script/tool/README.md b/script/tool/README.md index d52ee08fdc3..cc9621f4408 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -118,6 +118,31 @@ cd dart run ./script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages plugin_name ``` +### Update CHANGELOG and Version + +`update-release-info` will automatically update the version and `CHANGELOG.md` +following standard repository style and practice. It can be used for +single-package updates to handle the details of getting the `CHANGELOG.md` +format correct, but is especially useful for bulk updates across multiple packages. + +For instance, if you add a new analysis option that requires production +code changes across many packages: + +```sh +cd +dart run ./script/tool/bin/flutter_plugin_tools.dart update-release-info \ + --version=minimal \ + --changelog="Fixes violations of new analysis option some_new_option." +``` + +The `minimal` option for `--version` will skip unchanged packages, and treat +each changed package as either `bugfix` or `next` depending on the files that +have changed in that package, so it is often the best choice for a bulk change. + +For cases where you know the change time, `minor` or `bugfix` will make the +corresponding version bump, or `next` will update only `CHANGELOG.md` without +changing the version. + ### Publish a Release **Releases are automated for `flutter/plugins` and `flutter/packages`.** diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart new file mode 100644 index 00000000000..437bbf6df37 --- /dev/null +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -0,0 +1,95 @@ +// 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:meta/meta.dart'; +import 'package:path/path.dart' as p; + +import 'repository_package.dart'; + +/// The state of a package on disk relative to git state. +@immutable +class PackageChangeState { + /// Creates a new immutable state instance. + const PackageChangeState({ + required this.hasChanges, + required this.hasChangelogChange, + required this.needsVersionChange, + }); + + /// True if there are any changes to files in the package. + final bool hasChanges; + + /// True if the package's CHANGELOG.md has been changed. + final bool hasChangelogChange; + + /// True if any changes in the package require a version change according + /// to repository policy. + final bool needsVersionChange; +} + +/// Checks [package] against [changedPaths] to determine what changes it has +/// and how those changes relate to repository policy about CHANGELOG and +/// version updates. +/// +/// [changedPaths] should be a list of POSIX-style paths from a common root, +/// and [relativePackagePath] should be the path to [package] from that same +/// root. Commonly these will come from `gitVersionFinder.getChangedFiles()` +/// and `getRelativePoixPath(package.directory, gitDir.path)` respectively; +/// they are arguments mainly to allow for caching the changed paths for an +/// entire command run. +PackageChangeState checkPackageChangeState( + RepositoryPackage package, { + required List changedPaths, + required String relativePackagePath, +}) { + final String packagePrefix = relativePackagePath.endsWith('/') + ? relativePackagePath + : '$relativePackagePath/'; + + bool hasChanges = false; + bool hasChangelogChange = false; + bool needsVersionChange = false; + for (final String path in changedPaths) { + // Only consider files within the package. + if (!path.startsWith(packagePrefix)) { + continue; + } + final String packageRelativePath = path.substring(packagePrefix.length); + hasChanges = true; + + final List components = p.posix.split(packageRelativePath); + if (components.isEmpty) { + continue; + } + final bool isChangelog = components.first == 'CHANGELOG.md'; + if (isChangelog) { + hasChangelogChange = true; + } + + if (!needsVersionChange && + !isChangelog && + // One of a few special files example will be shown on pub.dev, but for + // anything else in the example publishing has no purpose. + !(components.first == 'example' && + !{'main.dart', 'readme.md', 'example.md'} + .contains(components.last.toLowerCase())) && + // Changes to tests don't need to be published. + !components.contains('test') && + !components.contains('androidTest') && + !components.contains('RunnerTests') && + !components.contains('RunnerUITests') && + // The top-level "tool" directory is for non-client-facing utility code, + // so doesn't need to be published. + components.first != 'tool' && + // Ignoring lints doesn't affect clients. + !components.contains('lint-baseline.xml')) { + needsVersionChange = true; + } + } + + return PackageChangeState( + hasChanges: hasChanges, + hasChangelogChange: hasChangelogChange, + needsVersionChange: needsVersionChange); +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 9c572ee270e..739aef56878 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -29,6 +29,7 @@ import 'pubspec_check_command.dart'; import 'readme_check_command.dart'; import 'test_command.dart'; import 'update_excerpts_command.dart'; +import 'update_release_info_command.dart'; import 'version_check_command.dart'; import 'xcode_analyze_command.dart'; @@ -70,6 +71,7 @@ void main(List args) { ..addCommand(ReadmeCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) ..addCommand(UpdateExcerptsCommand(packagesDir)) + ..addCommand(UpdateReleaseInfoCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) ..addCommand(XcodeAnalyzeCommand(packagesDir)); diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart new file mode 100644 index 00000000000..b998615ead1 --- /dev/null +++ b/script/tool/lib/src/update_release_info_command.dart @@ -0,0 +1,310 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +import 'common/git_version_finder.dart'; +import 'common/package_looping_command.dart'; +import 'common/package_state_utils.dart'; +import 'common/repository_package.dart'; + +/// Supported version change types, from smallest to largest component. +enum _VersionIncrementType { build, bugfix, minor } + +/// Possible results of attempting to update a CHANGELOG.md file. +enum _ChangelogUpdateOutcome { addedSection, updatedSection, failed } + +/// A state machine for the process of updating a CHANGELOG.md. +enum _ChangelogUpdateState { + /// Looking for the first version section. + findingFirstSection, + + /// Looking for the first list entry in an existing section. + findingFirstListItem, + + /// Finished with updates. + finishedUpdating, +} + +/// A command to update the changelog, and optionally version, of packages. +class UpdateReleaseInfoCommand extends PackageLoopingCommand { + /// Creates a publish metadata updater command instance. + UpdateReleaseInfoCommand( + Directory packagesDir, { + GitDir? gitDir, + }) : super(packagesDir, gitDir: gitDir) { + argParser.addOption(_changelogFlag, + mandatory: true, + help: 'The changelog entry to add. ' + 'Each line will be a separate list entry.'); + argParser.addOption(_versionTypeFlag, + mandatory: true, + help: 'The version change level', + allowed: [ + _versionNext, + _versionMinimal, + _versionBugfix, + _versionMinor, + ], + allowedHelp: { + _versionNext: + 'No version change; just adds a NEXT entry to the changelog.', + _versionBugfix: 'Increments the bugfix version.', + _versionMinor: 'Increments the minor version.', + _versionMinimal: 'Depending on the changes to each package: ' + 'increments the bugfix version (for publishable changes), ' + "uses NEXT (for changes that don't need to be published), " + 'or skips (if no changes).', + }); + } + + static const String _changelogFlag = 'changelog'; + static const String _versionTypeFlag = 'version'; + + static const String _versionNext = 'next'; + static const String _versionBugfix = 'bugfix'; + static const String _versionMinor = 'minor'; + static const String _versionMinimal = 'minimal'; + + // The version change type, if there is a set type for all platforms. + // + // If null, either there is no version change, or it is dynamic (`minimal`). + _VersionIncrementType? _versionChange; + + // The cache of changed files, for dynamic version change determination. + // + // Only set for `minimal` version change. + late final List _changedFiles; + + @override + final String name = 'update-release-info'; + + @override + final String description = 'Updates CHANGELOG.md files, and optionally the ' + 'version in pubspec.yaml, in a way that is consistent with version-check ' + 'enforcement.'; + + @override + bool get hasLongOutput => false; + + @override + Future initializeRun() async { + if (getStringArg(_changelogFlag).trim().isEmpty) { + throw UsageException('Changelog message must not be empty.', usage); + } + switch (getStringArg(_versionTypeFlag)) { + case _versionMinor: + _versionChange = _VersionIncrementType.minor; + break; + case _versionBugfix: + _versionChange = _VersionIncrementType.bugfix; + break; + case _versionMinimal: + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + _changedFiles = await gitVersionFinder.getChangedFiles(); + // Anothing other than a fixed change is null. + _versionChange = null; + break; + case _versionNext: + _versionChange = null; + break; + default: + throw UnimplementedError('Unimplemented version change type'); + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + String nextVersionString; + + _VersionIncrementType? versionChange = _versionChange; + + // If the change type is `minimal` determine what changes, if any, are + // needed. + if (versionChange == null && + getStringArg(_versionTypeFlag) == _versionMinimal) { + final Directory gitRoot = + packagesDir.fileSystem.directory((await gitDir).path); + final String relativePackagePath = + getRelativePosixPath(package.directory, from: gitRoot); + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: _changedFiles, + relativePackagePath: relativePackagePath); + + if (!state.hasChanges) { + return PackageResult.skip('No changes to package'); + } + if (state.needsVersionChange) { + versionChange = _VersionIncrementType.bugfix; + } + } + + if (versionChange != null) { + final Version? updatedVersion = + _updatePubspecVersion(package, versionChange); + if (updatedVersion == null) { + return PackageResult.fail( + ['Could not determine current version.']); + } + nextVersionString = updatedVersion.toString(); + print('${indentation}Incremented version to $nextVersionString.'); + } else { + nextVersionString = 'NEXT'; + } + + final _ChangelogUpdateOutcome updateOutcome = + _updateChangelog(package, nextVersionString); + switch (updateOutcome) { + case _ChangelogUpdateOutcome.addedSection: + print('${indentation}Added a $nextVersionString section.'); + break; + case _ChangelogUpdateOutcome.updatedSection: + print('${indentation}Updated NEXT section.'); + break; + case _ChangelogUpdateOutcome.failed: + return PackageResult.fail(['Could not update CHANGELOG.md.']); + } + + return PackageResult.success(); + } + + _ChangelogUpdateOutcome _updateChangelog( + RepositoryPackage package, String version) { + if (!package.changelogFile.existsSync()) { + printError('${indentation}Missing CHANGELOG.md.'); + return _ChangelogUpdateOutcome.failed; + } + + final String newHeader = '## $version'; + final RegExp listItemPattern = RegExp(r'^(\s*[-*])'); + + final StringBuffer newChangelog = StringBuffer(); + _ChangelogUpdateState state = _ChangelogUpdateState.findingFirstSection; + bool updatedExistingSection = false; + + for (final String line in package.changelogFile.readAsLinesSync()) { + switch (state) { + case _ChangelogUpdateState.findingFirstSection: + final String trimmedLine = line.trim(); + if (trimmedLine.isEmpty) { + // Discard any whitespace at the top of the file. + } else if (trimmedLine == '## NEXT') { + // Replace the header with the new version (which may also be NEXT). + newChangelog.writeln(newHeader); + // Find the existing list to add to. + state = _ChangelogUpdateState.findingFirstListItem; + } else { + // The first content in the file isn't a NEXT section, so just add + // the new section. + [ + newHeader, + '', + ..._changelogAdditionsAsList(), + '', + line, // Don't drop the current line. + ].forEach(newChangelog.writeln); + state = _ChangelogUpdateState.finishedUpdating; + } + break; + case _ChangelogUpdateState.findingFirstListItem: + final RegExpMatch? match = listItemPattern.firstMatch(line); + if (match != null) { + final String listMarker = match[1]!; + // Add the new items on top. If the new change is changing the + // version, then the new item should be more relevant to package + // clients than anything that was already there. If it's still + // NEXT, the order doesn't matter. + [ + ..._changelogAdditionsAsList(listMarker: listMarker), + line, // Don't drop the current line. + ].forEach(newChangelog.writeln); + state = _ChangelogUpdateState.finishedUpdating; + updatedExistingSection = true; + } else if (line.trim().isEmpty) { + // Scan past empty lines, but keep them. + newChangelog.writeln(line); + } else { + printError(' Existing NEXT section has unrecognized format.'); + return _ChangelogUpdateOutcome.failed; + } + break; + case _ChangelogUpdateState.finishedUpdating: + // Once changes are done, add the rest of the lines as-is. + newChangelog.writeln(line); + break; + } + } + + package.changelogFile.writeAsStringSync(newChangelog.toString()); + + return updatedExistingSection + ? _ChangelogUpdateOutcome.updatedSection + : _ChangelogUpdateOutcome.addedSection; + } + + /// Returns the changelog to add as a Markdown list, using the given list + /// bullet style (default to the repository standard of '*'), and adding + /// any missing periods. + /// + /// E.g., 'A line\nAnother line.' will become: + /// ``` + /// [ '* A line.', '* Another line.' ] + /// ``` + Iterable _changelogAdditionsAsList({String listMarker = '*'}) { + return getStringArg(_changelogFlag).split('\n').map((String entry) { + String standardizedEntry = entry.trim(); + if (!standardizedEntry.endsWith('.')) { + standardizedEntry = '$standardizedEntry.'; + } + return '$listMarker $standardizedEntry'; + }); + } + + /// Updates the version in [package]'s pubspec according to [type], returning + /// the new version, or null if there was an error updating the version. + Version? _updatePubspecVersion( + RepositoryPackage package, _VersionIncrementType type) { + final Pubspec pubspec = package.parsePubspec(); + final Version? currentVersion = pubspec.version; + if (currentVersion == null) { + printError('${indentation}No version in pubspec.yaml'); + return null; + } + + // For versions less than 1.0, shift the change down one component per + // Dart versioning conventions. + final _VersionIncrementType adjustedType = currentVersion.major > 0 + ? type + : _VersionIncrementType.values[type.index - 1]; + + final Version newVersion = _nextVersion(currentVersion, adjustedType); + + // Write the new version to the pubspec. + final YamlEditor editablePubspec = + YamlEditor(package.pubspecFile.readAsStringSync()); + editablePubspec.update(['version'], newVersion.toString()); + package.pubspecFile.writeAsStringSync(editablePubspec.toString()); + + return newVersion; + } + + Version _nextVersion(Version version, _VersionIncrementType type) { + switch (type) { + case _VersionIncrementType.minor: + return version.nextMinor; + case _VersionIncrementType.bugfix: + return version.nextPatch; + case _VersionIncrementType.build: + final int buildNumber = + version.build.isEmpty ? 0 : version.build.first as int; + return Version(version.major, version.minor, version.patch, + build: '${buildNumber + 1}'); + } + } +} diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index b816ee56999..62abdb2a432 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -13,6 +13,7 @@ import 'package:pub_semver/pub_semver.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; import 'common/package_looping_command.dart'; +import 'common/package_state_utils.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; import 'common/repository_package.dart'; @@ -531,44 +532,16 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. final Directory gitRoot = packagesDir.fileSystem.directory((await gitDir).path); final String relativePackagePath = - '${getRelativePosixPath(package.directory, from: gitRoot)}/'; - bool hasChanges = false; - bool needsVersionChange = false; - bool hasChangelogChange = false; - for (final String path in _changedFiles) { - // Only consider files within the package. - if (!path.startsWith(relativePackagePath)) { - continue; - } - hasChanges = true; - - final List components = p.posix.split(path); - final bool isChangelog = components.last == 'CHANGELOG.md'; - if (isChangelog) { - hasChangelogChange = true; - } + getRelativePosixPath(package.directory, from: gitRoot); - if (!needsVersionChange && - !isChangelog && - // The example's main.dart is shown on pub.dev, but for anything else - // in the example publishing has no purpose. - !(components.contains('example') && components.last != 'main.dart') && - // Changes to tests don't need to be published. - !components.contains('test') && - !components.contains('androidTest') && - !components.contains('RunnerTests') && - !components.contains('RunnerUITests') && - // Ignoring lints doesn't affect clients. - !components.contains('lint-baseline.xml')) { - needsVersionChange = true; - } - } + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: _changedFiles, relativePackagePath: relativePackagePath); - if (!hasChanges) { + if (!state.hasChanges) { return null; } - if (needsVersionChange) { + if (state.needsVersionChange) { if (_getChangeDescription().split('\n').any((String line) => line.startsWith(_missingVersionChangeJustificationMarker))) { logWarning('Ignoring lack of version change due to ' @@ -586,7 +559,7 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. } } - if (!hasChangelogChange) { + if (!state.hasChangelogChange) { if (_getChangeDescription().split('\n').any((String line) => line.startsWith(_missingChangelogChangeJustificationMarker))) { logWarning('Ignoring lack of CHANGELOG update due to ' diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 32bfc1b6228..138c1183fa1 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.5 +version: 0.8.6 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart new file mode 100644 index 00000000000..cc9116a9ea2 --- /dev/null +++ b/script/tool/test/common/package_state_utils_test.dart @@ -0,0 +1,140 @@ +// 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/package_state_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('checkPackageChangeState', () { + test('reports version change needed for code changes', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + const List changedFiles = [ + 'packages/a_package/lib/plugin.dart', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_package'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + + test('handles trailing slash on package path', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + const List changedFiles = [ + 'packages/a_package/lib/plugin.dart', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_package/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + expect(state.hasChangelogChange, false); + }); + + test('does not report version change exempt changes', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/android/lint-baseline.xml', + 'packages/a_plugin/example/android/src/androidTest/foo/bar/FooTest.java', + 'packages/a_plugin/example/ios/RunnerTests/Foo.m', + 'packages/a_plugin/example/ios/RunnerUITests/info.plist', + 'packages/a_plugin/tool/a_development_tool.dart', + 'packages/a_plugin/CHANGELOG.md', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, false); + expect(state.hasChangelogChange, true); + }); + + test('only considers a root "tool" folder to be special', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/lib/foo/tool/tool_thing.dart', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + + test('requires a version change for example main', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/lib/main.dart', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + + test('requires a version change for example readme.md', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/README.md', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + + test('requires a version change for example example.md', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/lib/example.md', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + }); +} diff --git a/script/tool/test/update_release_info_command_test.dart b/script/tool/test/update_release_info_command_test.dart new file mode 100644 index 00000000000..7e7ff54d594 --- /dev/null +++ b/script/tool/test/update_release_info_command_test.dart @@ -0,0 +1,645 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/update_release_info_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + late MockGitDir gitDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + + gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through a process runner, to make mock output + // consistent with other processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); + }); + + final UpdateReleaseInfoCommand command = UpdateReleaseInfoCommand( + packagesDir, + gitDir: gitDir, + ); + runner = CommandRunner( + 'update_release_info_command', 'Test for update_release_info_command'); + runner.addCommand(command); + }); + + group('flags', () { + test('fails if --changelog is missing', () async { + Exception? commandError; + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + ], exceptionHandler: (Exception e) { + commandError = e; + }); + + expect(commandError, isA()); + }); + + test('fails if --changelog is blank', () async { + Exception? commandError; + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + '', + ], exceptionHandler: (Exception e) { + commandError = e; + }); + + expect(commandError, isA()); + }); + + test('fails if --version is missing', () async { + Exception? commandError; + await runCapturingPrint( + runner, ['update-release-info', '--changelog', ''], + exceptionHandler: (Exception e) { + commandError = e; + }); + + expect(commandError, isA()); + }); + + test('fails if --version is an unknown value', () async { + Exception? commandError; + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=foo', + '--changelog', + '', + ], exceptionHandler: (Exception e) { + commandError = e; + }); + + expect(commandError, isA()); + }); + }); + + group('changelog', () { + test('adds new NEXT section', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## 1.0.0 + +* Previous changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## NEXT + +* A change. + +$originalChangelog'''; + + expect( + output, + containsAllInOrder([ + contains(' Added a NEXT section.'), + ]), + ); + expect(newChangelog, expectedChangeLog); + }); + + test('adds to existing NEXT section', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## NEXT + +* Already-pending changes. + +## 1.0.0 + +* Old changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## NEXT + +* A change. +* Already-pending changes. + +## 1.0.0 + +* Old changes. +'''; + + expect(output, + containsAllInOrder([contains(' Updated NEXT section.')])); + expect(newChangelog, expectedChangeLog); + }); + + test('adds new version section', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## 1.0.0 + +* Previous changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## 1.0.1 + +* A change. + +$originalChangelog'''; + + expect( + output, + containsAllInOrder([ + contains(' Added a 1.0.1 section.'), + ]), + ); + expect(newChangelog, expectedChangeLog); + }); + + test('converts existing NEXT section to version section', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## NEXT + +* Already-pending changes. + +## 1.0.0 + +* Old changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## 1.0.1 + +* A change. +* Already-pending changes. + +## 1.0.0 + +* Old changes. +'''; + + expect(output, + containsAllInOrder([contains(' Updated NEXT section.')])); + expect(newChangelog, expectedChangeLog); + }); + + test('treats multiple lines as multiple list items', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## 1.0.0 + +* Previous changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'First change.\nSecond change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## 1.0.1 + +* First change. +* Second change. + +$originalChangelog'''; + + expect(newChangelog, expectedChangeLog); + }); + + test('adds a period to any lines missing it, and removes whitespace', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## 1.0.0 + +* Previous changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'First change \nSecond change' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## 1.0.1 + +* First change. +* Second change. + +$originalChangelog'''; + + expect(newChangelog, expectedChangeLog); + }); + + test('handles non-standard changelog format', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +# 1.0.0 + +* A version with the wrong heading format. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## NEXT + +* A change. + +$originalChangelog'''; + + expect(output, + containsAllInOrder([contains(' Added a NEXT section.')])); + expect(newChangelog, expectedChangeLog); + }); + + test('adds to existing NEXT section using - list style', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## NEXT + + - Already-pending changes. + +## 1.0.0 + + - Previous changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## NEXT + + - A change. + - Already-pending changes. + +## 1.0.0 + + - Previous changes. +'''; + + expect(output, + containsAllInOrder([contains(' Updated NEXT section.')])); + expect(newChangelog, expectedChangeLog); + }); + + test('skips for "minimal" when there are no changes at all', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/different_package/test/plugin_test.dart +'''), + ]; + final String originalChangelog = package.changelogFile.readAsStringSync(); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minimal', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.1'); + expect(package.changelogFile.readAsStringSync(), originalChangelog); + expect( + output, + containsAllInOrder([ + contains('No changes to package'), + contains('Skipped 1 package') + ])); + }); + + test('fails if CHANGELOG.md is missing', () async { + createFakePackage('a_package', packagesDir, includeCommonFiles: false); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output, + containsAllInOrder([contains(' Missing CHANGELOG.md.')])); + }); + + test('fails if CHANGELOG.md has unexpected NEXT block format', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## NEXT + +Some free-form text that isn't a list. + +## 1.0.0 + +- Previous changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains(' Existing NEXT section has unrecognized format.') + ])); + }); + }); + + group('pubspec', () { + test('does not change for --next', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.0'); + }); + + test('updates bugfix version for pre-1.0 without existing build number', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '0.1.0'); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '0.1.0+1'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 0.1.0+1')])); + }); + + test('updates bugfix version for pre-1.0 with existing build number', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '0.1.0+2'); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '0.1.0+3'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 0.1.0+3')])); + }); + + test('updates bugfix version for post-1.0', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.2'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 1.0.2')])); + }); + + test('updates minor version for pre-1.0', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '0.1.0+2'); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '0.1.1'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 0.1.1')])); + }); + + test('updates minor version for post-1.0', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.1.0'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 1.1.0')])); + }); + + test('updates bugfix version for "minimal" with publish-worthy changes', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/a_package/lib/plugin.dart +'''), + ]; + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minimal', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.2'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 1.0.2')])); + }); + + test('no version change for "minimal" with non-publish-worthy changes', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/a_package/test/plugin_test.dart +'''), + ]; + + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minimal', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.1'); + }); + + test('fails if there is no version in pubspec', () async { + createFakePackage('a_package', packagesDir, version: null); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [contains('Could not determine current version.')])); + }); + }); +} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 7255bf9a927..4c99873e937 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -310,14 +310,15 @@ String _pluginPlatformSection( return entry; } -typedef ErrorHandler = void Function(Error error); - /// Run the command [runner] with the given [args] and return /// what was printed. /// A custom [errorHandler] can be used to handle the runner error as desired without throwing. Future> runCapturingPrint( - CommandRunner runner, List args, - {ErrorHandler? errorHandler}) async { + CommandRunner runner, + List args, { + Function(Error error)? errorHandler, + Function(Exception error)? exceptionHandler, +}) async { final List prints = []; final ZoneSpecification spec = ZoneSpecification( print: (_, __, ___, String message) { @@ -333,6 +334,11 @@ Future> runCapturingPrint( rethrow; } errorHandler(e); + } on Exception catch (e) { + if (exceptionHandler == null) { + rethrow; + } + exceptionHandler(e); } return prints; From 7877a8beb0929c419f9e3be7984f247f47f1a726 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 23 May 2022 14:08:08 -0400 Subject: [PATCH 192/249] [google_maps_flutter] Updates platform interface to new analysis options (#5793) --- script/tool/CHANGELOG.md | 4 ++++ script/tool/lib/src/analyze_command.dart | 8 +++++--- script/tool/test/analyze_command_test.dart | 12 ++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 4b40aecefa9..adc7bfcd29a 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +- Supports empty custom analysis allow list files. + ## 0.8.6 - Adds `update-release-info` to apply changelog and optional version changes diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 1cd85af076f..8778b3de9d8 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -87,9 +87,11 @@ class AnalyzeCommand extends PackageLoopingCommand { getStringListArg(_customAnalysisFlag).expand((String item) { if (item.endsWith('.yaml')) { final File file = packagesDir.fileSystem.file(item); - return (loadYaml(file.readAsStringSync()) as YamlList) - .toList() - .cast(); + final Object? yaml = loadYaml(file.readAsStringSync()); + if (yaml == null) { + return []; + } + return (yaml as YamlList).toList().cast(); } return [item]; }).toSet(); diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index a9b83349306..a4a47a22118 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -213,6 +213,18 @@ void main() { ])); }); + test('allows an empty config file', () async { + createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); + final File allowFile = packagesDir.childFile('custom.yaml'); + allowFile.createSync(); + + await expectLater( + () => runCapturingPrint( + runner, ['analyze', '--custom-analysis', allowFile.path]), + throwsA(isA())); + }); + // See: https://github.com/flutter/flutter/issues/78994 test('takes an empty allow list', () async { createFakePlugin('foo', packagesDir, From 66824fd7e6da836014367fd4e75d7b1b22f11130 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 6 Jun 2022 15:23:10 -0400 Subject: [PATCH 193/249] [various] Clean up obsolete references to "master" (#5912) --- script/tool/README.md | 2 +- .../tool/lib/src/pubspec_check_command.dart | 5 +++ .../tool/test/pubspec_check_command_test.dart | 32 +++++++++++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/script/tool/README.md b/script/tool/README.md index cc9621f4408..d3beb53a110 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -178,4 +178,4 @@ For changes that are relevant to flutter/packages, you will also need to: - Update the tool's pubspec.yaml and CHANGELOG - Publish the tool - Update the pinned version in - [flutter/packages](https://github.com/flutter/packages/blob/master/.cirrus.yml) + [flutter/packages](https://github.com/flutter/packages/blob/main/.cirrus.yml) diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 3598a39da96..79ef1e1d3e5 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -191,6 +191,11 @@ class PubspecCheckCommand extends PackageLoopingCommand { errorMessages .add('The "repository" link should end with the package path.'); } + + if (pubspec.repository!.path.contains('/master/')) { + errorMessages + .add('The "repository" link should use "main", not "master".'); + } } if (pubspec.homepage != null) { diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index fbe31c72bc2..2c254ca9498 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -27,6 +27,7 @@ String _headerSection( String name, { bool isPlugin = false, bool includeRepository = true, + String repositoryBranch = 'main', String? repositoryPackagesDirRelativePath, bool includeHomepage = false, bool includeIssueTracker = true, @@ -38,7 +39,7 @@ String _headerSection( 'flutter', if (isPlugin) 'plugins' else 'packages', 'tree', - 'main', + repositoryBranch, 'packages', repositoryPath, ]; @@ -328,7 +329,7 @@ ${_devDependenciesSection()} ); }); - test('fails when repository is incorrect', () async { + test('fails when repository package name is incorrect', () async { final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); @@ -355,6 +356,33 @@ ${_devDependenciesSection()} ); }); + test('fails when repository uses master instead of main', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true, repositoryBranch: 'master')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The "repository" link should use "main", not "master".'), + ]), + ); + }); + test('fails when issue tracker is missing', () async { final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, examples: []); From 79eb10169a0948af0c2c53c1aeafad9018ac8d67 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 8 Jun 2022 17:53:09 -0400 Subject: [PATCH 194/249] [tools] Check integration tests for `test` (#5936) --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/drive_examples_command.dart | 30 +++++++++++++- .../test/drive_examples_command_test.dart | 41 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index adc7bfcd29a..36d8d23eb75 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,8 @@ ## NEXT - Supports empty custom analysis allow list files. +- `drive-examples` now validates files to ensure that they don't accidentally + use `test(...)`. ## 0.8.6 diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 15366e17ae8..45e20c0f13c 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -182,7 +182,16 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (legacyTestFile != null) { testTargets.add(legacyTestFile); } else { - (await _getIntegrationTests(example)).forEach(testTargets.add); + for (final File testFile in await _getIntegrationTests(example)) { + // Check files for known problematic patterns. + final bool passesValidation = _validateIntegrationTest(testFile); + if (!passesValidation) { + // Report the issue, but continue with the test as the validation + // errors don't prevent running. + errors.add('${testFile.basename} failed validation'); + } + testTargets.add(testFile); + } } if (testTargets.isEmpty) { @@ -310,6 +319,25 @@ class DriveExamplesCommand extends PackageLoopingCommand { return tests; } + /// Checks [testFile] for known bad patterns in integration tests, logging + /// any issues. + /// + /// Returns true if the file passes validation without issues. + bool _validateIntegrationTest(File testFile) { + final List lines = testFile.readAsLinesSync(); + + final RegExp badTestPattern = RegExp(r'\s*test\('); + if (lines.any((String line) => line.startsWith(badTestPattern))) { + final String filename = testFile.basename; + printError( + '$filename uses "test", which will not report failures correctly. ' + 'Use testWidgets instead.'); + return false; + } + + return true; + } + /// For each file in [targets], uses /// `flutter drive --driver [driver] --target ` /// to drive [example], returning a list of any failing test targets. diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 23318f7cd60..0b6082098ae 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -307,6 +307,47 @@ void main() { ); }); + test('integration tests using test(...) fail validation', () async { + setMockFlutterDevicesOutput(); + final RepositoryPackage package = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/integration_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/android.java', + ], + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + }, + ); + package.directory + .childDirectory('example') + .childDirectory('integration_test') + .childFile('foo_test.dart') + .writeAsStringSync(''' + test('this is the wrong kind of test!'), () { + ... + } +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('foo_test.dart failed validation'), + ]), + ); + }); + test( 'driving under folder "test_driver" when targets are under "integration_test"', () async { From 840feda14f79d801c19e6d3743b212fb14761d91 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 22 Jun 2022 14:29:12 -0400 Subject: [PATCH 195/249] [ci] Ensure complete dependabot coverage (#5976) --- script/tool/CHANGELOG.md | 1 + .../lib/src/dependabot_check_command.dart | 114 ++++++++++++++ script/tool/lib/src/main.dart | 2 + .../test/dependabot_check_command_test.dart | 141 ++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 script/tool/lib/src/dependabot_check_command.dart create mode 100644 script/tool/test/dependabot_check_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 36d8d23eb75..07cafee7f8b 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -3,6 +3,7 @@ - Supports empty custom analysis allow list files. - `drive-examples` now validates files to ensure that they don't accidentally use `test(...)`. +- Adds a new `dependabot-check` command to ensure complete Dependabot coverage. ## 0.8.6 diff --git a/script/tool/lib/src/dependabot_check_command.dart b/script/tool/lib/src/dependabot_check_command.dart new file mode 100644 index 00000000000..5aa762e916e --- /dev/null +++ b/script/tool/lib/src/dependabot_check_command.dart @@ -0,0 +1,114 @@ +// 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 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:yaml/yaml.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/repository_package.dart'; + +/// A command to verify Dependabot configuration coverage of packages. +class DependabotCheckCommand extends PackageLoopingCommand { + /// Creates Dependabot check command instance. + DependabotCheckCommand(Directory packagesDir, {GitDir? gitDir}) + : super(packagesDir, gitDir: gitDir) { + argParser.addOption(_configPathFlag, + help: 'Path to the Dependabot configuration file', + defaultsTo: '.github/dependabot.yml'); + } + + static const String _configPathFlag = 'config'; + + late Directory _repoRoot; + + // The set of directories covered by "gradle" entries in the config. + Set _gradleDirs = const {}; + + @override + final String name = 'dependabot-check'; + + @override + final String description = + 'Checks that all packages have Dependabot coverage.'; + + @override + final PackageLoopingType packageLoopingType = + PackageLoopingType.includeAllSubpackages; + + @override + final bool hasLongOutput = false; + + @override + Future initializeRun() async { + _repoRoot = packagesDir.fileSystem.directory((await gitDir).path); + + final YamlMap config = loadYaml(_repoRoot + .childFile(getStringArg(_configPathFlag)) + .readAsStringSync()) as YamlMap; + final dynamic entries = config['updates']; + if (entries is! YamlList) { + return; + } + + const String typeKey = 'package-ecosystem'; + const String dirKey = 'directory'; + _gradleDirs = entries + .where((dynamic entry) => entry[typeKey] == 'gradle') + .map((dynamic entry) => (entry as YamlMap)[dirKey] as String) + .toSet(); + } + + @override + Future runForPackage(RepositoryPackage package) async { + bool skipped = true; + final List errors = []; + + final RunState gradleState = _validateDependabotGradleCoverage(package); + skipped = skipped && gradleState == RunState.skipped; + if (gradleState == RunState.failed) { + printError('${indentation}Missing Gradle coverage.'); + errors.add('Missing Gradle coverage'); + } + + // TODO(stuartmorgan): Add other ecosystem checks here as more are enabled. + + if (skipped) { + return PackageResult.skip('No supported package ecosystems'); + } + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); + } + + /// Returns the state for the Dependabot coverage of the Gradle ecosystem for + /// [package]: + /// - succeeded if it includes gradle and is covered. + /// - failed if it includes gradle and is not covered. + /// - skipped if it doesn't include gradle. + RunState _validateDependabotGradleCoverage(RepositoryPackage package) { + final Directory androidDir = + package.platformDirectory(FlutterPlatform.android); + final Directory appDir = androidDir.childDirectory('app'); + if (appDir.existsSync()) { + // It's an app, so only check for the app directory to be covered. + final String dependabotPath = + '/${getRelativePosixPath(appDir, from: _repoRoot)}'; + return _gradleDirs.contains(dependabotPath) + ? RunState.succeeded + : RunState.failed; + } else if (androidDir.existsSync()) { + // It's a library, so only check for the android directory to be covered. + final String dependabotPath = + '/${getRelativePosixPath(androidDir, from: _repoRoot)}'; + return _gradleDirs.contains(dependabotPath) + ? RunState.succeeded + : RunState.failed; + } + return RunState.skipped; + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 739aef56878..966e7b6be56 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -7,6 +7,7 @@ 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/dependabot_check_command.dart'; import 'analyze_command.dart'; import 'build_examples_command.dart'; @@ -55,6 +56,7 @@ void main(List args) { ..addCommand(BuildExamplesCommand(packagesDir)) ..addCommand(CreateAllPluginsAppCommand(packagesDir)) ..addCommand(CustomTestCommand(packagesDir)) + ..addCommand(DependabotCheckCommand(packagesDir)) ..addCommand(DriveExamplesCommand(packagesDir)) ..addCommand(FederationSafetyCheckCommand(packagesDir)) ..addCommand(FirebaseTestLabCommand(packagesDir)) diff --git a/script/tool/test/dependabot_check_command_test.dart b/script/tool/test/dependabot_check_command_test.dart new file mode 100644 index 00000000000..a4c8693b2c2 --- /dev/null +++ b/script/tool/test/dependabot_check_command_test.dart @@ -0,0 +1,141 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/dependabot_check_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'util.dart'; + +void main() { + late CommandRunner runner; + late FileSystem fileSystem; + late Directory root; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + root = fileSystem.currentDirectory; + packagesDir = root.childDirectory('packages'); + + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(root.path); + + final DependabotCheckCommand command = DependabotCheckCommand( + packagesDir, + gitDir: gitDir, + ); + runner = CommandRunner( + 'dependabot_test', 'Test for $DependabotCheckCommand'); + runner.addCommand(command); + }); + + void _setDependabotCoverage({ + Iterable gradleDirs = const [], + }) { + final Iterable gradleEntries = + gradleDirs.map((String directory) => ''' + - package-ecosystem: "gradle" + directory: "/$directory" + schedule: + interval: "daily" +'''); + final File configFile = + root.childDirectory('.github').childFile('dependabot.yml'); + configFile.createSync(recursive: true); + configFile.writeAsStringSync(''' +version: 2 +updates: +${gradleEntries.join('\n')} +'''); + } + + test('skips with no supported ecosystems', () async { + _setDependabotCoverage(); + createFakePackage('a_package', packagesDir); + + final List output = + await runCapturingPrint(runner, ['dependabot-check']); + + expect( + output, + containsAllInOrder([ + contains('SKIPPING: No supported package ecosystems'), + ])); + }); + + test('fails for app missing Gradle coverage', () async { + _setDependabotCoverage(); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + package.directory + .childDirectory('example') + .childDirectory('android') + .childDirectory('app') + .createSync(recursive: true); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['dependabot-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing Gradle coverage.'), + contains('a_package/example:\n' + ' Missing Gradle coverage') + ])); + }); + + test('fails for plugin missing Gradle coverage', () async { + _setDependabotCoverage(); + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir); + plugin.directory.childDirectory('android').createSync(recursive: true); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['dependabot-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing Gradle coverage.'), + contains('a_plugin:\n' + ' Missing Gradle coverage') + ])); + }); + + test('passes for correct Gradle coverage', () async { + _setDependabotCoverage(gradleDirs: [ + 'packages/a_plugin/android', + 'packages/a_plugin/example/android/app', + ]); + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir); + // Test the plugin. + plugin.directory.childDirectory('android').createSync(recursive: true); + // And its example app. + plugin.directory + .childDirectory('example') + .childDirectory('android') + .childDirectory('app') + .createSync(recursive: true); + + final List output = + await runCapturingPrint(runner, ['dependabot-check']); + + expect(output, + containsAllInOrder([contains('Ran for 2 package(s)')])); + }); +} From b232558eb8abaff535e76dc760ce401ecd436001 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 30 Jun 2022 19:34:08 -0400 Subject: [PATCH 196/249] [tools] Allow skipping packages by Dart version (#6038) --- script/tool/CHANGELOG.md | 5 ++- .../src/common/package_looping_command.dart | 23 ++++++++++- script/tool/pubspec.yaml | 2 +- .../common/package_looping_command_test.dart | 33 ++++++++++++++- script/tool/test/util.dart | 41 ++++++++++++------- 5 files changed, 86 insertions(+), 18 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 07cafee7f8b..9606eed20b2 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,9 +1,12 @@ -## NEXT +## 0.8.7 - Supports empty custom analysis allow list files. - `drive-examples` now validates files to ensure that they don't accidentally use `test(...)`. - Adds a new `dependabot-check` command to ensure complete Dependabot coverage. +- Adds `skip-if-not-supporting-dart-version` to allow for the same use cases + as `skip-if-not-supporting-flutter-version` but for packages without Flutter + constraints. ## 0.8.6 diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index a295215f362..1a194bd45b9 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -96,10 +96,17 @@ abstract class PackageLoopingCommand extends PluginCommand { help: 'Skip any packages that require a Flutter version newer than ' 'the provided version.', ); + argParser.addOption( + _skipByDartVersionArg, + help: 'Skip any packages that require a Dart version newer than ' + 'the provided version.', + ); } static const String _skipByFlutterVersionArg = 'skip-if-not-supporting-flutter-version'; + static const String _skipByDartVersionArg = + 'skip-if-not-supporting-dart-version'; /// Packages that had at least one [logWarning] call. final Set _packagesWithWarnings = @@ -264,6 +271,9 @@ abstract class PackageLoopingCommand extends PluginCommand { final Version? minFlutterVersion = minFlutterVersionArg.isEmpty ? null : Version.parse(minFlutterVersionArg); + final String minDartVersionArg = getStringArg(_skipByDartVersionArg); + final Version? minDartVersion = + minDartVersionArg.isEmpty ? null : Version.parse(minDartVersionArg); final DateTime runStart = DateTime.now(); @@ -289,7 +299,8 @@ abstract class PackageLoopingCommand extends PluginCommand { PackageResult result; try { result = await _runForPackageIfSupported(entry.package, - minFlutterVersion: minFlutterVersion); + minFlutterVersion: minFlutterVersion, + minDartVersion: minDartVersion); } catch (e, stack) { printError(e.toString()); printError(stack.toString()); @@ -337,6 +348,7 @@ abstract class PackageLoopingCommand extends PluginCommand { Future _runForPackageIfSupported( RepositoryPackage package, { Version? minFlutterVersion, + Version? minDartVersion, }) async { if (minFlutterVersion != null) { final Pubspec pubspec = package.parsePubspec(); @@ -349,6 +361,15 @@ abstract class PackageLoopingCommand extends PluginCommand { } } + if (minDartVersion != null) { + final Pubspec pubspec = package.parsePubspec(); + final VersionConstraint? dartConstraint = pubspec.environment?['sdk']; + if (dartConstraint != null && !dartConstraint.allows(minDartVersion)) { + return PackageResult.skip( + 'Does not support Dart ${minDartVersion.toString()}'); + } + } + return await runForPackage(package); } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 138c1183fa1..1e631b1c0a1 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.6 +version: 0.8.7 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index a7e7dfdf6eb..ec2b9b9be23 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -340,7 +340,7 @@ void main() { } }); - test('skips unsupported versions when requested', () async { + test('skips unsupported Flutter versions when requested', () async { final RepositoryPackage excluded = createFakePlugin( 'a_plugin', packagesDir, flutterConstraint: '>=2.10.0'); @@ -370,6 +370,37 @@ void main() { '$_startSkipColor SKIPPING: Does not support Flutter 2.5.0$_endColor', ])); }); + + test('skips unsupported Dart versions when requested', () async { + final RepositoryPackage excluded = createFakePackage( + 'excluded_package', packagesDir, + isFlutter: false, dartConstraint: '>=2.17.0 <3.0.0'); + final RepositoryPackage included = createFakePackage( + 'a_package', packagesDir, + isFlutter: false, dartConstraint: '>=2.14.0 <3.0.0'); + + final TestPackageLoopingCommand command = createTestCommand( + packageLoopingType: PackageLoopingType.includeAllSubpackages, + hasLongOutput: false); + final List output = await runCommand(command, + arguments: ['--skip-if-not-supporting-dart-version=2.14.0']); + + expect( + command.checkedPackages, + unorderedEquals([ + included.path, + getExampleDir(included).path, + ])); + expect(command.checkedPackages, isNot(contains(excluded.path))); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for a_package...$_endColor', + '${_startHeadingColor}Running for excluded_package...$_endColor', + '$_startSkipColor SKIPPING: Does not support Dart 2.14.0$_endColor', + ])); + }); }); group('output', () { diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 4c99873e937..041d93367f9 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -23,6 +23,9 @@ import 'mocks.dart'; export 'package:flutter_plugin_tools/src/common/repository_package.dart'; +const String _defaultDartConstraint = '>=2.14.0 <3.0.0'; +const String _defaultFlutterConstraint = '>=2.5.0'; + /// Returns the exe name that command will use when running Flutter on /// [platform]. String getFlutterCommand(Platform platform) => @@ -97,14 +100,19 @@ RepositoryPackage createFakePlugin( Map platformSupport = const {}, String? version = '0.0.1', - String flutterConstraint = '>=2.5.0', + String flutterConstraint = _defaultFlutterConstraint, + String dartConstraint = _defaultDartConstraint, }) { - final RepositoryPackage package = createFakePackage(name, parentDirectory, - isFlutter: true, - examples: examples, - extraFiles: extraFiles, - version: version, - flutterConstraint: flutterConstraint); + final RepositoryPackage package = createFakePackage( + name, + parentDirectory, + isFlutter: true, + examples: examples, + extraFiles: extraFiles, + version: version, + flutterConstraint: flutterConstraint, + dartConstraint: dartConstraint, + ); createFakePubspec( package, @@ -114,6 +122,7 @@ RepositoryPackage createFakePlugin( platformSupport: platformSupport, version: version, flutterConstraint: flutterConstraint, + dartConstraint: dartConstraint, ); return package; @@ -136,7 +145,8 @@ RepositoryPackage createFakePackage( List extraFiles = const [], bool isFlutter = false, String? version = '0.0.1', - String flutterConstraint = '>=2.5.0', + String flutterConstraint = _defaultFlutterConstraint, + String dartConstraint = _defaultDartConstraint, bool includeCommonFiles = true, String? directoryName, String? publishTo, @@ -150,7 +160,8 @@ RepositoryPackage createFakePackage( name: name, isFlutter: isFlutter, version: version, - flutterConstraint: flutterConstraint); + flutterConstraint: flutterConstraint, + dartConstraint: dartConstraint); if (includeCommonFiles) { package.changelogFile.writeAsStringSync(''' ## $version @@ -167,7 +178,8 @@ RepositoryPackage createFakePackage( includeCommonFiles: false, isFlutter: isFlutter, publishTo: 'none', - flutterConstraint: flutterConstraint); + flutterConstraint: flutterConstraint, + dartConstraint: dartConstraint); } else if (examples.isNotEmpty) { final Directory examplesDirectory = getExampleDir(package)..createSync(); for (final String exampleName in examples) { @@ -176,7 +188,8 @@ RepositoryPackage createFakePackage( includeCommonFiles: false, isFlutter: isFlutter, publishTo: 'none', - flutterConstraint: flutterConstraint); + flutterConstraint: flutterConstraint, + dartConstraint: dartConstraint); } } @@ -189,7 +202,7 @@ RepositoryPackage createFakePackage( return package; } -/// Creates a `pubspec.yaml` file with a flutter dependency. +/// Creates a `pubspec.yaml` file for [package]. /// /// [platformSupport] is a map of platform string to the support details for /// that platform. If empty, no `plugin` entry will be created unless `isPlugin` @@ -203,8 +216,8 @@ void createFakePubspec( const {}, String? publishTo, String? version, - String dartConstraint = '>=2.0.0 <3.0.0', - String flutterConstraint = '>=2.5.0', + String dartConstraint = _defaultDartConstraint, + String flutterConstraint = _defaultFlutterConstraint, }) { isPlugin |= platformSupport.isNotEmpty; From b74ce39eb1a8bbcca81e8564cbb88d281add3d9d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 1 Jul 2022 14:54:04 -0400 Subject: [PATCH 197/249] [tools] Allow pre-release versions (#6061) --- script/tool/CHANGELOG.md | 4 + .../tool/lib/src/version_check_command.dart | 51 +++-- script/tool/pubspec.yaml | 2 +- .../tool/test/version_check_command_test.dart | 180 ++++++++++++++++++ 4 files changed, 224 insertions(+), 13 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 9606eed20b2..da14eae285e 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.8 + +- Allows pre-release versions in `version-check`. + ## 0.8.7 - Supports empty custom analysis allow list files. diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 62abdb2a432..246382dfae0 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -31,8 +31,8 @@ enum NextVersionType { /// A bugfix change. PATCH, - /// The release of an existing prerelease version. - RELEASE, + /// The release of an existing pre-1.0 version. + V1_RELEASE, } /// The state of a package's version relative to the comparison base. @@ -53,8 +53,8 @@ enum _CurrentVersionState { unknown, } -/// Returns the set of allowed next versions, with their change type, for -/// [version]. +/// Returns the set of allowed next non-prerelease versions, with their change +/// type, for [version]. /// /// [newVersion] is used to check whether this is a pre-1.0 version bump, as /// those have different semver rules. @@ -78,17 +78,17 @@ Map getAllowedNextVersions( final int currentBuildNumber = version.build.first as int; nextBuildNumber = currentBuildNumber + 1; } - final Version preReleaseVersion = Version( + final Version nextBuildVersion = Version( version.major, version.minor, version.patch, build: nextBuildNumber.toString(), ); allowedNextVersions.clear(); - allowedNextVersions[version.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[version.nextMajor] = NextVersionType.V1_RELEASE; allowedNextVersions[version.nextMinor] = NextVersionType.BREAKING_MAJOR; allowedNextVersions[version.nextPatch] = NextVersionType.MINOR; - allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; + allowedNextVersions[nextBuildVersion] = NextVersionType.PATCH; } return allowedNextVersions; } @@ -337,12 +337,11 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // Check for reverts when doing local validation. if (!getBoolArg(_againstPubFlag) && currentVersion < previousVersion) { - final Map possibleVersionsFromNewVersion = - getAllowedNextVersions(currentVersion, newVersion: previousVersion); // Since this skips validation, try to ensure that it really is likely // to be a revert rather than a typo by checking that the transition // from the lower version to the new version would have been valid. - if (possibleVersionsFromNewVersion.containsKey(previousVersion)) { + if (_shouldAllowVersionChange( + oldVersion: currentVersion, newVersion: previousVersion)) { logWarning('${indentation}New version is lower than previous version. ' 'This is assumed to be a revert.'); return _CurrentVersionState.validRevert; @@ -352,7 +351,8 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final Map allowedNextVersions = getAllowedNextVersions(previousVersion, newVersion: currentVersion); - if (allowedNextVersions.containsKey(currentVersion)) { + if (_shouldAllowVersionChange( + oldVersion: previousVersion, newVersion: currentVersion)) { print('$indentation$previousVersion -> $currentVersion'); } else { printError('${indentation}Incorrectly updated version.\n' @@ -361,7 +361,13 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} return _CurrentVersionState.invalidChange; } - if (allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR && + // Check whether the version (or for a pre-release, the version that + // pre-release would eventually be released as) is a breaking change, and + // if so, validate it. + final Version targetReleaseVersion = + currentVersion.isPreRelease ? currentVersion.nextPatch : currentVersion; + if (allowedNextVersions[targetReleaseVersion] == + NextVersionType.BREAKING_MAJOR && !_validateBreakingChange(package)) { printError('${indentation}Breaking change detected.\n' '${indentation}Breaking changes to platform interfaces are not ' @@ -520,6 +526,27 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return file.readAsStringSync(); } + /// Returns true if the given version transition should be allowed. + bool _shouldAllowVersionChange( + {required Version oldVersion, required Version newVersion}) { + // Get the non-pre-release next version mapping. + final Map allowedNextVersions = + getAllowedNextVersions(oldVersion, newVersion: newVersion); + + if (allowedNextVersions.containsKey(newVersion)) { + return true; + } + // Allow a pre-release version of a version that would be a valid + // transition. + if (newVersion.isPreRelease) { + final Version targetReleaseVersion = newVersion.nextPatch; + if (allowedNextVersions.containsKey(targetReleaseVersion)) { + return true; + } + } + return false; + } + /// Returns an error string if the changes to this package should have /// resulted in a version change, or shoud have resulted in a CHANGELOG change /// but didn't. diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 1e631b1c0a1..b8233de11b4 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.7 +version: 0.8.8 dependencies: args: ^2.1.0 diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index a310f0f09fc..8f8d510fd10 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -1178,6 +1178,186 @@ ${indentation}HTTP response: null ]), ); }); + + group('prelease versions', () { + test( + 'allow an otherwise-valid transition that also adds a pre-release component', + () async { + createFakePlugin('plugin', packagesDir, version: '2.0.0-dev'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('1.0.0 -> 2.0.0-dev'), + ]), + ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall('git-show', + ['main:packages/plugin/pubspec.yaml'], null) + ])); + }); + + test('allow releasing a pre-release', () async { + createFakePlugin('plugin', packagesDir, version: '1.2.0'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.2.0-dev'), + ]; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('1.2.0-dev -> 1.2.0'), + ]), + ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall('git-show', + ['main:packages/plugin/pubspec.yaml'], null) + ])); + }); + + // Allow abandoning a pre-release version in favor of a different version + // change type. + test( + 'allow an otherwise-valid transition that also removes a pre-release component', + () async { + createFakePlugin('plugin', packagesDir, version: '2.0.0'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.2.0-dev'), + ]; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('1.2.0-dev -> 2.0.0'), + ]), + ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall('git-show', + ['main:packages/plugin/pubspec.yaml'], null) + ])); + }); + + test('allow changing only the pre-release version', () async { + createFakePlugin('plugin', packagesDir, version: '1.2.0-dev.2'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.2.0-dev.1'), + ]; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('1.2.0-dev.1 -> 1.2.0-dev.2'), + ]), + ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall('git-show', + ['main:packages/plugin/pubspec.yaml'], null) + ])); + }); + + test('denies invalid version change that also adds a pre-release', + () async { + createFakePlugin('plugin', packagesDir, version: '0.2.0-dev'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 0.0.1'), + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Incorrectly updated version.'), + ])); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall('git-show', + ['main:packages/plugin/pubspec.yaml'], null) + ])); + }); + + test('denies invalid version change that also removes a pre-release', + () async { + createFakePlugin('plugin', packagesDir, version: '0.2.0'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 0.0.1-dev'), + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Incorrectly updated version.'), + ])); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall('git-show', + ['main:packages/plugin/pubspec.yaml'], null) + ])); + }); + + test('denies invalid version change between pre-releases', () async { + createFakePlugin('plugin', packagesDir, version: '0.2.0-dev'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 0.0.1-dev'), + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Incorrectly updated version.'), + ])); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall('git-show', + ['main:packages/plugin/pubspec.yaml'], null) + ])); + }); + }); }); group('Pre 1.0', () { From 3158ec8daa93ba65dc8ec3c605c1aa18dc768edd Mon Sep 17 00:00:00 2001 From: Ricardo Amador <32242716+ricardoamador@users.noreply.github.com> Date: Fri, 1 Jul 2022 14:57:05 -0700 Subject: [PATCH 198/249] testing autosubmit (#6062) From 223a080ad65953f9c171377f47495c69c6f469ca Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 20 Jul 2022 16:15:04 -0400 Subject: [PATCH 199/249] [tool] Bypass version/changelog checks for some PRs (#6124) --- script/tool/CHANGELOG.md | 5 + .../tool/lib/src/version_check_command.dart | 54 ++++- .../tool/test/version_check_command_test.dart | 213 ++++++++++++++++++ 3 files changed, 270 insertions(+), 2 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index da14eae285e..a84e9af6390 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +- Bypasses version and CHANGELOG checks for Dependabot PRs for packages + that are known not to be client-affecting. + ## 0.8.8 - Allows pre-release versions in `version-check`. diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 246382dfae0..e914d2f080e 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -569,11 +569,15 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. } if (state.needsVersionChange) { - if (_getChangeDescription().split('\n').any((String line) => + final String changeDescription = _getChangeDescription(); + if (changeDescription.split('\n').any((String line) => line.startsWith(_missingVersionChangeJustificationMarker))) { logWarning('Ignoring lack of version change due to ' '"$_missingVersionChangeJustificationMarker" in the ' 'change description.'); + } else if (_isAllowedDependabotChange(package, changeDescription)) { + logWarning('Ignoring lack of version change for Dependabot change to ' + 'a known internal dependency.'); } else { printError( 'No version change found, but the change to this package could ' @@ -587,11 +591,15 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. } if (!state.hasChangelogChange) { - if (_getChangeDescription().split('\n').any((String line) => + final String changeDescription = _getChangeDescription(); + if (changeDescription.split('\n').any((String line) => line.startsWith(_missingChangelogChangeJustificationMarker))) { logWarning('Ignoring lack of CHANGELOG update due to ' '"$_missingChangelogChangeJustificationMarker" in the ' 'change description.'); + } else if (_isAllowedDependabotChange(package, changeDescription)) { + logWarning('Ignoring lack of CHANGELOG update for Dependabot change to ' + 'a known internal dependency.'); } else { printError( 'No CHANGELOG change found. If this PR needs an exemption from ' @@ -605,4 +613,46 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return null; } + + /// Returns true if [changeDescription] matches a Dependabot change for a + /// dependency roll that should bypass the normal version and CHANGELOG change + /// checks (for dependencies that are known not to have client impact). + bool _isAllowedDependabotChange( + RepositoryPackage package, String changeDescription) { + // Espresso exports some dependencies that are normally just internal test + // utils, so always require reviewers to check that. + if (package.directory.basename == 'espresso') { + return false; + } + + // A string that is in all Dependabot PRs, but extremely unlikely to be in + // any other PR, to identify Dependabot PRs. + const String dependabotMarker = 'Dependabot commands and options'; + // Expression to extract the name of the dependency being updated. + final RegExp dependencyRegex = + RegExp(r'Bumps? \[(.*?)\]\(.*?\) from [\d.]+ to [\d.]+'); + + // Allowed exact dependency names. + const Set allowedDependencies = { + 'junit', + 'robolectric', + }; + const Set allowedDependencyPrefixes = { + 'mockito-' // mockito-core, mockito-inline, etc. + }; + + if (changeDescription.contains(dependabotMarker)) { + final Match? match = dependencyRegex.firstMatch(changeDescription); + if (match != null) { + final String dependency = match.group(1)!; + if (allowedDependencies.contains(dependency) || + allowedDependencyPrefixes + .any((String prefix) => dependency.startsWith(prefix))) { + return true; + } + } + } + + return false; + } } diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 8f8d510fd10..82133328089 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -40,6 +40,55 @@ void testAllowedVersion( } } +String _generateFakeDependabotPRDescription(String package) { + return ''' +Bumps [$package](https://github.com/foo/$package) from 1.0.0 to 2.0.0. +
+Release notes +

Sourced from $package's releases.

+
+... +
+
+
+Commits +
    +
  • ...
  • +
+
+
+ + +[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=$package&package-manager=gradle&previous-version=1.0.0&new-version=2.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) + +Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. + +[//]: # (dependabot-automerge-start) +[//]: # (dependabot-automerge-end) + +--- + +
+Dependabot commands and options +
+ +You can trigger Dependabot actions by commenting on this PR: +- `@dependabot rebase` will rebase this PR +- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it +- `@dependabot merge` will merge this PR after your CI passes on it +- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it +- `@dependabot cancel merge` will cancel a previously requested merge and block automerging +- `@dependabot reopen` will reopen this PR if it is closed +- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually +- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) + + +
+'''; +} + class MockProcessResult extends Mock implements io.ProcessResult {} void main() { @@ -1078,6 +1127,170 @@ No CHANGELOG change: Code change is only to implementation comments. ]), ); }); + + group('dependabot', () { + test('allows missing version and CHANGELOG change for mockito', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/android/build.gradle +'''), + ]; + + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile.writeAsStringSync( + _generateFakeDependabotPRDescription('mockito-core')); + + final List output = + await _runWithMissingChangeDetection([ + '--change-description-file=${changeDescriptionFile.path}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Ignoring lack of version change for Dependabot ' + 'change to a known internal dependency.'), + contains('Ignoring lack of CHANGELOG update for Dependabot ' + 'change to a known internal dependency.'), + ]), + ); + }); + + test('allows missing version and CHANGELOG change for robolectric', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/android/build.gradle +'''), + ]; + + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile.writeAsStringSync( + _generateFakeDependabotPRDescription('robolectric')); + + final List output = + await _runWithMissingChangeDetection([ + '--change-description-file=${changeDescriptionFile.path}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Ignoring lack of version change for Dependabot ' + 'change to a known internal dependency.'), + contains('Ignoring lack of CHANGELOG update for Dependabot ' + 'change to a known internal dependency.'), + ]), + ); + }); + + test('allows missing version and CHANGELOG change for junit', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/android/build.gradle +'''), + ]; + + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile + .writeAsStringSync(_generateFakeDependabotPRDescription('junit')); + + final List output = + await _runWithMissingChangeDetection([ + '--change-description-file=${changeDescriptionFile.path}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Ignoring lack of version change for Dependabot ' + 'change to a known internal dependency.'), + contains('Ignoring lack of CHANGELOG update for Dependabot ' + 'change to a known internal dependency.'), + ]), + ); + }); + + test('fails for dependencies that are not explicitly allowed', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/android/build.gradle +'''), + ]; + + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile.writeAsStringSync( + _generateFakeDependabotPRDescription('somethingelse')); + + Error? commandError; + final List output = + await _runWithMissingChangeDetection([ + '--change-description-file=${changeDescriptionFile.path}' + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No version change found'), + contains('plugin:\n' + ' Missing version change'), + ]), + ); + }); + }); }); test('allows valid against pub', () async { From f972cb1500a5d8556ae7f1fea69b0cfe6c36897e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 20 Jul 2022 20:15:06 -0400 Subject: [PATCH 200/249] [tool] Handle dependabot commit messages (#6127) --- .../tool/lib/src/version_check_command.dart | 10 +++- .../tool/test/version_check_command_test.dart | 58 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index e914d2f080e..30a45d33a77 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -627,7 +627,12 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. // A string that is in all Dependabot PRs, but extremely unlikely to be in // any other PR, to identify Dependabot PRs. - const String dependabotMarker = 'Dependabot commands and options'; + const String dependabotPRDescriptionMarker = + 'Dependabot commands and options'; + // The same thing, but for the Dependabot commit message, to work around + // https://github.com/cirruslabs/cirrus-ci-docs/issues/1029. + const String dependabotCommitMessageMarker = + 'Signed-off-by: dependabot[bot]'; // Expression to extract the name of the dependency being updated. final RegExp dependencyRegex = RegExp(r'Bumps? \[(.*?)\]\(.*?\) from [\d.]+ to [\d.]+'); @@ -641,7 +646,8 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. 'mockito-' // mockito-core, mockito-inline, etc. }; - if (changeDescription.contains(dependabotMarker)) { + if (changeDescription.contains(dependabotPRDescriptionMarker) || + changeDescription.contains(dependabotCommitMessageMarker)) { final Match? match = dependencyRegex.firstMatch(changeDescription); if (match != null) { final String dependency = match.group(1)!; diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 82133328089..4586070fd67 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -89,6 +89,23 @@ You can trigger Dependabot actions by commenting on this PR: '''; } +String _generateFakeDependabotCommitMessage(String package) { + return ''' +Bumps [$package](https://github.com/foo/$package) from 1.0.0 to 2.0.0. +- [Release notes](https://github.com/foo/$package/releases) +- [Commits](foo/$package@v4.3.1...v4.6.1) + +--- +updated-dependencies: +- dependency-name: $package + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] +'''; +} + class MockProcessResult extends Mock implements io.ProcessResult {} void main() { @@ -1290,6 +1307,47 @@ packages/plugin/android/build.gradle ]), ); }); + + // Tests workaround for + // https://github.com/cirruslabs/cirrus-ci-docs/issues/1029. + test('allow list works for commit messages', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/android/build.gradle +'''), + ]; + + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile.writeAsStringSync( + _generateFakeDependabotCommitMessage('mockito-core')); + + final List output = + await _runWithMissingChangeDetection([ + '--change-description-file=${changeDescriptionFile.path}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Ignoring lack of version change for Dependabot ' + 'change to a known internal dependency.'), + contains('Ignoring lack of CHANGELOG update for Dependabot ' + 'change to a known internal dependency.'), + ]), + ); + }); }); }); From c8e2e078574617c3bc2df26188db982a678ae2b9 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 25 Jul 2022 20:18:03 -0400 Subject: [PATCH 201/249] [tool] Include `dev_dependencies` in `make-deps-path-based` (#6146) --- script/tool/CHANGELOG.md | 4 +- .../lib/src/make_deps_path_based_command.dart | 10 ++++- script/tool/pubspec.yaml | 2 +- .../make_deps_path_based_command_test.dart | 44 ++++++++++++++++++- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a84e9af6390..f0534c23a29 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 0.8.9 +- Includes `dev_dependencies` when overridding dependencies using + `make-deps-path-based`. - Bypasses version and CHANGELOG checks for Dependabot PRs for packages that are known not to be client-affecting. diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index 4bbecb4d224..805dd68a0af 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -154,8 +154,14 @@ class MakeDepsPathBasedCommand extends PluginCommand { throw ToolExit(_exitCannotUpdatePubspec); } - final Iterable packagesToOverride = pubspec.dependencies.keys.where( - (String packageName) => localDependencies.containsKey(packageName)); + final Iterable combinedDependencies = [ + ...pubspec.dependencies.keys, + ...pubspec.devDependencies.keys, + ]; + final Iterable packagesToOverride = combinedDependencies + .where( + (String packageName) => localDependencies.containsKey(packageName)) + .toList(); if (packagesToOverride.isNotEmpty) { final String commonBasePath = packagesDir.path; // Find the relative path to the common base. diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index b8233de11b4..a4ecbfb75e9 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.8 +version: 0.8.9 dependencies: args: ^2.1.0 diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index 2644e814f57..33d6be261e8 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -60,6 +60,19 @@ void main() { package.pubspecFile.writeAsStringSync(lines.join('\n')); } + /// Adds a 'dev_dependencies:' section with entries for each package in + /// [dependencies] to [package]. + void _addDevDependenciesSection( + RepositoryPackage package, Iterable devDependencies) { + final String originalContent = package.pubspecFile.readAsStringSync(); + package.pubspecFile.writeAsStringSync(''' +$originalContent + +dev_dependencies: +${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} +'''); + } + test('no-ops for no plugins', () async { createFakePackage('foo', packagesDir, isFlutter: true); final RepositoryPackage packageBar = @@ -81,7 +94,7 @@ void main() { expect(packageBar.pubspecFile.readAsStringSync(), originalPubspecContents); }); - test('rewrites references', () async { + test('rewrites "dependencies" references', () async { final RepositoryPackage simplePackage = createFakePackage('foo', packagesDir, isFlutter: true); final Directory pluginGroup = packagesDir.childDirectory('bar'); @@ -142,6 +155,35 @@ void main() { ])); }); + test('rewrites "dev_dependencies" references', () async { + createFakePackage('foo', packagesDir); + final RepositoryPackage builderPackage = + createFakePackage('foo_builder', packagesDir); + + _addDevDependenciesSection(builderPackage, [ + 'foo', + ]); + + final List output = await runCapturingPrint( + runner, ['make-deps-path-based', '--target-dependencies=foo']); + + expect( + output, + containsAll([ + 'Rewriting references to: foo...', + ' Modified packages/foo_builder/pubspec.yaml', + ])); + + expect( + builderPackage.pubspecFile.readAsLinesSync(), + containsAllInOrder([ + '# FOR TESTING ONLY. DO NOT MERGE.', + 'dependency_overrides:', + ' foo:', + ' path: ../foo', + ])); + }); + // This test case ensures that running CI using this command on an interim // PR that itself used this command won't fail on the rewrite step. test('running a second time no-ops without failing', () async { From f2d9f51dd1028bbbe79981d1496f28ee22768483 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 27 Jul 2022 13:13:05 -0400 Subject: [PATCH 202/249] [path_provider] Restore 2.8 compatibility on Android and iOS (#6039) Adds a new repo tooling command that removes dev_dependencies, which aren't needed to consume a package, only for development. Also adds a --lib-only flag to analyze to analyze only the client-facing code. This is intended for use in the legacy analyze CI steps, primarily to solve the problem that currently plugins that use Pigeon can't support a version of Flutter older than the version supported by Pigeon, because otherwise the legacy analysis CI steps fail. Adds this new command to the legacy analysis CI step, and restores the recently-removed 2.8/2.10 compatibility to path_provider. --- script/tool/CHANGELOG.md | 7 ++ script/tool/lib/src/analyze_command.dart | 26 +++-- script/tool/lib/src/main.dart | 2 + .../tool/lib/src/remove_dev_dependencies.dart | 58 ++++++++++ script/tool/pubspec.yaml | 2 +- script/tool/test/analyze_command_test.dart | 53 +++++++++ .../test/remove_dev_dependencies_test.dart | 102 ++++++++++++++++++ 7 files changed, 241 insertions(+), 9 deletions(-) create mode 100644 script/tool/lib/src/remove_dev_dependencies.dart create mode 100644 script/tool/test/remove_dev_dependencies_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index f0534c23a29..ad3f35a8e9c 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.8.10 + +- Adds a new `remove-dev-dependencies` command to remove `dev_dependencies` + entries to make legacy version analysis possible in more cases. +- Adds a `--lib-only` option to `analyze` to allow only analyzing the client + parts of a library for legacy verison compatibility. + ## 0.8.9 - Includes `dev_dependencies` when overridding dependencies using diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 8778b3de9d8..54b4f33a234 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -32,10 +32,13 @@ class AnalyzeCommand extends PackageLoopingCommand { valueHelp: 'dart-sdk', help: 'An optional path to a Dart SDK; this is used to override the ' 'SDK used to provide analysis.'); + argParser.addFlag(_libOnlyFlag, + help: 'Only analyze the lib/ directory of the main package, not the ' + 'entire package.'); } static const String _customAnalysisFlag = 'custom-analysis'; - + static const String _libOnlyFlag = 'lib-only'; static const String _analysisSdk = 'analysis-sdk'; late String _dartBinaryPath; @@ -104,13 +107,20 @@ class AnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - // Analysis runs over the package and all subpackages, so all of them need - // `flutter pub get` run before analyzing. `example` packages can be - // skipped since 'flutter packages get' automatically runs `pub get` in - // examples as part of handling the parent directory. + final bool libOnly = getBoolArg(_libOnlyFlag); + + if (libOnly && !package.libDirectory.existsSync()) { + return PackageResult.skip('No lib/ directory.'); + } + + // Analysis runs over the package and all subpackages (unless only lib/ is + // being analyzed), so all of them need `flutter pub get` run before + // analyzing. `example` packages can be skipped since 'flutter packages get' + // automatically runs `pub get` in examples as part of handling the parent + // directory. final List packagesToGet = [ package, - ...await getSubpackages(package).toList(), + if (!libOnly) ...await getSubpackages(package).toList(), ]; for (final RepositoryPackage packageToGet in packagesToGet) { if (packageToGet.directory.basename != 'example' || @@ -129,8 +139,8 @@ class AnalyzeCommand extends PackageLoopingCommand { if (_hasUnexpecetdAnalysisOptions(package)) { return PackageResult.fail(['Unexpected local analysis options']); } - final int exitCode = await processRunner.runAndStream( - _dartBinaryPath, ['analyze', '--fatal-infos'], + final int exitCode = await processRunner.runAndStream(_dartBinaryPath, + ['analyze', '--fatal-infos', if (libOnly) 'lib'], workingDir: package.directory); if (exitCode != 0) { return PackageResult.fail(); diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 966e7b6be56..ea1ec067c82 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -28,6 +28,7 @@ import 'publish_check_command.dart'; import 'publish_plugin_command.dart'; import 'pubspec_check_command.dart'; import 'readme_check_command.dart'; +import 'remove_dev_dependencies.dart'; import 'test_command.dart'; import 'update_excerpts_command.dart'; import 'update_release_info_command.dart'; @@ -71,6 +72,7 @@ void main(List args) { ..addCommand(PublishPluginCommand(packagesDir)) ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(ReadmeCheckCommand(packagesDir)) + ..addCommand(RemoveDevDependenciesCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) ..addCommand(UpdateExcerptsCommand(packagesDir)) ..addCommand(UpdateReleaseInfoCommand(packagesDir)) diff --git a/script/tool/lib/src/remove_dev_dependencies.dart b/script/tool/lib/src/remove_dev_dependencies.dart new file mode 100644 index 00000000000..3085e0df85e --- /dev/null +++ b/script/tool/lib/src/remove_dev_dependencies.dart @@ -0,0 +1,58 @@ +// 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 'package:yaml_edit/yaml_edit.dart'; + +import 'common/package_looping_command.dart'; +import 'common/repository_package.dart'; + +/// A command to remove dev_dependencies, which are not used by package clients. +/// +/// This is intended for use with legacy Flutter version testing, to allow +/// running analysis (with --lib-only) with versions that are supported for +/// clients of the library, but not for development of the library. +class RemoveDevDependenciesCommand extends PackageLoopingCommand { + /// Creates a publish metadata updater command instance. + RemoveDevDependenciesCommand(Directory packagesDir) : super(packagesDir); + + @override + final String name = 'remove-dev-dependencies'; + + @override + final String description = 'Removes any dev_dependencies section from a ' + 'package, to allow more legacy testing.'; + + @override + bool get hasLongOutput => false; + + @override + PackageLoopingType get packageLoopingType => + PackageLoopingType.includeAllSubpackages; + + @override + Future runForPackage(RepositoryPackage package) async { + bool changed = false; + final YamlEditor editablePubspec = + YamlEditor(package.pubspecFile.readAsStringSync()); + const String devDependenciesKey = 'dev_dependencies'; + final YamlNode root = editablePubspec.parseAt([]); + final YamlMap? devDependencies = + (root as YamlMap)[devDependenciesKey] as YamlMap?; + if (devDependencies != null) { + changed = true; + print('${indentation}Removed dev_dependencies'); + editablePubspec.remove([devDependenciesKey]); + } + + if (changed) { + package.pubspecFile.writeAsStringSync(editablePubspec.toString()); + } + + return changed + ? PackageResult.success() + : PackageResult.skip('Nothing to remove.'); + } +} diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index a4ecbfb75e9..2eee1e3c7b1 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.9 +version: 0.8.10 dependencies: args: ^2.1.0 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index a4a47a22118..d3abf0b6b8e 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -93,6 +93,59 @@ void main() { ])); }); + test('passes lib/ directory with --lib-only', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + await runCapturingPrint(runner, ['analyze', '--lib-only']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['analyze', '--fatal-infos', 'lib'], + package.path), + ])); + }); + + test('skips when missing lib/ directory with --lib-only', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + package.libDirectory.deleteSync(); + + final List output = + await runCapturingPrint(runner, ['analyze', '--lib-only']); + + expect(processRunner.recordedCalls, isEmpty); + expect( + output, + containsAllInOrder([ + contains('SKIPPING: No lib/ directory'), + ]), + ); + }); + + test( + 'does not run flutter pub get for non-example subpackages with --lib-only', + () async { + final RepositoryPackage mainPackage = createFakePackage('a', packagesDir); + final Directory otherPackagesDir = + mainPackage.directory.childDirectory('other_packages'); + createFakePackage('subpackage1', otherPackagesDir); + createFakePackage('subpackage2', otherPackagesDir); + + await runCapturingPrint(runner, ['analyze', '--lib-only']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['pub', 'get'], mainPackage.path), + ProcessCall('dart', const ['analyze', '--fatal-infos', 'lib'], + mainPackage.path), + ])); + }); + test("don't elide a non-contained example package", () async { final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('example', packagesDir); diff --git a/script/tool/test/remove_dev_dependencies_test.dart b/script/tool/test/remove_dev_dependencies_test.dart new file mode 100644 index 00000000000..6b212870c53 --- /dev/null +++ b/script/tool/test/remove_dev_dependencies_test.dart @@ -0,0 +1,102 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/remove_dev_dependencies.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + final RemoveDevDependenciesCommand command = RemoveDevDependenciesCommand( + packagesDir, + ); + runner = CommandRunner('trim_dev_dependencies_command', + 'Test for trim_dev_dependencies_command'); + runner.addCommand(command); + }); + + void _addToPubspec(RepositoryPackage package, String addition) { + final String originalContent = package.pubspecFile.readAsStringSync(); + package.pubspecFile.writeAsStringSync(''' +$originalContent +$addition +'''); + } + + test('skips if nothing is removed', () async { + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + final List output = + await runCapturingPrint(runner, ['remove-dev-dependencies']); + + expect( + output, + containsAllInOrder([ + contains('SKIPPING: Nothing to remove.'), + ]), + ); + }); + + test('removes dev_dependencies', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + _addToPubspec(package, ''' +dev_dependencies: + some_dependency: ^2.1.8 + another_dependency: ^1.0.0 +'''); + + final List output = + await runCapturingPrint(runner, ['remove-dev-dependencies']); + + expect( + output, + containsAllInOrder([ + contains('Removed dev_dependencies'), + ]), + ); + expect(package.pubspecFile.readAsStringSync(), + isNot(contains('some_dependency:'))); + expect(package.pubspecFile.readAsStringSync(), + isNot(contains('another_dependency:'))); + }); + + test('removes from examples', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + final RepositoryPackage example = package.getExamples().first; + _addToPubspec(example, ''' +dev_dependencies: + some_dependency: ^2.1.8 + another_dependency: ^1.0.0 +'''); + + final List output = + await runCapturingPrint(runner, ['remove-dev-dependencies']); + + expect( + output, + containsAllInOrder([ + contains('Removed dev_dependencies'), + ]), + ); + expect(package.pubspecFile.readAsStringSync(), + isNot(contains('some_dependency:'))); + expect(package.pubspecFile.readAsStringSync(), + isNot(contains('another_dependency:'))); + }); +} From b83209e3e5a8bc03121902abd0d636fbdf499d52 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 27 Jul 2022 13:59:04 -0400 Subject: [PATCH 203/249] [tool] Switch PR description overrides over to labels (#6145) --- script/tool/CHANGELOG.md | 7 ++ .../tool/lib/src/version_check_command.dart | 104 +++++++++------- script/tool/pubspec.yaml | 2 +- .../tool/test/version_check_command_test.dart | 117 +++++++----------- 4 files changed, 109 insertions(+), 121 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index ad3f35a8e9c..667f1a188d1 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.9.0 + +* Replaces PR-description-based version/changelog/breaking change check + overrides in `version-check` with label-based overrides using a new + `pr-labels` flag, since we don't actually have reliable access to the + PR description in checks. + ## 0.8.10 - Adds a new `remove-dev-dependencies` command to remove `dev_dependencies` diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 30a45d33a77..d5e9675adcb 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -123,47 +123,49 @@ class VersionCheckCommand extends PackageLoopingCommand { '(e.g., PR description or commit message).\n\n' 'If supplied, this is used to allow overrides to some version ' 'checks.'); + argParser.addOption(_prLabelsArg, + help: 'A comma-separated list of labels associated with this PR, ' + 'if applicable.\n\n' + 'If supplied, this may be to allow overrides to some version ' + 'checks.'); argParser.addFlag(_checkForMissingChanges, help: 'Validates that changes to packages include CHANGELOG and ' 'version changes unless they meet an established exemption.\n\n' - 'If used with --$_changeDescriptionFile, this is should only be ' - 'used in pre-submit CI checks, to prevent the possibility of ' - 'post-submit breakage if an override justification is not ' - 'transferred into the commit message.', + 'If used with --$_prLabelsArg, this is should only be ' + 'used in pre-submit CI checks, to prevent post-submit breakage ' + 'when labels are no longer applicable.', hide: true); argParser.addFlag(_ignorePlatformInterfaceBreaks, help: 'Bypasses the check that platform interfaces do not contain ' 'breaking changes.\n\n' 'This is only intended for use in post-submit CI checks, to ' - 'prevent the possibility of post-submit breakage if a change ' - 'description justification is not transferred into the commit ' - 'message. Pre-submit checks should always use ' - '--$_changeDescriptionFile instead.', + 'prevent post-submit breakage when overriding the check with ' + 'labels. Pre-submit checks should always use ' + '--$_prLabelsArg instead.', hide: true); } static const String _againstPubFlag = 'against-pub'; static const String _changeDescriptionFile = 'change-description-file'; + static const String _prLabelsArg = 'pr-labels'; static const String _checkForMissingChanges = 'check-for-missing-changes'; static const String _ignorePlatformInterfaceBreaks = 'ignore-platform-interface-breaks'; - /// The string that must be in [_changeDescriptionFile] to allow a breaking + /// The label that must be on a PR to allow a breaking /// change to a platform interface. - static const String _breakingChangeJustificationMarker = - '## Breaking change justification'; + static const String _breakingChangeOverrideLabel = + 'override: allow breaking change'; - /// The string that must be at the start of a line in [_changeDescriptionFile] - /// to allow skipping a version change for a PR that would normally require - /// one. - static const String _missingVersionChangeJustificationMarker = - 'No version change:'; + /// The label that must be on a PR to allow skipping a version change for a PR + /// that would normally require one. + static const String _missingVersionChangeOverrideLabel = + 'override: no versioning needed'; - /// The string that must be at the start of a line in [_changeDescriptionFile] - /// to allow skipping a CHANGELOG change for a PR that would normally require - /// one. - static const String _missingChangelogChangeJustificationMarker = - 'No CHANGELOG change:'; + /// The label that must be on a PR to allow skipping a CHANGELOG change for a + /// PR that would normally require one. + static const String _missingChangelogChangeOverrideLabel = + 'override: no changelog needed'; final PubVersionFinder _pubVersionFinder; @@ -172,6 +174,7 @@ class VersionCheckCommand extends PackageLoopingCommand { late final List _changedFiles; late final String _changeDescription = _loadChangeDescription(); + late final Set _prLabels = _getPRLabels(); @override final String name = 'version-check'; @@ -498,18 +501,25 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return true; } - if (_getChangeDescription().contains(_breakingChangeJustificationMarker)) { + if (_prLabels.contains(_breakingChangeOverrideLabel)) { logWarning( '${indentation}Allowing breaking change to ${package.displayName} ' - 'due to "$_breakingChangeJustificationMarker" in the change ' - 'description.'); + 'due to the "$_breakingChangeOverrideLabel" label.'); return true; } return false; } - String _getChangeDescription() => _changeDescription; + /// Returns the labels associated with this PR, if any, or an empty set + /// if that flag is not provided. + Set _getPRLabels() { + final String labels = getStringArg(_prLabelsArg); + if (labels.isEmpty) { + return {}; + } + return labels.split(',').map((String label) => label.trim()).toSet(); + } /// Returns the contents of the file pointed to by [_changeDescriptionFile], /// or an empty string if that flag is not provided. @@ -569,44 +579,38 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. } if (state.needsVersionChange) { - final String changeDescription = _getChangeDescription(); - if (changeDescription.split('\n').any((String line) => - line.startsWith(_missingVersionChangeJustificationMarker))) { - logWarning('Ignoring lack of version change due to ' - '"$_missingVersionChangeJustificationMarker" in the ' - 'change description.'); - } else if (_isAllowedDependabotChange(package, changeDescription)) { + if (_prLabels.contains(_missingVersionChangeOverrideLabel)) { + logWarning('Ignoring lack of version change due to the ' + '"$_missingVersionChangeOverrideLabel" label.'); + } else if (_isAllowedDependabotChange(package, _changeDescription)) { logWarning('Ignoring lack of version change for Dependabot change to ' 'a known internal dependency.'); } else { printError( 'No version change found, but the change to this package could ' 'not be verified to be exempt from version changes according to ' - 'repository policy. If this is a false positive, please ' - 'add a line starting with\n' - '$_missingVersionChangeJustificationMarker\n' - 'to your PR description with an explanation of why it is exempt.'); + 'repository policy. If this is a false positive, please comment in ' + 'the PR to explain why the PR is exempt, and add (or ask your ' + 'reviewer to add) the "$_missingVersionChangeOverrideLabel" ' + 'label.'); return 'Missing version change'; } } if (!state.hasChangelogChange) { - final String changeDescription = _getChangeDescription(); - if (changeDescription.split('\n').any((String line) => - line.startsWith(_missingChangelogChangeJustificationMarker))) { - logWarning('Ignoring lack of CHANGELOG update due to ' - '"$_missingChangelogChangeJustificationMarker" in the ' - 'change description.'); - } else if (_isAllowedDependabotChange(package, changeDescription)) { + if (_prLabels.contains(_missingChangelogChangeOverrideLabel)) { + logWarning('Ignoring lack of CHANGELOG update due to the ' + '"$_missingChangelogChangeOverrideLabel" label.'); + } else if (_isAllowedDependabotChange(package, _changeDescription)) { logWarning('Ignoring lack of CHANGELOG update for Dependabot change to ' 'a known internal dependency.'); } else { printError( 'No CHANGELOG change found. If this PR needs an exemption from ' 'the standard policy of listing all changes in the CHANGELOG, ' - 'please add a line starting with\n' - '$_missingChangelogChangeJustificationMarker\n' - 'to your PR description with an explanation of why.'); + 'comment in the PR to explain why the PR is exempt, and add (or ' + 'ask your reviewer to add) the ' + '"$_missingChangelogChangeOverrideLabel" label.'); return 'Missing CHANGELOG change'; } } @@ -617,6 +621,11 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. /// Returns true if [changeDescription] matches a Dependabot change for a /// dependency roll that should bypass the normal version and CHANGELOG change /// checks (for dependencies that are known not to have client impact). + /// + /// Depending on CI, [changeDescription] may either be the PR description, or + /// the description of the last commit (see for example discussion in + /// https://github.com/cirruslabs/cirrus-ci-docs/issues/1029), so this needs + /// to handle both. bool _isAllowedDependabotChange( RepositoryPackage package, String changeDescription) { // Espresso exports some dependencies that are normally just internal test @@ -629,8 +638,7 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. // any other PR, to identify Dependabot PRs. const String dependabotPRDescriptionMarker = 'Dependabot commands and options'; - // The same thing, but for the Dependabot commit message, to work around - // https://github.com/cirruslabs/cirrus-ci-docs/issues/1029. + // The same thing, but for the Dependabot commit message. const String dependabotCommitMessageMarker = 'Signed-off-by: dependabot[bot]'; // Expression to extract the name of the dependency being updated. diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 2eee1e3c7b1..a99be709819 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.10 +version: 0.9.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 4586070fd67..f8153750122 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -364,35 +364,25 @@ void main() { ])); }); - test('allows breaking changes to platform interfaces with explanation', + test('allows breaking changes to platform interfaces with override label', () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync(''' -Some general PR description -## Breaking change justification - -This is necessary because of X, Y, and Z - -## Another section'''); final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', - '--change-description-file=${changeDescriptionFile.path}' + '--pr-labels=some label,override: allow breaking change,another-label' ]); expect( output, containsAllInOrder([ contains('Allowing breaking change to plugin_platform_interface ' - 'due to "## Breaking change justification" in the change ' - 'description.'), + 'due to the "override: allow breaking change" label.'), contains('Ran for 1 package(s) (1 with warnings)'), ]), ); @@ -408,42 +398,6 @@ This is necessary because of X, Y, and Z ])); }); - test('throws if a nonexistent change description file is specified', - () async { - createFakePlugin('plugin_platform_interface', packagesDir, - version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=main', - '--change-description-file=a_missing_file.txt' - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No such file: a_missing_file.txt'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', - [ - 'main:packages/plugin_platform_interface/pubspec.yaml' - ], - null) - ])); - }); - test('allows breaking changes to platform interfaces with bypass flag', () async { createFakePlugin('plugin_platform_interface', packagesDir, @@ -965,7 +919,7 @@ packages/plugin/CHANGELOG.md ); }); - test('allows missing version change with justification', () async { + test('allows missing version change with override label', () async { final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); @@ -985,23 +939,16 @@ packages/plugin/pubspec.yaml '''), ]; - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync(''' -Some general PR description - -No version change: Code change is only to implementation comments. -'''); final List output = await _runWithMissingChangeDetection([ - '--change-description-file=${changeDescriptionFile.path}' + '--pr-labels=some label,override: no versioning needed,another-label' ]); expect( output, containsAllInOrder([ - contains('Ignoring lack of version change due to ' - '"No version change:" in the change description.'), + contains('Ignoring lack of version change due to the ' + '"override: no versioning needed" label.'), ]), ); }); @@ -1124,28 +1071,56 @@ packages/plugin/example/lib/foo.dart '''), ]; - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync(''' -Some general PR description - -No CHANGELOG change: Code change is only to implementation comments. -'''); final List output = await _runWithMissingChangeDetection([ - '--change-description-file=${changeDescriptionFile.path}' + '--pr-labels=some label,override: no changelog needed,another-label' ]); expect( output, containsAllInOrder([ - contains('Ignoring lack of CHANGELOG update due to ' - '"No CHANGELOG change:" in the change description.'), + contains('Ignoring lack of CHANGELOG update due to the ' + '"override: no changelog needed" label.'), ]), ); }); group('dependabot', () { + test('throws if a nonexistent change description file is specified', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/android/build.gradle +'''), + ]; + + Error? commandError; + final List output = await _runWithMissingChangeDetection( + ['--change-description-file=a_missing_file.txt'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No such file: a_missing_file.txt'), + ]), + ); + }); + test('allows missing version and CHANGELOG change for mockito', () async { final RepositoryPackage plugin = @@ -1308,8 +1283,6 @@ packages/plugin/android/build.gradle ); }); - // Tests workaround for - // https://github.com/cirruslabs/cirrus-ci-docs/issues/1029. test('allow list works for commit messages', () async { final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); From 1fbf2f6219772d8fa93679fa6d323c6cef314c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Sharma?= <737941+loic-sharma@users.noreply.github.com> Date: Thu, 28 Jul 2022 16:16:05 -0700 Subject: [PATCH 204/249] [camera_windows] Improve several error handling scenarios (#6149) --- script/tool/README.md | 2 ++ script/tool/lib/src/common/package_state_utils.dart | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/script/tool/README.md b/script/tool/README.md index d3beb53a110..225cda2ef26 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -105,6 +105,8 @@ cd dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android --no-integration --packages plugin_name # Run all tests for macOS: dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages plugin_name +# Run all tests for Windows: +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --windows --packages plugin_name ``` ### Update README.md from Example Sources diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index 437bbf6df37..a03d643bdab 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -35,7 +35,7 @@ class PackageChangeState { /// [changedPaths] should be a list of POSIX-style paths from a common root, /// and [relativePackagePath] should be the path to [package] from that same /// root. Commonly these will come from `gitVersionFinder.getChangedFiles()` -/// and `getRelativePoixPath(package.directory, gitDir.path)` respectively; +/// and `getRelativePosixPath(package.directory, gitDir.path)` respectively; /// they are arguments mainly to allow for caching the changed paths for an /// entire command run. PackageChangeState checkPackageChangeState( From 1725b0721d9c4aa63e3e7ca5474aef5d98a40ab7 Mon Sep 17 00:00:00 2001 From: Phil Quitslund Date: Wed, 3 Aug 2022 15:16:04 -0700 Subject: [PATCH 205/249] [script] fix script noop interpolated toString()s (#6175) --- script/tool/lib/src/common/package_looping_command.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 1a194bd45b9..9e696e95619 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -357,7 +357,7 @@ abstract class PackageLoopingCommand extends PluginCommand { if (flutterConstraint != null && !flutterConstraint.allows(minFlutterVersion)) { return PackageResult.skip( - 'Does not support Flutter ${minFlutterVersion.toString()}'); + 'Does not support Flutter $minFlutterVersion'); } } @@ -365,8 +365,7 @@ abstract class PackageLoopingCommand extends PluginCommand { final Pubspec pubspec = package.parsePubspec(); final VersionConstraint? dartConstraint = pubspec.environment?['sdk']; if (dartConstraint != null && !dartConstraint.allows(minDartVersion)) { - return PackageResult.skip( - 'Does not support Dart ${minDartVersion.toString()}'); + return PackageResult.skip('Does not support Dart $minDartVersion'); } } From 62141c86f11aedef2e3f4dc4470be964690ca29e Mon Sep 17 00:00:00 2001 From: Ricardo Amador <32242716+ricardoamador@users.noreply.github.com> Date: Thu, 4 Aug 2022 16:25:42 -0700 Subject: [PATCH 206/249] Move Cirrus task logs to flutter-infra-staging project (#6180) * Changed cirrus logging to flutter-firebase-testlab-staging * Fix test data --- .../tool/lib/src/firebase_test_lab_command.dart | 2 +- .../test/firebase_test_lab_command_test.dart | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 4505259b731..832870e0e4c 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -59,7 +59,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { help: 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); argParser.addOption('results-bucket', - defaultsTo: 'gs://flutter_firebase_testlab'); + defaultsTo: 'gs://flutter_firebase_testlab_staging'); argParser.addOption( kEnableExperiment, defaultsTo: '', diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index a4140939472..1ab7055d287 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -174,7 +174,7 @@ public class MainActivityTest { '/packages/plugin1/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin1/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin1/example'), ProcessCall( @@ -188,7 +188,7 @@ public class MainActivityTest { '/packages/plugin2/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin2/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin2/example'), ]), @@ -255,7 +255,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -265,7 +265,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/1/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example/1/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -320,7 +320,7 @@ public class MainActivityTest { '/packages/plugin/example/example1/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example1/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example1/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example/example1'), ProcessCall( @@ -330,7 +330,7 @@ public class MainActivityTest { '/packages/plugin/example/example2/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example2/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example2/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example/example2'), ]), @@ -613,7 +613,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), @@ -785,7 +785,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), From 2b712593b401d9dce4f3864bf7c0814a30fbe798 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 5 Aug 2022 16:33:05 -0400 Subject: [PATCH 207/249] [ci] Run analyze with minimum resolved packages (#6207) --- script/tool/CHANGELOG.md | 7 +++- script/tool/lib/src/analyze_command.dart | 22 ++++++++-- script/tool/pubspec.yaml | 2 +- script/tool/test/analyze_command_test.dart | 49 ++++++++++++++++++++++ 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 667f1a188d1..50e389fd319 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,10 +1,15 @@ +## 0.9.1 + +* Adds a `--downgrade` flag to `analyze` for analyzing with the oldest possible + versions of packages. + ## 0.9.0 * Replaces PR-description-based version/changelog/breaking change check overrides in `version-check` with label-based overrides using a new `pr-labels` flag, since we don't actually have reliable access to the PR description in checks. - + ## 0.8.10 - Adds a new `remove-dev-dependencies` command to remove `dev_dependencies` diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 54b4f33a234..c7a953c50ca 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -32,12 +32,16 @@ class AnalyzeCommand extends PackageLoopingCommand { valueHelp: 'dart-sdk', help: 'An optional path to a Dart SDK; this is used to override the ' 'SDK used to provide analysis.'); + argParser.addFlag(_downgradeFlag, + help: 'Runs "flutter pub downgrade" before analysis to verify that ' + 'the minimum constraints are sufficiently new for APIs used.'); argParser.addFlag(_libOnlyFlag, help: 'Only analyze the lib/ directory of the main package, not the ' 'entire package.'); } static const String _customAnalysisFlag = 'custom-analysis'; + static const String _downgradeFlag = 'downgrade'; static const String _libOnlyFlag = 'lib-only'; static const String _analysisSdk = 'analysis-sdk'; @@ -113,6 +117,12 @@ class AnalyzeCommand extends PackageLoopingCommand { return PackageResult.skip('No lib/ directory.'); } + if (getBoolArg(_downgradeFlag)) { + if (!await _runPubCommand(package, 'downgrade')) { + return PackageResult.fail(['Unable to downgrade dependencies']); + } + } + // Analysis runs over the package and all subpackages (unless only lib/ is // being analyzed), so all of them need `flutter pub get` run before // analyzing. `example` packages can be skipped since 'flutter packages get' @@ -127,10 +137,7 @@ class AnalyzeCommand extends PackageLoopingCommand { !RepositoryPackage(packageToGet.directory.parent) .pubspecFile .existsSync()) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, ['pub', 'get'], - workingDir: packageToGet.directory); - if (exitCode != 0) { + if (!await _runPubCommand(packageToGet, 'get')) { return PackageResult.fail(['Unable to get dependencies']); } } @@ -147,4 +154,11 @@ class AnalyzeCommand extends PackageLoopingCommand { } return PackageResult.success(); } + + Future _runPubCommand(RepositoryPackage package, String command) async { + final int exitCode = await processRunner.runAndStream( + flutterCommand, ['pub', command], + workingDir: package.directory); + return exitCode == 0; + } } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index a99be709819..e80afa22936 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.9.0 +version: 0.9.1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index d3abf0b6b8e..e10780fa9ec 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -187,6 +187,33 @@ void main() { ); }); + test('downgrades first when requested', () async { + final RepositoryPackage plugin = createFakePlugin('a', packagesDir); + + await runCapturingPrint(runner, ['analyze', '--downgrade']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['pub', 'downgrade'], + plugin.path, + ), + ProcessCall( + 'flutter', + const ['pub', 'get'], + plugin.path, + ), + ProcessCall( + 'dart', + const ['analyze', '--fatal-infos'], + plugin.path, + ), + ]), + ); + }); + group('verifies analysis settings', () { test('fails analysis_options.yaml', () async { createFakePlugin('foo', packagesDir, @@ -312,6 +339,28 @@ void main() { ); }); + test('fails if "pub downgrade" fails', () async { + createFakePlugin('foo', packagesDir); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(exitCode: 1) // flutter pub downgrade + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['analyze', '--downgrade'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to downgrade dependencies'), + ]), + ); + }); + test('fails if "analyze" fails', () async { createFakePlugin('foo', packagesDir); From d20563f6f416696c4dc9ba9962de233ed40f24c5 Mon Sep 17 00:00:00 2001 From: Rodrigues Date: Tue, 9 Aug 2022 02:52:06 +0300 Subject: [PATCH 208/249] Uncomments avoid_redundant_argument_values in analysis_options.yaml (#6163) --- script/tool/lib/src/common/plugin_command.dart | 1 - .../tool/lib/src/firebase_test_lab_command.dart | 2 +- script/tool/lib/src/lint_android_command.dart | 2 +- script/tool/lib/src/publish_check_command.dart | 5 +---- script/tool/lib/src/publish_plugin_command.dart | 7 +------ script/tool/lib/src/version_check_command.dart | 2 -- .../tool/test/build_examples_command_test.dart | 2 +- script/tool/test/common/file_utils_test.dart | 2 +- .../common/package_looping_command_test.dart | 16 +++++++--------- script/tool/test/common/plugin_utils_test.dart | 9 +++------ .../test/common/repository_package_test.dart | 2 +- script/tool/test/custom_test_command_test.dart | 4 ++-- script/tool/test/license_check_command_test.dart | 2 +- script/tool/test/lint_android_command_test.dart | 6 +++--- script/tool/test/lint_podspecs_command_test.dart | 2 +- .../tool/test/update_excerpts_command_test.dart | 6 +++--- script/tool/test/util.dart | 5 ++--- 17 files changed, 29 insertions(+), 46 deletions(-) diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index be9fb23e57a..843768fd4c6 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -44,7 +44,6 @@ abstract class PluginCommand extends Command { }) : _gitDir = gitDir { argParser.addMultiOption( _packagesArg, - splitCommas: true, help: 'Specifies which packages the command should run on (before sharding).\n', valueHelp: 'package1,package2,...', diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 832870e0e4c..4fc19e30014 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -335,7 +335,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } yield* integrationTestDir - .listSync(recursive: true, followLinks: true) + .listSync(recursive: true) .where((FileSystemEntity file) => file is File && file.basename.endsWith('_test.dart')) .cast(); diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index 8ba1d643a89..607674c80d3 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -35,7 +35,7 @@ class LintAndroidCommand extends PackageLoopingCommand { if (!pluginSupportsPlatform(platformAndroid, package, requiredMode: PlatformSupport.inline)) { return PackageResult.skip( - 'Plugin does not have an Android implemenatation.'); + 'Plugin does not have an Android implementation.'); } bool failed = false; diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index af8eac2257e..d98a286ff58 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -33,16 +33,13 @@ class PublishCheckCommand extends PackageLoopingCommand { help: 'Allows the pre-release SDK warning to pass.\n' 'When enabled, a pub warning, which asks to publish the package as a pre-release version when ' 'the SDK constraint is a pre-release version, is ignored.', - defaultsTo: false, ); argParser.addFlag(_machineFlag, help: 'Switch outputs to a machine readable JSON. \n' 'The JSON contains a "status" field indicating the final status of the command, the possible values are:\n' ' $_statusNeedsPublish: There is at least one package need to be published. They also passed all publish checks.\n' ' $_statusMessageNoPublish: There are no packages needs to be published. Either no pubspec change detected or all versions have already been published.\n' - ' $_statusMessageError: Some error has occurred.', - defaultsTo: false, - negatable: true); + ' $_statusMessageError: Some error has occurred.'); } static const String _allowPrereleaseFlag = 'allow-pre-release'; diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 7aa70bd4fd1..cae8edac71c 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -74,7 +74,6 @@ class PublishPluginCommand extends PackageLoopingCommand { help: 'Release all packages that contains pubspec changes at the current commit compares to the base-sha.\n' 'The --packages option is ignored if this is on.', - defaultsTo: false, ); argParser.addFlag( _dryRunFlag, @@ -82,14 +81,10 @@ class PublishPluginCommand extends PackageLoopingCommand { 'Skips the real `pub publish` and `git tag` commands and assumes both commands are successful.\n' 'This does not run `pub publish --dry-run`.\n' 'If you want to run the command with `pub publish --dry-run`, use `pub-publish-flags=--dry-run`', - defaultsTo: false, - negatable: true, ); argParser.addFlag(_skipConfirmationFlag, help: 'Run the command without asking for Y/N inputs.\n' - 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n', - defaultsTo: false, - negatable: true); + 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n'); } static const String _pubFlagsOption = 'pub-publish-flags'; diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index d5e9675adcb..2e5f1efd793 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -115,8 +115,6 @@ class VersionCheckCommand extends PackageLoopingCommand { help: 'Whether the version check should run against the version on pub.\n' 'Defaults to false, which means the version check only run against ' 'the previous version in code.', - defaultsTo: false, - negatable: true, ); argParser.addOption(_changeDescriptionFile, help: 'The path to a file containing the description of the change ' diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 420b3b1161d..a819e7a1267 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -504,7 +504,7 @@ void main() { }); test('skips non-Flutter examples', () async { - createFakePackage('package', packagesDir, isFlutter: false); + createFakePackage('package', packagesDir); final List output = await runCapturingPrint( runner, ['build-examples', '--ios']); diff --git a/script/tool/test/common/file_utils_test.dart b/script/tool/test/common/file_utils_test.dart index e3986842a96..79b804e31ea 100644 --- a/script/tool/test/common/file_utils_test.dart +++ b/script/tool/test/common/file_utils_test.dart @@ -10,7 +10,7 @@ import 'package:test/test.dart'; void main() { test('works on Posix', () async { final FileSystem fileSystem = - MemoryFileSystem(style: FileSystemStyle.posix); + MemoryFileSystem(); final Directory base = fileSystem.directory('/').childDirectory('base'); final File file = diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index ec2b9b9be23..7e9f63cb034 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -373,11 +373,9 @@ void main() { test('skips unsupported Dart versions when requested', () async { final RepositoryPackage excluded = createFakePackage( - 'excluded_package', packagesDir, - isFlutter: false, dartConstraint: '>=2.17.0 <3.0.0'); + 'excluded_package', packagesDir, dartConstraint: '>=2.17.0 <3.0.0'); final RepositoryPackage included = createFakePackage( - 'a_package', packagesDir, - isFlutter: false, dartConstraint: '>=2.14.0 <3.0.0'); + 'a_package', packagesDir); final TestPackageLoopingCommand command = createTestCommand( packageLoopingType: PackageLoopingType.includeAllSubpackages, @@ -409,7 +407,7 @@ void main() { createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true); + createTestCommand(); final List output = await runCommand(command); const String separator = @@ -443,7 +441,7 @@ void main() { createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true); + createTestCommand(); final List output = await runCommand(command, arguments: ['--log-timing']); @@ -595,7 +593,7 @@ void main() { createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true, captureOutput: true); + createTestCommand(captureOutput: true); final List output = await runCommand(command); expect(output, isEmpty); @@ -786,7 +784,7 @@ void main() { createFakePackage('package_f', packagesDir); final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true); + createTestCommand(); final List output = await runCommand(command); expect( @@ -812,7 +810,7 @@ void main() { createFakePackage('package_a', packagesDir); final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true); + createTestCommand(); final List output = await runCommand(command, arguments: ['--exclude=package_a']); diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index 9c5ddc3f85b..415b1db8932 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -223,12 +223,9 @@ void main() { 'plugin', packagesDir, platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: true, hasDartCode: true), - platformMacOS: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: true, hasDartCode: true), - platformWindows: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: true, hasDartCode: true), + platformLinux: const PlatformDetails(PlatformSupport.inline, hasDartCode: true), + platformMacOS: const PlatformDetails(PlatformSupport.inline, hasDartCode: true), + platformWindows: const PlatformDetails(PlatformSupport.inline, hasDartCode: true), }, ); diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index dadfc883299..db519c00823 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -213,7 +213,7 @@ void main() { test('returns false for non-Flutter package', () async { final RepositoryPackage package = - createFakePackage('a_package', packagesDir, isFlutter: false); + createFakePackage('a_package', packagesDir); expect(package.requiresFlutter(), false); }); }); diff --git a/script/tool/test/custom_test_command_test.dart b/script/tool/test/custom_test_command_test.dart index 54a1acf8b82..a28b47505e9 100644 --- a/script/tool/test/custom_test_command_test.dart +++ b/script/tool/test/custom_test_command_test.dart @@ -146,7 +146,7 @@ void main() { ]); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 0), // pub get + MockProcess(), // pub get MockProcess(exitCode: 1), // test script ]; @@ -306,7 +306,7 @@ void main() { ]); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 0), // pub get + MockProcess(), // pub get MockProcess(exitCode: 1), // test script ]; diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index efaf969c83f..43fbd6b5eca 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -200,7 +200,7 @@ void main() { test('handles the comment styles for all supported languages', () async { final File fileA = root.childFile('file_a.cc'); fileA.createSync(); - _writeLicense(fileA, comment: '// '); + _writeLicense(fileA); final File fileB = root.childFile('file_b.sh'); fileB.createSync(); _writeLicense(fileB, comment: '# '); diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index b072946ff95..e4a6c5c859e 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -24,7 +24,7 @@ void main() { late RecordingProcessRunner processRunner; setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); + fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); mockPlatform = MockPlatform(); processRunner = RecordingProcessRunner(); @@ -178,7 +178,7 @@ void main() { containsAllInOrder( [ contains( - 'SKIPPING: Plugin does not have an Android implemenatation.') + 'SKIPPING: Plugin does not have an Android implementation.') ], )); }); @@ -197,7 +197,7 @@ void main() { containsAllInOrder( [ contains( - 'SKIPPING: Plugin does not have an Android implemenatation.') + 'SKIPPING: Plugin does not have an Android implementation.') ], )); }); diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 516a32fa692..097bcff338a 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -23,7 +23,7 @@ void main() { late RecordingProcessRunner processRunner; setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); + fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); mockPlatform = MockPlatform(isMacOS: true); diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart index 5c1d74444ee..dd4b64a98cf 100644 --- a/script/tool/test/update_excerpts_command_test.dart +++ b/script/tool/test/update_excerpts_command_test.dart @@ -186,7 +186,7 @@ void main() { extraFiles: ['example/build.excerpt.yaml']); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 0), // dart pub get + MockProcess(), // dart pub get MockProcess(exitCode: 1), // dart run build_runner ... ]; @@ -211,8 +211,8 @@ void main() { extraFiles: ['example/build.excerpt.yaml']); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 0), // dart pub get - MockProcess(exitCode: 0), // dart run build_runner ... + MockProcess(), // dart pub get + MockProcess(), // dart run build_runner ... MockProcess(exitCode: 1), // dart run code_excerpt_updater ... ]; diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 041d93367f9..2754f64dde1 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -117,7 +117,6 @@ RepositoryPackage createFakePlugin( createFakePubspec( package, name: name, - isFlutter: true, isPlugin: true, platformSupport: platformSupport, version: version, @@ -399,10 +398,10 @@ class RecordingProcessRunner extends ProcessRunner { final io.Process? process = _getProcessToReturn(executable); final List? processStdout = await process?.stdout.transform(stdoutEncoding.decoder).toList(); - final String stdout = processStdout?.join('') ?? ''; + final String stdout = processStdout?.join() ?? ''; final List? processStderr = await process?.stderr.transform(stderrEncoding.decoder).toList(); - final String stderr = processStderr?.join('') ?? ''; + final String stderr = processStderr?.join() ?? ''; final io.ProcessResult result = process == null ? io.ProcessResult(1, 0, '', '') From 2e2d4d265d8064c602a52eb0082a214c953ef915 Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Fri, 26 Aug 2022 11:40:55 -0700 Subject: [PATCH 209/249] Adds info about commands requiring Flutter-bundled dart (#6312) --- script/tool/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/tool/README.md b/script/tool/README.md index 225cda2ef26..9b91b03eada 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -15,6 +15,8 @@ instead. (It is marked as Discontinued since it is no longer maintained as a general-purpose tool, but updates are still published for use in flutter/packages.) +The commands in tools require the Flutter-bundled version of Dart to be the first `dart` loaded in the path. + ### From Source (flutter/plugins only) Set up: From 8ceef0db44dc83cdbefb7cd214d41e10ffe5c6bb Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 26 Aug 2022 16:51:20 -0400 Subject: [PATCH 210/249] [tools] Validate code excerpt configuration (#6286) --- script/tool/CHANGELOG.md | 6 +++ script/tool/lib/src/readme_check_command.dart | 30 +++++++++++-- script/tool/pubspec.yaml | 2 +- .../tool/test/readme_check_command_test.dart | 44 ++++++++++++++++++- .../test/update_excerpts_command_test.dart | 16 +++---- script/tool/test/util.dart | 7 +++ 6 files changed, 90 insertions(+), 15 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 50e389fd319..595943777c6 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.9.2 + +* Adds checking of `code-excerpt` configuration to `readme-check`, to validate + that if the excerpting tags are added to a README they are actually being + used. + ## 0.9.1 * Adds a `--downgrade` flag to `analyze` for analyzing with the oldest possible diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index 6e79b736781..4acf997a008 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -12,6 +12,9 @@ import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; +const String _instructionWikiUrl = + 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages'; + /// A command to enforce README conventions across the repository. class ReadmeCheckCommand extends PackageLoopingCommand { /// Creates an instance of the README check command. @@ -95,7 +98,8 @@ class ReadmeCheckCommand extends PackageLoopingCommand { final List readmeLines = readme.readAsLinesSync(); final List errors = []; - final String? blockValidationError = _validateCodeBlocks(readmeLines); + final String? blockValidationError = + _validateCodeBlocks(readmeLines, mainPackage: mainPackage); if (blockValidationError != null) { errors.add(blockValidationError); } @@ -123,8 +127,12 @@ class ReadmeCheckCommand extends PackageLoopingCommand { } /// Validates that code blocks (``` ... ```) follow repository standards. - String? _validateCodeBlocks(List readmeLines) { + String? _validateCodeBlocks( + List readmeLines, { + required RepositoryPackage mainPackage, + }) { final RegExp codeBlockDelimiterPattern = RegExp(r'^\s*```\s*([^ ]*)\s*'); + const String excerptTagStart = ' missingLanguageLines = []; final List missingExcerptLines = []; bool inBlock = false; @@ -151,7 +159,6 @@ class ReadmeCheckCommand extends PackageLoopingCommand { // Check for code-excerpt usage if requested. if (getBoolArg(_requireExcerptsArg) && infoString == 'dart') { - const String excerptTagStart = ' line.startsWith(excerptTagStart))) { + const String buildRunnerConfigFile = 'build.excerpt.yaml'; + if (!mainPackage.getExamples().any((RepositoryPackage example) => + example.directory.childFile(buildRunnerConfigFile).existsSync())) { + printError('code-excerpt tag found, but the package is not configured ' + 'for excerpting. Follow the instructions at\n' + '$_instructionWikiUrl\n' + 'for setting up a build.excerpt.yaml file.'); + errorSummary ??= 'Missing code-excerpt configuration'; + } + } + if (missingExcerptLines.isNotEmpty) { for (final int lineNumber in missingExcerptLines) { printError('${indentation}Dart code block at line $lineNumber is not ' @@ -180,7 +201,8 @@ class ReadmeCheckCommand extends PackageLoopingCommand { printError( '\n${indentation}For each block listed above, add ' 'tag on the previous line, and ensure that a build.excerpt.yaml is ' - 'configured for the source example.\n'); + 'configured for the source example as explained at\n' + '$_instructionWikiUrl'); errorSummary ??= 'Missing code-excerpt management for code block'; } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index e80afa22936..3c25ef62b20 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.9.1 +version: 0.9.2 dependencies: args: ^2.1.0 diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart index fa4fc604dd7..37224fddc56 100644 --- a/script/tool/test/readme_check_command_test.dart +++ b/script/tool/test/readme_check_command_test.dart @@ -504,8 +504,11 @@ A B C }); test('passes when excerpt requirement is met', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + extraFiles: [kReadmeExcerptConfigPath], + ); package.readmeFile.writeAsStringSync(''' Example: @@ -528,6 +531,40 @@ A B C ); }); + test('fails when excerpts are used but the package is not configured', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + package.readmeFile.writeAsStringSync(''' +Example: + + +```dart +A B C +``` +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check', '--require-excerpts'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('code-excerpt tag found, but the package is not configured ' + 'for excerpting. Follow the instructions at\n' + 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages\n' + 'for setting up a build.excerpt.yaml file.'), + contains('Missing code-excerpt configuration'), + ]), + ); + }); + test('fails on missing excerpt tag when requested', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir); @@ -552,6 +589,9 @@ A B C output, containsAllInOrder([ contains('Dart code block at line 3 is not managed by code-excerpt.'), + // Ensure that the failure message links to instructions. + contains( + 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages'), contains('Missing code-excerpt management for code block'), ]), ); diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart index dd4b64a98cf..34c85cc172f 100644 --- a/script/tool/test/update_excerpts_command_test.dart +++ b/script/tool/test/update_excerpts_command_test.dart @@ -42,7 +42,7 @@ void main() { test('runs pub get before running scripts', () async { final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: ['example/build.excerpt.yaml']); + extraFiles: [kReadmeExcerptConfigPath]); final Directory example = getExampleDir(package); await runCapturingPrint(runner, ['update-excerpts']); @@ -69,7 +69,7 @@ void main() { test('runs when config is present', () async { final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: ['example/build.excerpt.yaml']); + extraFiles: [kReadmeExcerptConfigPath]); final Directory example = getExampleDir(package); final List output = @@ -128,7 +128,7 @@ void main() { test('restores pubspec even if running the script fails', () async { final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: ['example/build.excerpt.yaml']); + extraFiles: [kReadmeExcerptConfigPath]); processRunner.mockProcessesForExecutable['dart'] = [ MockProcess(exitCode: 1), // dart pub get @@ -159,7 +159,7 @@ void main() { test('fails if pub get fails', () async { createFakePlugin('a_package', packagesDir, - extraFiles: ['example/build.excerpt.yaml']); + extraFiles: [kReadmeExcerptConfigPath]); processRunner.mockProcessesForExecutable['dart'] = [ MockProcess(exitCode: 1), // dart pub get @@ -183,7 +183,7 @@ void main() { test('fails if extraction fails', () async { createFakePlugin('a_package', packagesDir, - extraFiles: ['example/build.excerpt.yaml']); + extraFiles: [kReadmeExcerptConfigPath]); processRunner.mockProcessesForExecutable['dart'] = [ MockProcess(), // dart pub get @@ -208,7 +208,7 @@ void main() { test('fails if injection fails', () async { createFakePlugin('a_package', packagesDir, - extraFiles: ['example/build.excerpt.yaml']); + extraFiles: [kReadmeExcerptConfigPath]); processRunner.mockProcessesForExecutable['dart'] = [ MockProcess(), // dart pub get @@ -234,7 +234,7 @@ void main() { test('fails if files are changed with --fail-on-change', () async { createFakePlugin('a_plugin', packagesDir, - extraFiles: ['example/build.excerpt.yaml']); + extraFiles: [kReadmeExcerptConfigPath]); const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; processRunner.mockProcessesForExecutable['git'] = [ @@ -258,7 +258,7 @@ void main() { test('fails if git ls-files fails', () async { createFakePlugin('a_plugin', packagesDir, - extraFiles: ['example/build.excerpt.yaml']); + extraFiles: [kReadmeExcerptConfigPath]); processRunner.mockProcessesForExecutable['git'] = [ MockProcess(exitCode: 1) diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 2754f64dde1..a8cb527d923 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -23,6 +23,13 @@ import 'mocks.dart'; export 'package:flutter_plugin_tools/src/common/repository_package.dart'; +/// The relative path from a package to the file that is used to enable +/// README excerpting for a package. +// This is a shared constant to ensure that both readme-check and +// update-excerpt are looking for the same file, so that readme-check can't +// get out of sync with what actually drives excerpting. +const String kReadmeExcerptConfigPath = 'example/build.excerpt.yaml'; + const String _defaultDartConstraint = '>=2.14.0 <3.0.0'; const String _defaultFlutterConstraint = '>=2.5.0'; From 33b29cd7a017b42dc14dc289ce9da652b586d588 Mon Sep 17 00:00:00 2001 From: godofredoc Date: Tue, 30 Aug 2022 15:54:04 -0700 Subject: [PATCH 211/249] Upload firebase test result to a buket in flutter-cirrus. (#6337) --- .../tool/lib/src/firebase_test_lab_command.dart | 2 +- .../test/firebase_test_lab_command_test.dart | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 4fc19e30014..a1128441190 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -59,7 +59,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { help: 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); argParser.addOption('results-bucket', - defaultsTo: 'gs://flutter_firebase_testlab_staging'); + defaultsTo: 'gs://flutter_cirrus_testlab'); argParser.addOption( kEnableExperiment, defaultsTo: '', diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 1ab7055d287..2d3175e171e 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -174,7 +174,7 @@ public class MainActivityTest { '/packages/plugin1/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin1/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin1/example'), ProcessCall( @@ -188,7 +188,7 @@ public class MainActivityTest { '/packages/plugin2/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin2/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin2/example'), ]), @@ -255,7 +255,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -265,7 +265,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example/1/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/1/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -320,7 +320,7 @@ public class MainActivityTest { '/packages/plugin/example/example1/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example1/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example1/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example/example1'), ProcessCall( @@ -330,7 +330,7 @@ public class MainActivityTest { '/packages/plugin/example/example2/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example2/0/ --device model=redfin,version=30 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example2/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example/example2'), ]), @@ -613,7 +613,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), @@ -785,7 +785,7 @@ public class MainActivityTest { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab_staging --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), From e353d4a284d83ade6cf8278da87d5287b9ea1b79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Aug 2022 23:18:32 +0000 Subject: [PATCH 212/249] [webview]: Bump webkit from 1.0.0 to 1.5.0 in /packages/webview_flutter/webview_flutter_android/android (#6325) --- script/tool/CHANGELOG.md | 4 ++++ script/tool/lib/src/create_all_plugins_app_command.dart | 4 ++-- script/tool/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 595943777c6..ebfbe80b018 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.3 + +* Raises minimum `compileSdkVersion` to 32 for the `all-plugins-app` command. + ## 0.9.2 * Adds checking of `code-excerpt` configuration to `readme-check`, to validate 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 595779b8be6..7bf007ac72a 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -101,8 +101,8 @@ class CreateAllPluginsAppCommand extends PluginCommand { // minSdkVersion 19 is required by WebView. newGradle.writeln('minSdkVersion 20'); } else if (line.contains('compileSdkVersion')) { - // compileSdkVersion 31 is required by Camera. - newGradle.writeln('compileSdkVersion 31'); + // compileSdkVersion 32 is required by webview_flutter. + newGradle.writeln('compileSdkVersion 32'); } else { newGradle.writeln(line); } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 3c25ef62b20..b1d55e0fe55 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.9.2 +version: 0.9.3 dependencies: args: ^2.1.0 From edcd2662e6f26157b6a87ff837a9829d6917db11 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 2 Sep 2022 06:22:13 -0700 Subject: [PATCH 213/249] Add missing submodule command to setup for packages repo. (#6276) --- script/tool/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/script/tool/README.md b/script/tool/README.md index 9b91b03eada..0e44bf16c26 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -17,6 +17,12 @@ flutter/packages.) The commands in tools require the Flutter-bundled version of Dart to be the first `dart` loaded in the path. +### Extra Setup + +When updating sample code excerpts (`update-excerpts`) for the README.md files, +there is some [extra setup for +submodules](#update-readmemd-from-example-sources) that is necessary. + ### From Source (flutter/plugins only) Set up: From 1aa2e82b56a3587063798bfc26abdda9a861c66b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 9 Sep 2022 13:52:16 -0400 Subject: [PATCH 214/249] [tools] Improves version-check logic (#6354) Improves the logic used to determine whether to require a version and/or CHANGELOG change: - Removes the requirement that dev-only (e.g., test) changes update the CHANGELOG, since in practice we were essentially always overriding in that case. - Adds file-level analysis of `build.gradle` files to determine whether they are only changing test dependencies. - Improves the "is this a published example file" logic to better match pub.dev's logic, to fix some false positives and false negatives (e.g., `rfw`'s `example//lib/main.dart` being considered published). Removes the no-longer-necessary special-case handling of some Dependabot PRs, as well as the PR-description-based system it was built on (and that turned out not to be very useful due to the way `CIRRUS_CHANGE_MESSAGE` actually worked). `build.gradle` analysis should not cover all such cases, and without the need to hard-code them by package name. --- script/tool/CHANGELOG.md | 10 + .../lib/src/common/git_version_finder.dart | 21 ++ .../lib/src/common/package_state_utils.dart | 153 +++++++-- .../lib/src/update_release_info_command.dart | 2 +- .../tool/lib/src/version_check_command.dart | 94 +---- script/tool/pubspec.yaml | 2 +- .../test/common/package_state_utils_test.dart | 214 +++++++++++- .../tool/test/version_check_command_test.dart | 323 ++---------------- 8 files changed, 403 insertions(+), 416 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index ebfbe80b018..8dbb4840a67 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.10.0 + +* Improves the logic in `version-check` to determine what changes don't require + version changes, as well as making any dev-only changes also not require + changelog changes since in practice we almost always override the check in + that case. +* Removes special-case handling of Dependabot PRs, and the (fragile) + `--change-description-file` flag was only still used for that case, as + the improved diff analysis now handles that case more robustly. + ## 0.9.3 * Raises minimum `compileSdkVersion` to 32 for the `all-plugins-app` command. diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index 32d30e60feb..eb8fba6b76b 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -50,6 +50,27 @@ class GitVersionFinder { return changedFiles.toList(); } + /// Get a list of all the changed files. + Future> getDiffContents({ + String? targetPath, + bool includeUncommitted = false, + }) async { + final String baseSha = await getBaseSha(); + final io.ProcessResult diffCommand = await baseGitDir.runCommand([ + 'diff', + baseSha, + if (!includeUncommitted) 'HEAD', + if (targetPath != null) ...['--', targetPath], + ]); + final String diffStdout = diffCommand.stdout.toString(); + if (diffStdout.isEmpty) { + return []; + } + final List changedFiles = diffStdout.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, diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index a03d643bdab..1cff65bb6b0 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.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. +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; @@ -14,6 +16,7 @@ class PackageChangeState { const PackageChangeState({ required this.hasChanges, required this.hasChangelogChange, + required this.needsChangelogChange, required this.needsVersionChange, }); @@ -26,6 +29,10 @@ class PackageChangeState { /// True if any changes in the package require a version change according /// to repository policy. final bool needsVersionChange; + + /// True if any changes in the package require a CHANGELOG change according + /// to repository policy. + final bool needsChangelogChange; } /// Checks [package] against [changedPaths] to determine what changes it has @@ -38,11 +45,15 @@ class PackageChangeState { /// and `getRelativePosixPath(package.directory, gitDir.path)` respectively; /// they are arguments mainly to allow for caching the changed paths for an /// entire command run. -PackageChangeState checkPackageChangeState( +/// +/// If [git] is provided, [changedPaths] must be repository-relative +/// paths, and change type detection can use file diffs in addition to paths. +Future checkPackageChangeState( RepositoryPackage package, { required List changedPaths, required String relativePackagePath, -}) { + GitVersionFinder? git, +}) async { final String packagePrefix = relativePackagePath.endsWith('/') ? relativePackagePath : '$relativePackagePath/'; @@ -50,6 +61,7 @@ PackageChangeState checkPackageChangeState( bool hasChanges = false; bool hasChangelogChange = false; bool needsVersionChange = false; + bool needsChangelogChange = false; for (final String path in changedPaths) { // Only consider files within the package. if (!path.startsWith(packagePrefix)) { @@ -62,34 +74,131 @@ PackageChangeState checkPackageChangeState( if (components.isEmpty) { continue; } - final bool isChangelog = components.first == 'CHANGELOG.md'; - if (isChangelog) { + + if (components.first == 'CHANGELOG.md') { hasChangelogChange = true; + continue; } - if (!needsVersionChange && - !isChangelog && - // One of a few special files example will be shown on pub.dev, but for - // anything else in the example publishing has no purpose. - !(components.first == 'example' && - !{'main.dart', 'readme.md', 'example.md'} - .contains(components.last.toLowerCase())) && - // Changes to tests don't need to be published. - !components.contains('test') && - !components.contains('androidTest') && - !components.contains('RunnerTests') && - !components.contains('RunnerUITests') && - // The top-level "tool" directory is for non-client-facing utility code, - // so doesn't need to be published. - components.first != 'tool' && - // Ignoring lints doesn't affect clients. - !components.contains('lint-baseline.xml')) { - needsVersionChange = true; + if (!needsVersionChange) { + // Developer-only changes don't need version changes or changelog changes. + if (await _isDevChange(components, git: git, repoPath: path)) { + continue; + } + + // Some other changes don't need version changes, but might benefit from + // changelog changes. + needsChangelogChange = true; + if ( + // One of a few special files example will be shown on pub.dev, but + // for anything else in the example publishing has no purpose. + !_isUnpublishedExampleChange(components, package)) { + needsVersionChange = true; + } } } return PackageChangeState( hasChanges: hasChanges, hasChangelogChange: hasChangelogChange, + needsChangelogChange: needsChangelogChange, needsVersionChange: needsVersionChange); } + +bool _isTestChange(List pathComponents) { + return pathComponents.contains('test') || + pathComponents.contains('androidTest') || + pathComponents.contains('RunnerTests') || + pathComponents.contains('RunnerUITests'); +} + +// True if the given file is an example file other than the one that will be +// published according to https://dart.dev/tools/pub/package-layout#examples. +// +// This is not exhastive; it currently only handles variations we actually have +// in our repositories. +bool _isUnpublishedExampleChange( + List pathComponents, RepositoryPackage package) { + if (pathComponents.first != 'example') { + return false; + } + final List exampleComponents = pathComponents.sublist(1); + if (exampleComponents.isEmpty) { + return false; + } + + final Directory exampleDirectory = + package.directory.childDirectory('example'); + + // Check for example.md/EXAMPLE.md first, as that has priority. If it's + // present, any other example file is unpublished. + final bool hasExampleMd = + exampleDirectory.childFile('example.md').existsSync() || + exampleDirectory.childFile('EXAMPLE.md').existsSync(); + if (hasExampleMd) { + return !(exampleComponents.length == 1 && + exampleComponents.first.toLowerCase() == 'example.md'); + } + + // Most packages have an example/lib/main.dart (or occasionally + // example/main.dart), so check for that. The other naming variations aren't + // currently used. + const String mainName = 'main.dart'; + final bool hasExampleCode = + exampleDirectory.childDirectory('lib').childFile(mainName).existsSync() || + exampleDirectory.childFile(mainName).existsSync(); + if (hasExampleCode) { + // If there is an example main, only that example file is published. + return !((exampleComponents.length == 1 && + exampleComponents.first == mainName) || + (exampleComponents.length == 2 && + exampleComponents.first == 'lib' && + exampleComponents[1] == mainName)); + } + + // If there's no example code either, the example README.md, if any, is the + // file that will be published. + return exampleComponents.first.toLowerCase() != 'readme.md'; +} + +// True if the change is only relevant to people working on the plugin. +Future _isDevChange(List pathComponents, + {GitVersionFinder? git, String? repoPath}) async { + return _isTestChange(pathComponents) || + // The top-level "tool" directory is for non-client-facing utility + // code, such as test scripts. + pathComponents.first == 'tool' || + // Ignoring lints doesn't affect clients. + pathComponents.contains('lint-baseline.xml') || + await _isGradleTestDependencyChange(pathComponents, + git: git, repoPath: repoPath); +} + +Future _isGradleTestDependencyChange(List pathComponents, + {GitVersionFinder? git, String? repoPath}) async { + if (git == null) { + return false; + } + if (pathComponents.last != 'build.gradle') { + return false; + } + final List diff = await git.getDiffContents(targetPath: repoPath); + final RegExp changeLine = RegExp(r'[+-] '); + final RegExp testDependencyLine = + RegExp(r'[+-]\s*(?:androidT|t)estImplementation\s'); + bool foundTestDependencyChange = false; + for (final String line in diff) { + if (!changeLine.hasMatch(line) || + line.startsWith('--- ') || + line.startsWith('+++ ')) { + continue; + } + if (!testDependencyLine.hasMatch(line)) { + return false; + } + foundTestDependencyChange = true; + } + // Only return true if a test dependency change was found, as a failsafe + // against having the wrong (e.g., incorrectly empty) diff output. + return foundTestDependencyChange; +} diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart index b998615ead1..465b475eb9b 100644 --- a/script/tool/lib/src/update_release_info_command.dart +++ b/script/tool/lib/src/update_release_info_command.dart @@ -133,7 +133,7 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand { packagesDir.fileSystem.directory((await gitDir).path); final String relativePackagePath = getRelativePosixPath(package.directory, from: gitRoot); - final PackageChangeState state = checkPackageChangeState(package, + final PackageChangeState state = await checkPackageChangeState(package, changedPaths: _changedFiles, relativePackagePath: relativePackagePath); diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 2e5f1efd793..235611492b2 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -18,8 +18,6 @@ import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; import 'common/repository_package.dart'; -const int _exitMissingChangeDescriptionFile = 3; - /// Categories of version change types. enum NextVersionType { /// A breaking change. @@ -116,11 +114,6 @@ class VersionCheckCommand extends PackageLoopingCommand { 'Defaults to false, which means the version check only run against ' 'the previous version in code.', ); - argParser.addOption(_changeDescriptionFile, - help: 'The path to a file containing the description of the change ' - '(e.g., PR description or commit message).\n\n' - 'If supplied, this is used to allow overrides to some version ' - 'checks.'); argParser.addOption(_prLabelsArg, help: 'A comma-separated list of labels associated with this PR, ' 'if applicable.\n\n' @@ -144,7 +137,6 @@ class VersionCheckCommand extends PackageLoopingCommand { } static const String _againstPubFlag = 'against-pub'; - static const String _changeDescriptionFile = 'change-description-file'; static const String _prLabelsArg = 'pr-labels'; static const String _checkForMissingChanges = 'check-for-missing-changes'; static const String _ignorePlatformInterfaceBreaks = @@ -171,7 +163,6 @@ class VersionCheckCommand extends PackageLoopingCommand { late final String _mergeBase; late final List _changedFiles; - late final String _changeDescription = _loadChangeDescription(); late final Set _prLabels = _getPRLabels(); @override @@ -519,21 +510,6 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return labels.split(',').map((String label) => label.trim()).toSet(); } - /// Returns the contents of the file pointed to by [_changeDescriptionFile], - /// or an empty string if that flag is not provided. - String _loadChangeDescription() { - final String path = getStringArg(_changeDescriptionFile); - if (path.isEmpty) { - return ''; - } - final File file = packagesDir.fileSystem.file(path); - if (!file.existsSync()) { - printError('${indentation}No such file: $path'); - throw ToolExit(_exitMissingChangeDescriptionFile); - } - return file.readAsStringSync(); - } - /// Returns true if the given version transition should be allowed. bool _shouldAllowVersionChange( {required Version oldVersion, required Version newVersion}) { @@ -569,8 +545,10 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. final String relativePackagePath = getRelativePosixPath(package.directory, from: gitRoot); - final PackageChangeState state = checkPackageChangeState(package, - changedPaths: _changedFiles, relativePackagePath: relativePackagePath); + final PackageChangeState state = await checkPackageChangeState(package, + changedPaths: _changedFiles, + relativePackagePath: relativePackagePath, + git: await retrieveVersionFinder()); if (!state.hasChanges) { return null; @@ -580,9 +558,6 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. if (_prLabels.contains(_missingVersionChangeOverrideLabel)) { logWarning('Ignoring lack of version change due to the ' '"$_missingVersionChangeOverrideLabel" label.'); - } else if (_isAllowedDependabotChange(package, _changeDescription)) { - logWarning('Ignoring lack of version change for Dependabot change to ' - 'a known internal dependency.'); } else { printError( 'No version change found, but the change to this package could ' @@ -595,76 +570,23 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. } } - if (!state.hasChangelogChange) { + if (!state.hasChangelogChange && state.needsChangelogChange) { if (_prLabels.contains(_missingChangelogChangeOverrideLabel)) { logWarning('Ignoring lack of CHANGELOG update due to the ' '"$_missingChangelogChangeOverrideLabel" label.'); - } else if (_isAllowedDependabotChange(package, _changeDescription)) { - logWarning('Ignoring lack of CHANGELOG update for Dependabot change to ' - 'a known internal dependency.'); } else { printError( 'No CHANGELOG change found. If this PR needs an exemption from ' 'the standard policy of listing all changes in the CHANGELOG, ' 'comment in the PR to explain why the PR is exempt, and add (or ' 'ask your reviewer to add) the ' - '"$_missingChangelogChangeOverrideLabel" label.'); + '"$_missingChangelogChangeOverrideLabel" label. Otherwise, ' + 'please add a NEXT entry in the CHANGELOG as described in ' + 'the contributing guide.'); return 'Missing CHANGELOG change'; } } return null; } - - /// Returns true if [changeDescription] matches a Dependabot change for a - /// dependency roll that should bypass the normal version and CHANGELOG change - /// checks (for dependencies that are known not to have client impact). - /// - /// Depending on CI, [changeDescription] may either be the PR description, or - /// the description of the last commit (see for example discussion in - /// https://github.com/cirruslabs/cirrus-ci-docs/issues/1029), so this needs - /// to handle both. - bool _isAllowedDependabotChange( - RepositoryPackage package, String changeDescription) { - // Espresso exports some dependencies that are normally just internal test - // utils, so always require reviewers to check that. - if (package.directory.basename == 'espresso') { - return false; - } - - // A string that is in all Dependabot PRs, but extremely unlikely to be in - // any other PR, to identify Dependabot PRs. - const String dependabotPRDescriptionMarker = - 'Dependabot commands and options'; - // The same thing, but for the Dependabot commit message. - const String dependabotCommitMessageMarker = - 'Signed-off-by: dependabot[bot]'; - // Expression to extract the name of the dependency being updated. - final RegExp dependencyRegex = - RegExp(r'Bumps? \[(.*?)\]\(.*?\) from [\d.]+ to [\d.]+'); - - // Allowed exact dependency names. - const Set allowedDependencies = { - 'junit', - 'robolectric', - }; - const Set allowedDependencyPrefixes = { - 'mockito-' // mockito-core, mockito-inline, etc. - }; - - if (changeDescription.contains(dependabotPRDescriptionMarker) || - changeDescription.contains(dependabotCommitMessageMarker)) { - final Match? match = dependencyRegex.firstMatch(changeDescription); - if (match != null) { - final String dependency = match.group(1)!; - if (allowedDependencies.contains(dependency) || - allowedDependencyPrefixes - .any((String prefix) => dependency.startsWith(prefix))) { - return true; - } - } - } - - return false; - } } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index b1d55e0fe55..e51c7433aa5 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.9.3 +version: 0.10.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart index cc9116a9ea2..63ac1802e70 100644 --- a/script/tool/test/common/package_state_utils_test.dart +++ b/script/tool/test/common/package_state_utils_test.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; import 'package:flutter_plugin_tools/src/common/package_state_utils.dart'; +import 'package:test/fake.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -26,12 +28,13 @@ void main() { 'packages/a_package/lib/plugin.dart', ]; - final PackageChangeState state = checkPackageChangeState(package, + final PackageChangeState state = await checkPackageChangeState(package, changedPaths: changedFiles, relativePackagePath: 'packages/a_package'); expect(state.hasChanges, true); expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); }); test('handles trailing slash on package path', () async { @@ -42,16 +45,18 @@ void main() { 'packages/a_package/lib/plugin.dart', ]; - final PackageChangeState state = checkPackageChangeState(package, + final PackageChangeState state = await checkPackageChangeState(package, changedPaths: changedFiles, relativePackagePath: 'packages/a_package/'); expect(state.hasChanges, true); expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); expect(state.hasChangelogChange, false); }); - test('does not report version change exempt changes', () async { + test('does not flag version- and changelog-change-exempt changes', + () async { final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir); @@ -64,12 +69,13 @@ void main() { 'packages/a_plugin/CHANGELOG.md', ]; - final PackageChangeState state = checkPackageChangeState(package, + final PackageChangeState state = await checkPackageChangeState(package, changedPaths: changedFiles, relativePackagePath: 'packages/a_plugin/'); expect(state.hasChanges, true); expect(state.needsVersionChange, false); + expect(state.needsChangelogChange, false); expect(state.hasChangelogChange, true); }); @@ -81,28 +87,49 @@ void main() { 'packages/a_plugin/lib/foo/tool/tool_thing.dart', ]; - final PackageChangeState state = checkPackageChangeState(package, + final PackageChangeState state = await checkPackageChangeState(package, changedPaths: changedFiles, relativePackagePath: 'packages/a_plugin/'); expect(state.hasChanges, true); expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); }); - test('requires a version change for example main', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); + test('requires a version change for example/lib/main.dart', () async { + final RepositoryPackage package = createFakePlugin( + 'a_plugin', packagesDir, + extraFiles: ['example/lib/main.dart']); const List changedFiles = [ 'packages/a_plugin/example/lib/main.dart', ]; - final PackageChangeState state = checkPackageChangeState(package, + final PackageChangeState state = await checkPackageChangeState(package, changedPaths: changedFiles, relativePackagePath: 'packages/a_plugin/'); expect(state.hasChanges, true); expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); + }); + + test('requires a version change for example/main.dart', () async { + final RepositoryPackage package = createFakePlugin( + 'a_plugin', packagesDir, + extraFiles: ['example/main.dart']); + + const List changedFiles = [ + 'packages/a_plugin/example/main.dart', + ]; + + final PackageChangeState state = await checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); }); test('requires a version change for example readme.md', () async { @@ -113,28 +140,189 @@ void main() { 'packages/a_plugin/example/README.md', ]; - final PackageChangeState state = checkPackageChangeState(package, + final PackageChangeState state = await checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); + }); + + test('requires a version change for example/example.md', () async { + final RepositoryPackage package = createFakePlugin( + 'a_plugin', packagesDir, + extraFiles: ['example/example.md']); + + const List changedFiles = [ + 'packages/a_plugin/example/example.md', + ]; + + final PackageChangeState state = await checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); + }); + + test( + 'requires a changelog change but no version change for ' + 'lower-priority examples when example.md is present', () async { + final RepositoryPackage package = createFakePlugin( + 'a_plugin', packagesDir, + extraFiles: ['example/example.md']); + + const List changedFiles = [ + 'packages/a_plugin/example/lib/main.dart', + 'packages/a_plugin/example/main.dart', + 'packages/a_plugin/example/README.md', + ]; + + final PackageChangeState state = await checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, false); + expect(state.needsChangelogChange, true); + }); + + test( + 'requires a changelog change but no version change for README.md when ' + 'code example is present', () async { + final RepositoryPackage package = createFakePlugin( + 'a_plugin', packagesDir, + extraFiles: ['example/lib/main.dart']); + + const List changedFiles = [ + 'packages/a_plugin/example/README.md', + ]; + + final PackageChangeState state = await checkPackageChangeState(package, changedPaths: changedFiles, relativePackagePath: 'packages/a_plugin/'); + expect(state.hasChanges, true); + expect(state.needsVersionChange, false); + expect(state.needsChangelogChange, true); + }); + + test( + 'does not requires changelog or version change for build.gradle ' + 'test-dependency-only changes', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/android/build.gradle', + ]; + + final GitVersionFinder git = FakeGitVersionFinder(>{ + 'packages/a_plugin/android/build.gradle': [ + "- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'", + "- testImplementation 'junit:junit:4.10.0'", + "+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'", + "+ testImplementation 'junit:junit:4.13.2'", + ] + }); + + final PackageChangeState state = await checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/', + git: git); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, false); + expect(state.needsChangelogChange, false); + }); + + test('requires changelog or version change for other build.gradle changes', + () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/android/build.gradle', + ]; + + final GitVersionFinder git = FakeGitVersionFinder(>{ + 'packages/a_plugin/android/build.gradle': [ + "- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'", + "- testImplementation 'junit:junit:4.10.0'", + "+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'", + "+ testImplementation 'junit:junit:4.13.2'", + "- implementation 'com.google.android.gms:play-services-maps:18.0.0'", + "+ implementation 'com.google.android.gms:play-services-maps:18.0.2'", + ] + }); + + final PackageChangeState state = await checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/', + git: git); + expect(state.hasChanges, true); expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); }); - test('requires a version change for example example.md', () async { + test( + 'requires changelog or version change if build.gradle diffs cannot ' + 'be checked', () async { final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir); const List changedFiles = [ - 'packages/a_plugin/example/lib/example.md', + 'packages/a_plugin/android/build.gradle', ]; - final PackageChangeState state = checkPackageChangeState(package, + final PackageChangeState state = await checkPackageChangeState(package, changedPaths: changedFiles, relativePackagePath: 'packages/a_plugin/'); expect(state.hasChanges, true); expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); + }); + + test( + 'requires changelog or version change if build.gradle diffs cannot ' + 'be determined', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/android/build.gradle', + ]; + + final GitVersionFinder git = FakeGitVersionFinder(>{ + 'packages/a_plugin/android/build.gradle': [] + }); + + final PackageChangeState state = await checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/', + git: git); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + expect(state.needsChangelogChange, true); }); }); } + +class FakeGitVersionFinder extends Fake implements GitVersionFinder { + FakeGitVersionFinder(this.fileDiffs); + + final Map> fileDiffs; + + @override + Future> getDiffContents({ + String? targetPath, + bool includeUncommitted = false, + }) async { + return fileDiffs[targetPath]!; + } +} diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index f8153750122..2ff48d61825 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -40,72 +40,6 @@ void testAllowedVersion( } } -String _generateFakeDependabotPRDescription(String package) { - return ''' -Bumps [$package](https://github.com/foo/$package) from 1.0.0 to 2.0.0. -
-Release notes -

Sourced from $package's releases.

-
-... -
-
-
-Commits -
    -
  • ...
  • -
-
-
- - -[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=$package&package-manager=gradle&previous-version=1.0.0&new-version=2.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) - -Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. - -[//]: # (dependabot-automerge-start) -[//]: # (dependabot-automerge-end) - ---- - -
-Dependabot commands and options -
- -You can trigger Dependabot actions by commenting on this PR: -- `@dependabot rebase` will rebase this PR -- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it -- `@dependabot merge` will merge this PR after your CI passes on it -- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it -- `@dependabot cancel merge` will cancel a previously requested merge and block automerging -- `@dependabot reopen` will reopen this PR if it is closed -- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually -- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) -- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) -- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - - -
-'''; -} - -String _generateFakeDependabotCommitMessage(String package) { - return ''' -Bumps [$package](https://github.com/foo/$package) from 1.0.0 to 2.0.0. -- [Release notes](https://github.com/foo/$package/releases) -- [Commits](foo/$package@v4.3.1...v4.6.1) - ---- -updated-dependencies: -- dependency-name: $package - dependency-type: direct:production - update-type: version-update:semver-minor -... - -Signed-off-by: dependabot[bot] -'''; -} - class MockProcessResult extends Mock implements io.ProcessResult {} void main() { @@ -1085,242 +1019,45 @@ packages/plugin/example/lib/foo.dart ); }); - group('dependabot', () { - test('throws if a nonexistent change description file is specified', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/android/build.gradle -'''), - ]; - - Error? commandError; - final List output = await _runWithMissingChangeDetection( - ['--change-description-file=a_missing_file.txt'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No such file: a_missing_file.txt'), - ]), - ); - }); - - test('allows missing version and CHANGELOG change for mockito', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/android/build.gradle -'''), - ]; - - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync( - _generateFakeDependabotPRDescription('mockito-core')); - - final List output = - await _runWithMissingChangeDetection([ - '--change-description-file=${changeDescriptionFile.path}' - ]); - - expect( - output, - containsAllInOrder([ - contains('Ignoring lack of version change for Dependabot ' - 'change to a known internal dependency.'), - contains('Ignoring lack of CHANGELOG update for Dependabot ' - 'change to a known internal dependency.'), - ]), - ); - }); - - test('allows missing version and CHANGELOG change for robolectric', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/android/build.gradle -'''), - ]; - - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync( - _generateFakeDependabotPRDescription('robolectric')); - - final List output = - await _runWithMissingChangeDetection([ - '--change-description-file=${changeDescriptionFile.path}' - ]); - - expect( - output, - containsAllInOrder([ - contains('Ignoring lack of version change for Dependabot ' - 'change to a known internal dependency.'), - contains('Ignoring lack of CHANGELOG update for Dependabot ' - 'change to a known internal dependency.'), - ]), - ); - }); - - test('allows missing version and CHANGELOG change for junit', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/android/build.gradle -'''), - ]; - - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile - .writeAsStringSync(_generateFakeDependabotPRDescription('junit')); - - final List output = - await _runWithMissingChangeDetection([ - '--change-description-file=${changeDescriptionFile.path}' - ]); - - expect( - output, - containsAllInOrder([ - contains('Ignoring lack of version change for Dependabot ' - 'change to a known internal dependency.'), - contains('Ignoring lack of CHANGELOG update for Dependabot ' - 'change to a known internal dependency.'), - ]), - ); - }); - - test('fails for dependencies that are not explicitly allowed', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); + // This test ensures that Dependabot Gradle changes to test-only files + // aren't flagged by the version check. + test( + 'allows missing CHANGELOG and version change for test-only Gradle changes', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); - const String changelog = ''' + const String changelog = ''' ## 1.0.0 * Some changes. '''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + // File list. + MockProcess(stdout: ''' packages/plugin/android/build.gradle '''), - ]; - - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync( - _generateFakeDependabotPRDescription('somethingelse')); - - Error? commandError; - final List output = - await _runWithMissingChangeDetection([ - '--change-description-file=${changeDescriptionFile.path}' - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No version change found'), - contains('plugin:\n' - ' Missing version change'), - ]), - ); - }); - - test('allow list works for commit messages', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/android/build.gradle + // build.gradle diff + MockProcess(stdout: ''' +- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +- testImplementation 'junit:junit:4.10.0' ++ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' ++ testImplementation 'junit:junit:4.13.2' '''), - ]; - - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync( - _generateFakeDependabotCommitMessage('mockito-core')); + ]; - final List output = - await _runWithMissingChangeDetection([ - '--change-description-file=${changeDescriptionFile.path}' - ]); + final List output = + await _runWithMissingChangeDetection([]); - expect( - output, - containsAllInOrder([ - contains('Ignoring lack of version change for Dependabot ' - 'change to a known internal dependency.'), - contains('Ignoring lack of CHANGELOG update for Dependabot ' - 'change to a known internal dependency.'), - ]), - ); - }); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); }); }); From 5a069fe5b3e92c4ebdf550980ae11fe2a2421136 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 22 Sep 2022 12:11:56 -0400 Subject: [PATCH 215/249] [tools] Require implementation package README warning (#6459) --- script/tool/CHANGELOG.md | 6 + script/tool/lib/src/readme_check_command.dart | 66 +++++++- .../tool/test/readme_check_command_test.dart | 141 ++++++++++++++++++ 3 files changed, 205 insertions(+), 8 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 8dbb4840a67..f6dfb0e5826 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## NEXT + +* Adds `readme-check` validation that the example/README.md for a federated + plugin's implementation packages has a warning about the intended use of the + example instead of the template boilerplate. + ## 0.10.0 * Improves the logic in `version-check` to determine what changes don't require diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index 4acf997a008..e3fbc7bc454 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -104,11 +104,8 @@ class ReadmeCheckCommand extends PackageLoopingCommand { errors.add(blockValidationError); } - if (_containsTemplateBoilerplate(readmeLines)) { - printError('${indentation}The boilerplate section about getting started ' - 'with Flutter should not be left in.'); - errors.add('Contains template boilerplate'); - } + errors.addAll(_validateBoilerplate(readmeLines, + mainPackage: mainPackage, isExample: isExample)); // Check if this is the main readme for a plugin, and if so enforce extra // checks. @@ -284,10 +281,63 @@ ${indentation * 2}Please use standard capitalizations: ${sortedListString(expect return null; } - /// Returns true if the README still has the boilerplate from the - /// `flutter create` templates. - bool _containsTemplateBoilerplate(List readmeLines) { + /// Validates [readmeLines], outputing error messages for any issue and + /// returning an array of error summaries (if any). + /// + /// Returns an empty array if validation passes. + List _validateBoilerplate( + List readmeLines, { + required RepositoryPackage mainPackage, + required bool isExample, + }) { + final List errors = []; + + if (_containsTemplateFlutterBoilerplate(readmeLines)) { + printError('${indentation}The boilerplate section about getting started ' + 'with Flutter should not be left in.'); + errors.add('Contains template boilerplate'); + } + + // Enforce a repository-standard message in implementation plugin examples, + // since they aren't typical examples, which has been a source of + // confusion for plugin clients who find them. + if (isExample && mainPackage.isPlatformImplementation) { + if (_containsExampleBoilerplate(readmeLines)) { + printError('${indentation}The boilerplate should not be left in for a ' + "federated plugin implementation package's example."); + errors.add('Contains template boilerplate'); + } + if (!_containsImplementationExampleExplanation(readmeLines)) { + printError('${indentation}The example README for a platform ' + 'implementation package should warn readers about its intended ' + 'use. Please copy the example README from another implementation ' + 'package in this repository.'); + errors.add('Missing implementation package example warning'); + } + } + + return errors; + } + + /// Returns true if the README still has unwanted parts of the boilerplate + /// from the `flutter create` templates. + bool _containsTemplateFlutterBoilerplate(List readmeLines) { return readmeLines.any((String line) => line.contains('For help getting started with Flutter')); } + + /// Returns true if the README still has the generic description of an + /// example from the `flutter create` templates. + bool _containsExampleBoilerplate(List readmeLines) { + return readmeLines + .any((String line) => line.contains('Demonstrates how to use the')); + } + + /// Returns true if the README contains the repository-standard explanation of + /// the purpose of a federated plugin implementation's example. + bool _containsImplementationExampleExplanation(List readmeLines) { + return readmeLines.contains('# Platform Implementation Test App') && + readmeLines + .any((String line) => line.contains('This is a test app for')); + } } diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart index 37224fddc56..eb2b6c8e751 100644 --- a/script/tool/test/readme_check_command_test.dart +++ b/script/tool/test/readme_check_command_test.dart @@ -176,6 +176,147 @@ samples, guidance on mobile development, and a full API reference. ); }); + test( + 'fails when a plugin implementation package example README has the ' + 'template boilerplate', () async { + final RepositoryPackage package = createFakePlugin( + 'a_plugin_ios', packagesDir.childDirectory('a_plugin')); + package.getExamples().first.readmeFile.writeAsStringSync(''' +# a_plugin_ios_example + +Demonstrates how to use the a_plugin_ios plugin. +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The boilerplate should not be left in for a federated plugin ' + "implementation package's example."), + contains('Contains template boilerplate'), + ]), + ); + }); + + test( + 'allows the template boilerplate in the example README for packages ' + 'other than plugin implementation packages', () async { + final RepositoryPackage package = createFakePlugin( + 'a_plugin', + packagesDir.childDirectory('a_plugin'), + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + }, + ); + // Write a README with an OS support table so that the main README check + // passes. + package.readmeFile.writeAsStringSync(''' +# a_plugin + +| | Android | +|----------------|---------| +| **Support** | SDK 19+ | + +A great plugin. +'''); + package.getExamples().first.readmeFile.writeAsStringSync(''' +# a_plugin_example + +Demonstrates how to use the a_plugin plugin. +'''); + + final List output = + await runCapturingPrint(runner, ['readme-check']); + + expect( + output, + containsAll([ + contains(' Checking README.md...'), + contains(' Checking example/README.md...'), + ]), + ); + }); + + test( + 'fails when a plugin implementation package example README does not have ' + 'the repo-standard message', () async { + final RepositoryPackage package = createFakePlugin( + 'a_plugin_ios', packagesDir.childDirectory('a_plugin')); + package.getExamples().first.readmeFile.writeAsStringSync(''' +# a_plugin_ios_example + +Some random description. +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The example README for a platform implementation package ' + 'should warn readers about its intended use. Please copy the ' + 'example README from another implementation package in this ' + 'repository.'), + contains('Missing implementation package example warning'), + ]), + ); + }); + + test('passes for a plugin implementation package with the expected content', + () async { + final RepositoryPackage package = createFakePlugin( + 'a_plugin', + packagesDir.childDirectory('a_plugin'), + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + }, + ); + // Write a README with an OS support table so that the main README check + // passes. + package.readmeFile.writeAsStringSync(''' +# a_plugin + +| | Android | +|----------------|---------| +| **Support** | SDK 19+ | + +A great plugin. +'''); + package.getExamples().first.readmeFile.writeAsStringSync(''' +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. +'''); + + final List output = + await runCapturingPrint(runner, ['readme-check']); + + expect( + output, + containsAll([ + contains(' Checking README.md...'), + contains(' Checking example/README.md...'), + ]), + ); + }); + test( 'fails when multi-example top-level example directory README still has ' 'application template boilerplate', () async { From ecaf5a954bbf296a88a516e7a38498887613972f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 23 Sep 2022 17:38:04 -0400 Subject: [PATCH 216/249] [tools] Add 'run_tests.sh' to the dev-only list (#6474) --- script/tool/CHANGELOG.md | 3 +- .../lib/src/common/package_state_utils.dart | 3 ++ script/tool/pubspec.yaml | 2 +- .../test/common/package_state_utils_test.dart | 1 + .../tool/test/version_check_command_test.dart | 32 +++++++++++++++++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index f6dfb0e5826..2417d0fa833 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.10.0+1 +* Recognizes `run_test.sh` as a developer-only file in `version-check`. * Adds `readme-check` validation that the example/README.md for a federated plugin's implementation packages has a warning about the intended use of the example instead of the template boilerplate. diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index 1cff65bb6b0..c9465876f29 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -168,6 +168,9 @@ Future _isDevChange(List pathComponents, // The top-level "tool" directory is for non-client-facing utility // code, such as test scripts. pathComponents.first == 'tool' || + // Entry point for the 'custom-test' command, which is only for CI and + // local testing. + pathComponents.first == 'run_tests.sh' || // Ignoring lints doesn't affect clients. pathComponents.contains('lint-baseline.xml') || await _isGradleTestDependencyChange(pathComponents, diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index e51c7433aa5..9b9d7328855 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.10.0 +version: 0.10.0+1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart index 63ac1802e70..c20951876e3 100644 --- a/script/tool/test/common/package_state_utils_test.dart +++ b/script/tool/test/common/package_state_utils_test.dart @@ -66,6 +66,7 @@ void main() { 'packages/a_plugin/example/ios/RunnerTests/Foo.m', 'packages/a_plugin/example/ios/RunnerUITests/info.plist', 'packages/a_plugin/tool/a_development_tool.dart', + 'packages/a_plugin/run_tests.sh', 'packages/a_plugin/CHANGELOG.md', ]; diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 2ff48d61825..0e94712e9ef 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -1059,6 +1059,38 @@ packages/plugin/android/build.gradle ]), ); }); + + test('allows missing CHANGELOG and version change for dev-only changes', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + // File list. + MockProcess(stdout: ''' +packages/plugin/tool/run_tests.dart +packages/plugin/run_tests.sh +'''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); }); test('allows valid against pub', () async { From 9974a8ef44e8682eab0b3142282972f6412b9fb1 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 27 Sep 2022 15:43:04 -0400 Subject: [PATCH 217/249] [tool] Clean up "plugin" references (#6503) --- script/tool/CHANGELOG.md | 8 +++ script/tool/README.md | 26 ++++---- ...ugin_command.dart => package_command.dart} | 62 ++++++++++--------- .../src/common/package_looping_command.dart | 4 +- .../lib/src/common/package_state_utils.dart | 2 +- .../src/create_all_plugins_app_command.dart | 4 +- .../tool/lib/src/drive_examples_command.dart | 2 +- script/tool/lib/src/format_command.dart | 4 +- .../tool/lib/src/license_check_command.dart | 4 +- script/tool/lib/src/list_command.dart | 14 ++--- script/tool/lib/src/main.dart | 4 +- .../lib/src/make_deps_path_based_command.dart | 6 +- .../tool/lib/src/publish_check_command.dart | 2 +- ...ugin_command.dart => publish_command.dart} | 10 +-- .../tool/lib/src/version_check_command.dart | 4 +- script/tool/pubspec.yaml | 2 +- script/tool/test/analyze_command_test.dart | 8 +-- .../test/common/git_version_finder_test.dart | 2 +- ...nd_test.dart => package_command_test.dart} | 20 +++--- ...s.dart => package_command_test.mocks.dart} | 0 .../common/package_looping_command_test.dart | 21 +++---- .../tool/test/custom_test_command_test.dart | 24 +++---- .../test/dependabot_check_command_test.dart | 2 +- .../federation_safety_check_command_test.dart | 2 +- script/tool/test/format_command_test.dart | 10 +-- .../tool/test/license_check_command_test.dart | 2 +- script/tool/test/list_command_test.dart | 20 +++--- .../make_deps_path_based_command_test.dart | 2 +- ...nd_test.dart => publish_command_test.dart} | 62 +++++++++---------- .../test/update_excerpts_command_test.dart | 2 +- .../update_release_info_command_test.dart | 2 +- .../tool/test/version_check_command_test.dart | 2 +- 32 files changed, 173 insertions(+), 166 deletions(-) rename script/tool/lib/src/common/{plugin_command.dart => package_command.dart} (92%) rename script/tool/lib/src/{publish_plugin_command.dart => publish_command.dart} (98%) rename script/tool/test/common/{plugin_command_test.dart => package_command_test.dart} (98%) rename script/tool/test/common/{plugin_command_test.mocks.dart => package_command_test.mocks.dart} (100%) rename script/tool/test/{publish_plugin_command_test.dart => publish_command_test.dart} (94%) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2417d0fa833..830d3ca931e 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.11 + +* Renames `publish-plugin` to `publish`. +* Renames arguments to `list`: + * `--package` now lists top-level packages (previously `--plugin`). + * `--package-or-subpackage` now lists top-level packages (previously + `--package`). + ## 0.10.0+1 * Recognizes `run_test.sh` as a developer-only file in `version-check`. diff --git a/script/tool/README.md b/script/tool/README.md index 0e44bf16c26..9f0ac84145f 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -54,7 +54,7 @@ dart pub global run flutter_plugin_tools ## Commands Run with `--help` for a full list of commands and arguments, but the -following shows a number of common commands being run for a specific plugin. +following shows a number of common commands being run for a specific package. All examples assume running from source; see above for running the published version instead. @@ -71,29 +71,29 @@ command is targetting. An package name can be any of: ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart format --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart format --packages package_name ``` ### Run the Dart Static Analyzer ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart analyze --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart analyze --packages package_name ``` ### Run Dart Unit Tests ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart test --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart test --packages package_name ``` ### Run Dart Integration Tests ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart build-examples --apk --packages plugin_name -dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --android --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart build-examples --apk --packages package_name +dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --android --packages package_name ``` Replace `--apk`/`--android` with the platform you want to test against @@ -110,11 +110,11 @@ Examples: ```sh cd # Run just unit tests for iOS and Android: -dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android --no-integration --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android --no-integration --packages package_name # Run all tests for macOS: -dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages package_name # Run all tests for Windows: -dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --windows --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --windows --packages package_name ``` ### Update README.md from Example Sources @@ -125,7 +125,7 @@ before running this command. ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages package_name ``` ### Update CHANGELOG and Version @@ -165,18 +165,18 @@ on the Flutter Wiki first. ```sh cd git checkout -dart run ./script/tool/bin/flutter_plugin_tools.dart publish-plugin --packages +dart run ./script/tool/bin/flutter_plugin_tools.dart publish --packages ``` By default the tool tries to push tags to the `upstream` remote, but some additional settings can be configured. Run `dart run ./script/tool/bin/flutter_plugin_tools.dart -publish-plugin --help` for more usage information. +publish --help` for more usage information. The tool wraps `pub publish` for pushing the package to pub, and then will automatically use git to try to create and push tags. It has some additional safety checking around `pub publish` too. By default `pub publish` publishes _everything_, including untracked or uncommitted files in version control. -`publish-plugin` will first check the status of the local +`publish` will first check the status of the local directory and refuse to publish if there are any mismatched files with version control present. diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/package_command.dart similarity index 92% rename from script/tool/lib/src/common/plugin_command.dart rename to script/tool/lib/src/common/package_command.dart index 843768fd4c6..60d7ecc8007 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -34,9 +34,9 @@ class PackageEnumerationEntry { /// Interface definition for all commands in this tool. // TODO(stuartmorgan): Move most of this logic to PackageLoopingCommand. -abstract class PluginCommand extends Command { +abstract class PackageCommand extends Command { /// Creates a command to operate on [packagesDir] with the given environment. - PluginCommand( + PackageCommand( this.packagesDir, { this.processRunner = const ProcessRunner(), this.platform = const LocalPlatform(), @@ -47,7 +47,7 @@ abstract class PluginCommand extends Command { help: 'Specifies which packages the command should run on (before sharding).\n', valueHelp: 'package1,package2,...', - aliases: [_pluginsArg], + aliases: [_pluginsLegacyAliasArg], ); argParser.addOption( _shardIndexArg, @@ -58,7 +58,7 @@ abstract class PluginCommand extends Command { ); argParser.addOption( _shardCountArg, - help: 'Specifies the number of shards into which plugins are divided.', + help: 'Specifies the number of shards into which packages are divided.', valueHelp: 'n', defaultsTo: '1', ); @@ -71,7 +71,7 @@ abstract class PluginCommand extends Command { defaultsTo: [], ); argParser.addFlag(_runOnChangedPackagesArg, - help: 'Run the command on changed packages/plugins.\n' + help: 'Run the command on changed packages.\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' 'Packages excluded with $_excludeArg are excluded even if changed.\n' @@ -106,13 +106,13 @@ abstract class PluginCommand extends Command { static const String _logTimingArg = 'log-timing'; static const String _packagesArg = 'packages'; static const String _packagesForBranchArg = 'packages-for-branch'; - static const String _pluginsArg = 'plugins'; + static const String _pluginsLegacyAliasArg = 'plugins'; static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; static const String _runOnDirtyPackagesArg = 'run-on-dirty-packages'; static const String _shardCountArg = 'shardCount'; static const String _shardIndexArg = 'shardIndex'; - /// The directory containing the plugin packages. + /// The directory containing the packages. final Directory packagesDir; /// The process runner. @@ -221,7 +221,7 @@ abstract class PluginCommand extends Command { _shardCount = shardCount; } - /// Returns the set of plugins to exclude based on the `--exclude` argument. + /// Returns the set of packages to exclude based on the `--exclude` argument. Set getExcludedPackageNames() { final Set excludedPackages = _excludedPackages ?? getStringListArg(_excludeArg).expand((String item) { @@ -250,22 +250,22 @@ abstract class PluginCommand extends Command { Stream getTargetPackages( {bool filterExcluded = true}) async* { // To avoid assuming consistency of `Directory.list` across command - // invocations, we collect and sort the plugin folders before sharding. + // invocations, we collect and sort the package folders before sharding. // This is considered an implementation detail which is why the API still // uses streams. - final List allPlugins = + final List allPackages = await _getAllPackages().toList(); - allPlugins.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => + allPackages.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => p1.package.path.compareTo(p2.package.path)); - 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 PackageEnumerationEntry plugin - in allPlugins.sublist(start, end)) { - if (!(filterExcluded && plugin.excluded)) { - yield plugin; + final int shardSize = allPackages.length ~/ shardCount + + (allPackages.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPackages.length); + final int end = min(start + shardSize, allPackages.length); + + for (final PackageEnumerationEntry package + in allPackages.sublist(start, end)) { + if (!(filterExcluded && package.excluded)) { + yield package; } } } @@ -330,7 +330,7 @@ abstract class PluginCommand extends Command { runOnChangedPackages = false; } - final Set excludedPluginNames = getExcludedPackageNames(); + final Set excludedPackageNames = getExcludedPackageNames(); if (runOnChangedPackages) { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); @@ -368,15 +368,16 @@ abstract class PluginCommand extends Command { ]) { await for (final FileSystemEntity entity in dir.list(followLinks: false)) { - // A top-level Dart package is a plugin package. + // A top-level Dart package is a standard package. if (isPackage(entity)) { if (packages.isEmpty || packages.contains(p.basename(entity.path))) { yield PackageEnumerationEntry( RepositoryPackage(entity as Directory), - excluded: excludedPluginNames.contains(entity.basename)); + excluded: excludedPackageNames.contains(entity.basename)); } } else if (entity is Directory) { - // Look for Dart packages under this top-level directory. + // Look for Dart packages under this top-level directory; this is the + // standard structure for federated plugins. await for (final FileSystemEntity subdir in entity.list(followLinks: false)) { if (isPackage(subdir)) { @@ -394,7 +395,7 @@ abstract class PluginCommand extends Command { packages.intersection(possibleMatches).isNotEmpty) { yield PackageEnumerationEntry( RepositoryPackage(subdir as Directory), - excluded: excludedPluginNames + excluded: excludedPackageNames .intersection(possibleMatches) .isNotEmpty); } @@ -415,11 +416,12 @@ abstract class PluginCommand extends Command { /// stream. Stream getTargetPackagesAndSubpackages( {bool filterExcluded = true}) async* { - await for (final PackageEnumerationEntry plugin + await for (final PackageEnumerationEntry package in getTargetPackages(filterExcluded: filterExcluded)) { - yield plugin; - yield* getSubpackages(plugin.package).map((RepositoryPackage package) => - PackageEnumerationEntry(package, excluded: plugin.excluded)); + yield package; + yield* getSubpackages(package.package).map( + (RepositoryPackage subPackage) => + PackageEnumerationEntry(subPackage, excluded: package.excluded)); } } @@ -524,7 +526,7 @@ abstract class PluginCommand extends Command { } // Returns true if one or more files changed that have the potential to affect - // any plugin (e.g., CI script changes). + // any packages (e.g., CI script changes). bool _changesRequireFullTest(List changedFiles) { const List specialFiles = [ '.ci.yaml', // LUCI config. diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 9e696e95619..a32ada2c35e 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -12,7 +12,7 @@ import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; import 'core.dart'; -import 'plugin_command.dart'; +import 'package_command.dart'; import 'process_runner.dart'; import 'repository_package.dart'; @@ -82,7 +82,7 @@ class PackageResult { /// An abstract base class for a command that iterates over a set of packages /// controlled by a standard set of flags, running some actions on each package, /// and collecting and reporting the success/failure of those actions. -abstract class PackageLoopingCommand extends PluginCommand { +abstract class PackageLoopingCommand extends PackageCommand { /// Creates a command to operate on [packagesDir] with the given environment. PackageLoopingCommand( Directory packagesDir, { diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index c9465876f29..870956a24b1 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -161,7 +161,7 @@ bool _isUnpublishedExampleChange( return exampleComponents.first.toLowerCase() != 'readme.md'; } -// True if the change is only relevant to people working on the plugin. +// True if the change is only relevant to people working on the package. Future _isDevChange(List pathComponents, {GitVersionFinder? git, String? repoPath}) async { return _isTestChange(pathComponents) || 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 7bf007ac72a..a23dc83d98f 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -10,13 +10,13 @@ import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_command.dart'; import 'common/repository_package.dart'; const String _outputDirectoryFlag = 'output-dir'; /// A command to create an application that builds all in a single application. -class CreateAllPluginsAppCommand extends PluginCommand { +class CreateAllPluginsAppCommand extends PackageCommand { /// Creates an instance of the builder command. CreateAllPluginsAppCommand( Directory packagesDir, { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 45e20c0f13c..e8fb11b5f28 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -49,7 +49,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { final String name = 'drive-examples'; @override - final String description = 'Runs driver tests for plugin example apps.\n\n' + final String description = 'Runs driver tests for package example apps.\n\n' 'For each *_test.dart in test_driver/ it drives an application with ' 'either the corresponding test in test_driver (for example, ' 'test_driver/app_test.dart would match test_driver/app.dart), or the ' diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index f640cbaa5f6..cc6936c566e 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -11,7 +11,7 @@ import 'package:meta/meta.dart'; import 'package:platform/platform.dart'; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_command.dart'; import 'common/process_runner.dart'; /// In theory this should be 8191, but in practice that was still resulting in @@ -37,7 +37,7 @@ 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'); /// A command to format all package code. -class FormatCommand extends PluginCommand { +class FormatCommand extends PackageCommand { /// Creates an instance of the format command. FormatCommand( Directory packagesDir, { diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 5e74d846c13..0517bcf4329 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -8,7 +8,7 @@ import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'common/core.dart'; -import 'common/plugin_command.dart'; +import 'common/package_command.dart'; const Set _codeFileExtensions = { '.c', @@ -105,7 +105,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. '''; /// Validates that code files have copyright and license blocks. -class LicenseCheckCommand extends PluginCommand { +class LicenseCheckCommand extends PackageCommand { /// Creates a new license check command for [packagesDir]. LicenseCheckCommand(Directory packagesDir, {Platform platform = const LocalPlatform(), GitDir? gitDir}) diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index e45c09bfd2e..b47657e47ef 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -5,11 +5,11 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; -import 'common/plugin_command.dart'; +import 'common/package_command.dart'; import 'common/repository_package.dart'; /// A command to list different types of repository content. -class ListCommand extends PluginCommand { +class ListCommand extends PackageCommand { /// Creates an instance of the list command, whose behavior depends on the /// 'type' argument it provides. ListCommand( @@ -18,14 +18,14 @@ class ListCommand extends PluginCommand { }) : super(packagesDir, platform: platform) { argParser.addOption( _type, - defaultsTo: _plugin, - allowed: [_plugin, _example, _package, _file], + defaultsTo: _package, + allowed: [_package, _example, _allPackage, _file], help: 'What type of file system content to list.', ); } static const String _type = 'type'; - static const String _plugin = 'plugin'; + static const String _allPackage = 'package-or-subpackage'; static const String _example = 'example'; static const String _package = 'package'; static const String _file = 'file'; @@ -39,7 +39,7 @@ class ListCommand extends PluginCommand { @override Future run() async { switch (getStringArg(_type)) { - case _plugin: + case _package: await for (final PackageEnumerationEntry entry in getTargetPackages()) { print(entry.package.path); } @@ -52,7 +52,7 @@ class ListCommand extends PluginCommand { print(package.path); } break; - case _package: + case _allPackage: await for (final PackageEnumerationEntry entry in getTargetPackagesAndSubpackages()) { print(entry.package.path); diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index ea1ec067c82..0f73c71e843 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -25,7 +25,7 @@ import 'list_command.dart'; import 'make_deps_path_based_command.dart'; import 'native_test_command.dart'; import 'publish_check_command.dart'; -import 'publish_plugin_command.dart'; +import 'publish_command.dart'; import 'pubspec_check_command.dart'; import 'readme_check_command.dart'; import 'remove_dev_dependencies.dart'; @@ -69,7 +69,7 @@ void main(List args) { ..addCommand(NativeTestCommand(packagesDir)) ..addCommand(MakeDepsPathBasedCommand(packagesDir)) ..addCommand(PublishCheckCommand(packagesDir)) - ..addCommand(PublishPluginCommand(packagesDir)) + ..addCommand(PublishCommand(packagesDir)) ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(ReadmeCheckCommand(packagesDir)) ..addCommand(RemoveDevDependenciesCommand(packagesDir)) diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index 805dd68a0af..a09511ad7f4 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -9,7 +9,7 @@ import 'package:pub_semver/pub_semver.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; -import 'common/plugin_command.dart'; +import 'common/package_command.dart'; import 'common/repository_package.dart'; const int _exitPackageNotFound = 3; @@ -24,7 +24,7 @@ enum _RewriteOutcome { changed, noChangesNeeded, alreadyChanged } /// where a non-breaking change to a platform interface package of a federated /// plugin would cause post-publish analyzer failures in another package of that /// plugin. -class MakeDepsPathBasedCommand extends PluginCommand { +class MakeDepsPathBasedCommand extends PackageCommand { /// Creates an instance of the command to convert selected dependencies to /// path-based. MakeDepsPathBasedCommand( @@ -150,7 +150,7 @@ class MakeDepsPathBasedCommand extends PluginCommand { return _RewriteOutcome.alreadyChanged; } printError( - 'Plugins with dependency overrides are not currently supported.'); + 'Packages with dependency overrides are not currently supported.'); throw ToolExit(_exitCannotUpdatePubspec); } diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index d98a286ff58..38e1b7bdebb 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -55,7 +55,7 @@ class PublishCheckCommand extends PackageLoopingCommand { @override final String description = - 'Checks to make sure that a plugin *could* be published.'; + 'Checks to make sure that a package *could* be published.'; final PubVersionFinder _pubVersionFinder; diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_command.dart similarity index 98% rename from script/tool/lib/src/publish_plugin_command.dart rename to script/tool/lib/src/publish_command.dart index cae8edac71c..e7b3d110c5f 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_command.dart @@ -18,8 +18,8 @@ import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/file_utils.dart'; import 'common/git_version_finder.dart'; +import 'common/package_command.dart'; import 'common/package_looping_command.dart'; -import 'common/plugin_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; import 'common/repository_package.dart'; @@ -42,13 +42,13 @@ class _RemoteInfo { /// 2. Tags the release with the format -v. /// 3. Pushes the release to a remote. /// -/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full +/// Both 2 and 3 are optional, see `plugin_tools help publish` for full /// usage information. /// /// [processRunner], [print], and [stdin] can be overriden for easier testing. -class PublishPluginCommand extends PackageLoopingCommand { +class PublishCommand extends PackageLoopingCommand { /// Creates an instance of the publish command. - PublishPluginCommand( + PublishCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), @@ -100,7 +100,7 @@ class PublishPluginCommand extends PackageLoopingCommand { static const String _tagFormat = '%PACKAGE%-v%VERSION%'; @override - final String name = 'publish-plugin'; + final String name = 'publish'; @override final String description = diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 235611492b2..b3be672066d 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -170,7 +170,7 @@ class VersionCheckCommand extends PackageLoopingCommand { @override final String description = - 'Checks if the versions of the plugins have been incremented per pub specification.\n' + 'Checks if the versions of packages have been incremented per pub specification.\n' 'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n' 'This command requires "pub" and "flutter" to be in your path.'; @@ -318,7 +318,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} print('${indentation}Unable to find previous version ' '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); logWarning( - '${indentation}If this plugin is not new, something has gone wrong.'); + '${indentation}If this package is not new, something has gone wrong.'); return _CurrentVersionState.validIncrease; // Assume new, thus valid. } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 9b9d7328855..246c2ade256 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.10.0+1 +version: 0.11.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index e10780fa9ec..e6b91096084 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -37,7 +37,7 @@ void main() { }); test('analyzes all packages', () async { - final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); + final RepositoryPackage package1 = createFakePackage('a', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('b', packagesDir); await runCapturingPrint(runner, ['analyze']); @@ -45,9 +45,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin1.path), - ProcessCall( - 'dart', const ['analyze', '--fatal-infos'], plugin1.path), + ProcessCall('flutter', const ['pub', 'get'], package1.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + package1.path), ProcessCall('flutter', const ['pub', 'get'], plugin2.path), ProcessCall( 'dart', const ['analyze', '--fatal-infos'], plugin2.path), diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart index ad1a26ffc16..d5a5dd4fe87 100644 --- a/script/tool/test/common/git_version_finder_test.dart +++ b/script/tool/test/common/git_version_finder_test.dart @@ -8,7 +8,7 @@ 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'; +import 'package_command_test.mocks.dart'; void main() { late List?> gitDirCommands; diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/package_command_test.dart similarity index 98% rename from script/tool/test/common/plugin_command_test.dart rename to script/tool/test/common/package_command_test.dart index 8c6b3868241..c3d1ee343ff 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/package_command_test.dart @@ -8,7 +8,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; +import 'package:flutter_plugin_tools/src/common/package_command.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:git/git.dart'; import 'package:mockito/annotations.dart'; @@ -18,12 +18,12 @@ import 'package:test/test.dart'; import '../mocks.dart'; import '../util.dart'; -import 'plugin_command_test.mocks.dart'; +import 'package_command_test.mocks.dart'; @GenerateMocks([GitDir]) void main() { late RecordingProcessRunner processRunner; - late SamplePluginCommand command; + late SamplePackageCommand command; late CommandRunner runner; late FileSystem fileSystem; late MockPlatform mockPlatform; @@ -49,7 +49,7 @@ void main() { return processRunner.run('git-$gitCommand', arguments); }); processRunner = RecordingProcessRunner(); - command = SamplePluginCommand( + command = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -282,7 +282,7 @@ packages/plugin1/plugin1/plugin1.dart }); test('returns subpackages after the enclosing package', () async { - final SamplePluginCommand localCommand = SamplePluginCommand( + final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -848,7 +848,7 @@ packages/b_package/lib/src/foo.dart ]; for (int i = 0; i < expectedShards.length; ++i) { - final SamplePluginCommand localCommand = SamplePluginCommand( + final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -892,7 +892,7 @@ packages/b_package/lib/src/foo.dart ]; for (int i = 0; i < expectedShards.length; ++i) { - final SamplePluginCommand localCommand = SamplePluginCommand( + final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -945,7 +945,7 @@ packages/b_package/lib/src/foo.dart createFakePackage('package9', packagesDir); for (int i = 0; i < expectedShards.length; ++i) { - final SamplePluginCommand localCommand = SamplePluginCommand( + final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -971,8 +971,8 @@ packages/b_package/lib/src/foo.dart }); } -class SamplePluginCommand extends PluginCommand { - SamplePluginCommand( +class SamplePackageCommand extends PackageCommand { + SamplePackageCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), diff --git a/script/tool/test/common/plugin_command_test.mocks.dart b/script/tool/test/common/package_command_test.mocks.dart similarity index 100% rename from script/tool/test/common/plugin_command_test.mocks.dart rename to script/tool/test/common/package_command_test.mocks.dart diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 7e9f63cb034..c858df0022c 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -18,7 +18,7 @@ import 'package:test/test.dart'; import '../mocks.dart'; import '../util.dart'; -import 'plugin_command_test.mocks.dart'; +import 'package_command_test.mocks.dart'; // Constants for colorized output start and end. const String _startElapsedTimeColor = '\x1B[90m'; @@ -373,9 +373,10 @@ void main() { test('skips unsupported Dart versions when requested', () async { final RepositoryPackage excluded = createFakePackage( - 'excluded_package', packagesDir, dartConstraint: '>=2.17.0 <3.0.0'); - final RepositoryPackage included = createFakePackage( - 'a_package', packagesDir); + 'excluded_package', packagesDir, + dartConstraint: '>=2.17.0 <3.0.0'); + final RepositoryPackage included = + createFakePackage('a_package', packagesDir); final TestPackageLoopingCommand command = createTestCommand( packageLoopingType: PackageLoopingType.includeAllSubpackages, @@ -406,8 +407,7 @@ void main() { createFakePlugin('package_a', packagesDir); createFakePackage('package_b', packagesDir); - final TestPackageLoopingCommand command = - createTestCommand(); + final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command); const String separator = @@ -440,8 +440,7 @@ void main() { createFakePlugin('package_a', packagesDir); createFakePackage('package_b', packagesDir); - final TestPackageLoopingCommand command = - createTestCommand(); + final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command, arguments: ['--log-timing']); @@ -783,8 +782,7 @@ void main() { createFakePackage('package_f', packagesDir); - final TestPackageLoopingCommand command = - createTestCommand(); + final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command); expect( @@ -809,8 +807,7 @@ void main() { test('prints exclusions as skips in long-form run summary', () async { createFakePackage('package_a', packagesDir); - final TestPackageLoopingCommand command = - createTestCommand(); + final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command, arguments: ['--exclude=package_a']); diff --git a/script/tool/test/custom_test_command_test.dart b/script/tool/test/custom_test_command_test.dart index a28b47505e9..8b0c021b125 100644 --- a/script/tool/test/custom_test_command_test.dart +++ b/script/tool/test/custom_test_command_test.dart @@ -40,7 +40,7 @@ void main() { test('runs both new and legacy when both are present', () async { final RepositoryPackage package = - createFakePlugin('a_package', packagesDir, extraFiles: [ + createFakePackage('a_package', packagesDir, extraFiles: [ 'tool/run_tests.dart', 'run_tests.sh', ]); @@ -65,7 +65,7 @@ void main() { }); test('runs when only new is present', () async { - final RepositoryPackage package = createFakePlugin( + final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, extraFiles: ['tool/run_tests.dart']); @@ -87,7 +87,7 @@ void main() { }); test('runs pub get before running Dart test script', () async { - final RepositoryPackage package = createFakePlugin( + final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, extraFiles: ['tool/run_tests.dart']); @@ -103,7 +103,7 @@ void main() { }); test('runs when only legacy is present', () async { - final RepositoryPackage package = createFakePlugin( + final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, extraFiles: ['run_tests.sh']); @@ -125,7 +125,7 @@ void main() { }); test('skips when neither is present', () async { - createFakePlugin('a_package', packagesDir); + createFakePackage('a_package', packagesDir); final List output = await runCapturingPrint(runner, ['custom-test']); @@ -140,7 +140,7 @@ void main() { }); test('fails if new fails', () async { - createFakePlugin('a_package', packagesDir, extraFiles: [ + createFakePackage('a_package', packagesDir, extraFiles: [ 'tool/run_tests.dart', 'run_tests.sh', ]); @@ -166,7 +166,7 @@ void main() { }); test('fails if pub get fails', () async { - createFakePlugin('a_package', packagesDir, extraFiles: [ + createFakePackage('a_package', packagesDir, extraFiles: [ 'tool/run_tests.dart', 'run_tests.sh', ]); @@ -193,7 +193,7 @@ void main() { test('fails if legacy fails', () async { final RepositoryPackage package = - createFakePlugin('a_package', packagesDir, extraFiles: [ + createFakePackage('a_package', packagesDir, extraFiles: [ 'tool/run_tests.dart', 'run_tests.sh', ]); @@ -238,7 +238,7 @@ void main() { test('runs new and skips old when both are present', () async { final RepositoryPackage package = - createFakePlugin('a_package', packagesDir, extraFiles: [ + createFakePackage('a_package', packagesDir, extraFiles: [ 'tool/run_tests.dart', 'run_tests.sh', ]); @@ -261,7 +261,7 @@ void main() { }); test('runs when only new is present', () async { - final RepositoryPackage package = createFakePlugin( + final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, extraFiles: ['tool/run_tests.dart']); @@ -283,7 +283,7 @@ void main() { }); test('skips package when only legacy is present', () async { - createFakePlugin('a_package', packagesDir, + createFakePackage('a_package', packagesDir, extraFiles: ['run_tests.sh']); final List output = @@ -300,7 +300,7 @@ void main() { }); test('fails if new fails', () async { - createFakePlugin('a_package', packagesDir, extraFiles: [ + createFakePackage('a_package', packagesDir, extraFiles: [ 'tool/run_tests.dart', 'run_tests.sh', ]); diff --git a/script/tool/test/dependabot_check_command_test.dart b/script/tool/test/dependabot_check_command_test.dart index a4c8693b2c2..b0558c3d22a 100644 --- a/script/tool/test/dependabot_check_command_test.dart +++ b/script/tool/test/dependabot_check_command_test.dart @@ -10,7 +10,7 @@ import 'package:flutter_plugin_tools/src/dependabot_check_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'util.dart'; void main() { diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart index 015a0eb634d..6b6b1a51453 100644 --- a/script/tool/test/federation_safety_check_command_test.dart +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -12,7 +12,7 @@ import 'package:flutter_plugin_tools/src/federation_safety_check_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index 5bd6f97832f..c1c4a212a01 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -60,15 +60,15 @@ void main() { } /// Returns a list of [count] relative paths to pass to [createFakePlugin] - /// with name [pluginName] such that each path will be 99 characters long - /// relative to [packagesDir]. + /// or [createFakePackage] with name [packageName] such that each path will + /// be 99 characters long relative to [packagesDir]. /// /// This is for each of testing batching, since it means each file will /// consume 100 characters of the batch length. - List _get99CharacterPathExtraFiles(String pluginName, int count) { + List _get99CharacterPathExtraFiles(String packageName, int count) { final int padding = 99 - - pluginName.length - - 1 - // the path separator after the plugin name + packageName.length - + 1 - // the path separator after the package name 1 - // the path separator after the padding 10; // the file name const int filenameBase = 10000; diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 43fbd6b5eca..005b77d99a1 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -11,7 +11,7 @@ import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index f74431c5cee..f19215c89b9 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -29,17 +29,17 @@ void main() { runner.addCommand(command); }); - test('lists plugins', () async { - createFakePlugin('plugin1', packagesDir); + test('lists top-level packages', () async { + createFakePackage('package1', packagesDir); createFakePlugin('plugin2', packagesDir); final List plugins = - await runCapturingPrint(runner, ['list', '--type=plugin']); + await runCapturingPrint(runner, ['list', '--type=package']); expect( plugins, orderedEquals([ - '/packages/plugin1', + '/packages/package1', '/packages/plugin2', ]), ); @@ -64,20 +64,20 @@ void main() { ); }); - test('lists packages', () async { - createFakePlugin('plugin1', packagesDir); + test('lists packages and subpackages', () async { + createFakePackage('package1', packagesDir); createFakePlugin('plugin2', packagesDir, examples: ['example1', 'example2']); createFakePlugin('plugin3', packagesDir, examples: []); - final List packages = - await runCapturingPrint(runner, ['list', '--type=package']); + final List packages = await runCapturingPrint( + runner, ['list', '--type=package-or-subpackage']); expect( packages, unorderedEquals([ - '/packages/plugin1', - '/packages/plugin1/example', + '/packages/package1', + '/packages/package1/example', '/packages/plugin2', '/packages/plugin2/example/example1', '/packages/plugin2/example/example2', diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index 33d6be261e8..7e52dd6bbbc 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -11,7 +11,7 @@ import 'package:flutter_plugin_tools/src/make_deps_path_based_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_command_test.dart similarity index 94% rename from script/tool/test/publish_plugin_command_test.dart rename to script/tool/test/publish_command_test.dart index f3be3b48b1f..19e8bdd233e 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_command_test.dart @@ -10,14 +10,14 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:flutter_plugin_tools/src/publish_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; @@ -34,7 +34,7 @@ void main() { late Map> mockHttpResponses; void _createMockCredentialFile() { - final String credentialPath = PublishPluginCommand.getCredentialPath(); + final String credentialPath = PublishCommand.getCredentialPath(); fileSystem.file(credentialPath) ..createSync(recursive: true) ..writeAsStringSync('some credential'); @@ -72,7 +72,7 @@ void main() { mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand( + ..addCommand(PublishCommand( packagesDir, processRunner: processRunner, stdinput: mockStdin, @@ -93,7 +93,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; @@ -122,7 +122,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--packages=foo'], + commandRunner, ['publish', '--packages=foo'], errorHandler: (Error e) { commandError = e; }); @@ -154,8 +154,8 @@ void main() { stderrEncoding: utf8), // pub publish for plugin1 ]; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--packages=plugin1,plugin2']); + final List output = await runCapturingPrint( + commandRunner, ['publish', '--packages=plugin1,plugin2']); expect( output, @@ -176,7 +176,7 @@ void main() { mockStdin.mockUserInputs.add(utf8.encode('user input')); await runCapturingPrint( - commandRunner, ['publish-plugin', '--packages=foo']); + commandRunner, ['publish', '--packages=foo']); expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); @@ -187,7 +187,7 @@ void main() { createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', '--pub-publish-flags', '--dry-run,--server=bar' @@ -209,7 +209,7 @@ void main() { createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', '--skip-confirmation', '--pub-publish-flags', @@ -232,7 +232,7 @@ void main() { createFakePlugin('plugin_b', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=plugin_a,plugin_b', '--skip-confirmation', '--pub-publish-flags', @@ -263,7 +263,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; @@ -283,7 +283,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', '--dry-run', ]); @@ -310,7 +310,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=$packageName', ]); @@ -330,7 +330,7 @@ void main() { test('with the version and name from the pubspec.yaml', () async { createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ]); @@ -348,7 +348,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; @@ -375,7 +375,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ]); @@ -398,7 +398,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--skip-confirmation', '--packages=foo', ]); @@ -420,8 +420,8 @@ void main() { mockStdin.readLineOutput = 'y'; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--packages=foo', '--dry-run']); + final List output = await runCapturingPrint( + commandRunner, ['publish', '--packages=foo', '--dry-run']); expect( processRunner.recordedCalls @@ -445,7 +445,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', '--remote', 'origin', @@ -491,7 +491,7 @@ void main() { mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, @@ -553,7 +553,7 @@ void main() { mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, @@ -598,7 +598,7 @@ void main() { final List output = await runCapturingPrint( commandRunner, [ - 'publish-plugin', + 'publish', '--all-changed', '--base-sha=HEAD~', '--dry-run' @@ -651,7 +651,7 @@ void main() { mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output2, containsAllInOrder([ @@ -700,7 +700,7 @@ void main() { mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output2, containsAllInOrder([ @@ -749,7 +749,7 @@ void main() { ]; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, @@ -795,7 +795,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~'], + ['publish', '--all-changed', '--base-sha=HEAD~'], errorHandler: (Error e) { commandError = e; }); @@ -830,7 +830,7 @@ void main() { ]; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect(output, containsAllInOrder(['Ran for 0 package(s)'])); expect( @@ -852,7 +852,7 @@ void main() { ]; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart index 34c85cc172f..79f53d8779b 100644 --- a/script/tool/test/update_excerpts_command_test.dart +++ b/script/tool/test/update_excerpts_command_test.dart @@ -12,7 +12,7 @@ import 'package:flutter_plugin_tools/src/update_excerpts_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; diff --git a/script/tool/test/update_release_info_command_test.dart b/script/tool/test/update_release_info_command_test.dart index 7e7ff54d594..8cd2e9591e7 100644 --- a/script/tool/test/update_release_info_command_test.dart +++ b/script/tool/test/update_release_info_command_test.dart @@ -12,7 +12,7 @@ import 'package:flutter_plugin_tools/src/update_release_info_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 0e94712e9ef..598176bbe20 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -16,7 +16,7 @@ import 'package:mockito/mockito.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; From 2e2c4c58377fe4da6bb58ff2a92fc42d1b4704f8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 28 Sep 2022 10:25:24 -0400 Subject: [PATCH 218/249] Enable `prefer_relative_imports` (#6501) --- script/tool/lib/src/common/cmake.dart | 2 +- .../lib/src/common/package_state_utils.dart | 2 +- script/tool/lib/src/common/plugin_utils.dart | 2 +- .../src/federation_safety_check_command.dart | 2 +- script/tool/lib/src/lint_android_command.dart | 2 +- script/tool/lib/src/main.dart | 2 +- .../lib/src/make_deps_path_based_command.dart | 4 ++- .../tool/lib/src/update_excerpts_command.dart | 2 +- .../lib/src/update_release_info_command.dart | 2 +- .../make_deps_path_based_command_test.dart | 36 +++++++++++++++++++ 10 files changed, 47 insertions(+), 9 deletions(-) diff --git a/script/tool/lib/src/common/cmake.dart b/script/tool/lib/src/common/cmake.dart index 04ad880292b..3f5d8452bd4 100644 --- a/script/tool/lib/src/common/cmake.dart +++ b/script/tool/lib/src/common/cmake.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:platform/platform.dart'; +import 'core.dart'; import 'process_runner.dart'; const String _cacheCommandKey = 'CMAKE_COMMAND:INTERNAL'; diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index 870956a24b1..a0c82400e1d 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -3,10 +3,10 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import 'git_version_finder.dart'; import 'repository_package.dart'; /// The state of a package on disk relative to git state. diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index f33d3d73bb7..94677fe7e5a 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -2,10 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:yaml/yaml.dart'; import 'core.dart'; +import 'repository_package.dart'; /// Possible plugin support options for a platform. enum PlatformSupport { diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index 383637a9e89..93a832eb0e2 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; @@ -13,6 +12,7 @@ import 'common/core.dart'; import 'common/file_utils.dart'; import 'common/git_version_finder.dart'; import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index 607674c80d3..eb78ce89168 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -3,12 +3,12 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 0f73c71e843..078976d9737 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -7,13 +7,13 @@ 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/dependabot_check_command.dart'; import 'analyze_command.dart'; import 'build_examples_command.dart'; import 'common/core.dart'; import 'create_all_plugins_app_command.dart'; import 'custom_test_command.dart'; +import 'dependabot_check_command.dart'; import 'drive_examples_command.dart'; import 'federation_safety_check_command.dart'; import 'firebase_test_lab_command.dart'; diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index a09511ad7f4..10abcd44ae6 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -158,10 +158,12 @@ class MakeDepsPathBasedCommand extends PackageCommand { ...pubspec.dependencies.keys, ...pubspec.devDependencies.keys, ]; - final Iterable packagesToOverride = combinedDependencies + final List packagesToOverride = combinedDependencies .where( (String packageName) => localDependencies.containsKey(packageName)) .toList(); + // Sort the combined list to avoid sort_pub_dependencies lint violations. + packagesToOverride.sort(); if (packagesToOverride.isNotEmpty) { final String commonBasePath = packagesDir.path; // Find the relative path to the common base. diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart index 320a3c59632..5a59104d4e7 100644 --- a/script/tool/lib/src/update_excerpts_command.dart +++ b/script/tool/lib/src/update_excerpts_command.dart @@ -5,12 +5,12 @@ import 'dart:io' as io; import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:git/git.dart'; import 'package:platform/platform.dart'; import 'package:yaml/yaml.dart'; import 'package:yaml_edit/yaml_edit.dart'; +import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart index 465b475eb9b..67aa994d963 100644 --- a/script/tool/lib/src/update_release_info_command.dart +++ b/script/tool/lib/src/update_release_info_command.dart @@ -4,11 +4,11 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:git/git.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:yaml_edit/yaml_edit.dart'; +import 'common/core.dart'; import 'common/git_version_finder.dart'; import 'common/package_looping_command.dart'; import 'common/package_state_utils.dart'; diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index 7e52dd6bbbc..36753e8001f 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -184,6 +184,42 @@ ${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} ])); }); + test( + 'alphabetizes overrides from different sectinos to avoid lint warnings in analysis', + () async { + createFakePackage('a', packagesDir); + createFakePackage('b', packagesDir); + createFakePackage('c', packagesDir); + final RepositoryPackage targetPackage = + createFakePackage('target', packagesDir); + + _addDependencies(targetPackage, ['a', 'c']); + _addDevDependenciesSection(targetPackage, ['b']); + + final List output = await runCapturingPrint(runner, + ['make-deps-path-based', '--target-dependencies=c,a,b']); + + expect( + output, + containsAllInOrder([ + 'Rewriting references to: c, a, b...', + ' Modified packages/target/pubspec.yaml', + ])); + + expect( + targetPackage.pubspecFile.readAsLinesSync(), + containsAllInOrder([ + '# FOR TESTING ONLY. DO NOT MERGE.', + 'dependency_overrides:', + ' a:', + ' path: ../a', + ' b:', + ' path: ../b', + ' c:', + ' path: ../c', + ])); + }); + // This test case ensures that running CI using this command on an interim // PR that itself used this command won't fail on the rewrite step. test('running a second time no-ops without failing', () async { From 92330dbd2c170af7ec3c5abd8aba5a8aec0b218a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 28 Sep 2022 14:22:46 -0400 Subject: [PATCH 219/249] Enable `no_leading_underscores_for_local_identifiers` (#6509) --- .../src/common/package_looping_command.dart | 4 +- script/tool/lib/src/native_test_command.dart | 4 +- .../test/dependabot_check_command_test.dart | 10 +- .../test/firebase_test_lab_command_test.dart | 32 ++--- script/tool/test/format_command_test.dart | 31 +++-- .../tool/test/license_check_command_test.dart | 38 +++--- .../make_deps_path_based_command_test.dart | 24 ++-- .../tool/test/native_test_command_test.dart | 116 +++++++++--------- script/tool/test/publish_command_test.dart | 8 +- .../test/remove_dev_dependencies_test.dart | 6 +- .../tool/test/version_check_command_test.dart | 27 ++-- 11 files changed, 148 insertions(+), 152 deletions(-) diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index a32ada2c35e..d8b1cf001d1 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -428,9 +428,9 @@ abstract class PackageLoopingCommand extends PackageCommand { .length; // Split the warnings into those from packages that ran, and those that // were skipped. - final Set _skippedPackagesWithWarnings = + final Set skippedPackagesWithWarnings = _packagesWithWarnings.intersection(skippedPackages); - final int skippedWarningCount = _skippedPackagesWithWarnings.length; + final int skippedWarningCount = skippedPackagesWithWarnings.length; final int runWarningCount = _packagesWithWarnings.length - skippedWarningCount; diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 81b13cbb75e..af5f4df98e8 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -406,9 +406,9 @@ this command. ); // The exit code from 'xcodebuild test' when there are no tests. - const int _xcodebuildNoTestExitCode = 66; + const int xcodebuildNoTestExitCode = 66; switch (exitCode) { - case _xcodebuildNoTestExitCode: + case xcodebuildNoTestExitCode: _printNoExampleTestsMessage(example, platform); break; case 0: diff --git a/script/tool/test/dependabot_check_command_test.dart b/script/tool/test/dependabot_check_command_test.dart index b0558c3d22a..39dd8f4fcb9 100644 --- a/script/tool/test/dependabot_check_command_test.dart +++ b/script/tool/test/dependabot_check_command_test.dart @@ -36,7 +36,7 @@ void main() { runner.addCommand(command); }); - void _setDependabotCoverage({ + void setDependabotCoverage({ Iterable gradleDirs = const [], }) { final Iterable gradleEntries = @@ -57,7 +57,7 @@ ${gradleEntries.join('\n')} } test('skips with no supported ecosystems', () async { - _setDependabotCoverage(); + setDependabotCoverage(); createFakePackage('a_package', packagesDir); final List output = @@ -71,7 +71,7 @@ ${gradleEntries.join('\n')} }); test('fails for app missing Gradle coverage', () async { - _setDependabotCoverage(); + setDependabotCoverage(); final RepositoryPackage package = createFakePackage('a_package', packagesDir); package.directory @@ -97,7 +97,7 @@ ${gradleEntries.join('\n')} }); test('fails for plugin missing Gradle coverage', () async { - _setDependabotCoverage(); + setDependabotCoverage(); final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir); plugin.directory.childDirectory('android').createSync(recursive: true); @@ -118,7 +118,7 @@ ${gradleEntries.join('\n')} }); test('passes for correct Gradle coverage', () async { - _setDependabotCoverage(gradleDirs: [ + setDependabotCoverage(gradleDirs: [ 'packages/a_plugin/android', 'packages/a_plugin/example/android/app', ]); diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 2d3175e171e..68ea62b2334 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -40,7 +40,7 @@ void main() { runner.addCommand(command); }); - void _writeJavaTestFile(RepositoryPackage plugin, String relativeFilePath, + void writeJavaTestFile(RepositoryPackage plugin, String relativeFilePath, {String runnerClass = 'FlutterTestRunner'}) { childFileWithSubcomponents( plugin.directory, p.posix.split(relativeFilePath)) @@ -67,7 +67,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -97,7 +97,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, ['firebase-test-lab']); @@ -120,7 +120,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin1, javaTestFileRelativePath); + writeJavaTestFile(plugin1, javaTestFileRelativePath); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: [ 'test/plugin_test.dart', @@ -128,7 +128,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin2, javaTestFileRelativePath); + writeJavaTestFile(plugin2, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -207,7 +207,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -286,7 +286,7 @@ public class MainActivityTest { ], ]); for (final String example in examples) { - _writeJavaTestFile( + writeJavaTestFile( plugin, 'example/$example/$javaTestFileExampleRelativePath'); } @@ -347,7 +347,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(), // auth @@ -393,7 +393,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(), // auth @@ -460,7 +460,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -501,7 +501,7 @@ public class MainActivityTest { javaTestFileRelativePath, ]); // Use the wrong @RunWith annotation. - _writeJavaTestFile(plugin, javaTestFileRelativePath, + writeJavaTestFile(plugin, javaTestFileRelativePath, runnerClass: 'AndroidJUnit4.class'); Error? commandError; @@ -565,7 +565,7 @@ public class MainActivityTest { 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -628,7 +628,7 @@ public class MainActivityTest { 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['flutter'] = [ MockProcess(exitCode: 1) // flutter build @@ -663,7 +663,7 @@ public class MainActivityTest { 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); final String gradlewPath = plugin .getExamples() @@ -704,7 +704,7 @@ public class MainActivityTest { 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); final String gradlewPath = plugin .getExamples() @@ -750,7 +750,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); await runCapturingPrint(runner, [ 'firebase-test-lab', diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index c1c4a212a01..9a865053a2b 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -49,7 +49,7 @@ void main() { /// Returns a modified version of a list of [relativePaths] that are relative /// to [package] to instead be relative to [packagesDir]. - List _getPackagesDirRelativePaths( + List getPackagesDirRelativePaths( RepositoryPackage package, List relativePaths) { final p.Context path = analyzeCommand.path; final String relativeBase = @@ -65,7 +65,7 @@ void main() { /// /// This is for each of testing batching, since it means each file will /// consume 100 characters of the batch length. - List _get99CharacterPathExtraFiles(String packageName, int count) { + List get99CharacterPathExtraFiles(String packageName, int count) { final int padding = 99 - packageName.length - 1 - // the path separator after the package name @@ -99,10 +99,7 @@ void main() { orderedEquals([ ProcessCall( getFlutterCommand(mockPlatform), - [ - 'format', - ..._getPackagesDirRelativePaths(plugin, files) - ], + ['format', ...getPackagesDirRelativePaths(plugin, files)], packagesDir.path), ])); }); @@ -138,7 +135,7 @@ void main() { getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(plugin, formattedFiles) + ...getPackagesDirRelativePaths(plugin, formattedFiles) ], packagesDir.path), ])); @@ -191,7 +188,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(plugin, files) + ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -271,7 +268,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(plugin, files) + ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -303,7 +300,7 @@ void main() { [ '-i', '--style=file', - ..._getPackagesDirRelativePaths(plugin, files) + ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -358,7 +355,7 @@ void main() { [ '-i', '--style=file', - ..._getPackagesDirRelativePaths(plugin, files) + ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -426,14 +423,14 @@ void main() { [ '-i', '--style=file', - ..._getPackagesDirRelativePaths(plugin, clangFiles) + ...getPackagesDirRelativePaths(plugin, clangFiles) ], packagesDir.path), ProcessCall( getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(plugin, dartFiles) + ...getPackagesDirRelativePaths(plugin, dartFiles) ], packagesDir.path), ProcessCall( @@ -442,7 +439,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(plugin, javaFiles) + ...getPackagesDirRelativePaths(plugin, javaFiles) ], packagesDir.path), ])); @@ -541,7 +538,7 @@ void main() { // Make the file list one file longer than would fit in the batch. final List batch1 = - _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + get99CharacterPathExtraFiles(pluginName, batchSize + 1); final String extraFile = batch1.removeLast(); createFakePlugin( @@ -578,7 +575,7 @@ void main() { // Make the file list one file longer than would fit in a Windows batch. final List batch = - _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + get99CharacterPathExtraFiles(pluginName, batchSize + 1); createFakePlugin( pluginName, @@ -598,7 +595,7 @@ void main() { // Make the file list one file longer than would fit in the batch. final List batch1 = - _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + get99CharacterPathExtraFiles(pluginName, batchSize + 1); final String extraFile = batch1.removeLast(); createFakePlugin( diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 005b77d99a1..09841df74e7 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -48,7 +48,7 @@ void main() { /// [commentString] is added to the start of each line. /// [prefix] is added to the start of the entire block. /// [suffix] is added to the end of the entire block. - void _writeLicense( + void writeLicense( File file, { String comment = '// ', String prefix = '', @@ -164,7 +164,7 @@ void main() { test('passes if all checked files have license blocks', () async { final File checked = root.childFile('checked.cc'); checked.createSync(); - _writeLicense(checked); + writeLicense(checked); final File notChecked = root.childFile('not_checked.md'); notChecked.createSync(); @@ -183,7 +183,7 @@ void main() { test('passes correct license blocks on Windows', () async { final File checked = root.childFile('checked.cc'); checked.createSync(); - _writeLicense(checked, useCrlf: true); + writeLicense(checked, useCrlf: true); final List output = await runCapturingPrint(runner, ['license-check']); @@ -200,13 +200,13 @@ void main() { test('handles the comment styles for all supported languages', () async { final File fileA = root.childFile('file_a.cc'); fileA.createSync(); - _writeLicense(fileA); + writeLicense(fileA); final File fileB = root.childFile('file_b.sh'); fileB.createSync(); - _writeLicense(fileB, comment: '# '); + writeLicense(fileB, comment: '# '); final File fileC = root.childFile('file_c.html'); fileC.createSync(); - _writeLicense(fileC, comment: '', prefix: ''); + writeLicense(fileC, comment: '', prefix: ''); final List output = await runCapturingPrint(runner, ['license-check']); @@ -225,10 +225,10 @@ void main() { test('fails if any checked files are missing license blocks', () async { final File goodA = root.childFile('good.cc'); goodA.createSync(); - _writeLicense(goodA); + writeLicense(goodA); final File goodB = root.childFile('good.h'); goodB.createSync(); - _writeLicense(goodB); + writeLicense(goodB); root.childFile('bad.cc').createSync(); root.childFile('bad.h').createSync(); @@ -255,10 +255,10 @@ void main() { test('fails if any checked files are missing just the copyright', () async { final File good = root.childFile('good.cc'); good.createSync(); - _writeLicense(good); + writeLicense(good); final File bad = root.childFile('bad.cc'); bad.createSync(); - _writeLicense(bad, copyright: ''); + writeLicense(bad, copyright: ''); Error? commandError; final List output = await runCapturingPrint( @@ -282,10 +282,10 @@ void main() { test('fails if any checked files are missing just the license', () async { final File good = root.childFile('good.cc'); good.createSync(); - _writeLicense(good); + writeLicense(good); final File bad = root.childFile('bad.cc'); bad.createSync(); - _writeLicense(bad, license: []); + writeLicense(bad, license: []); Error? commandError; final List output = await runCapturingPrint( @@ -310,7 +310,7 @@ void main() { () async { final File thirdPartyFile = root.childFile('third_party.cc'); thirdPartyFile.createSync(); - _writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); + writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); Error? commandError; final List output = await runCapturingPrint( @@ -339,7 +339,7 @@ void main() { .childDirectory('third_party') .childFile('file.cc'); thirdPartyFile.createSync(recursive: true); - _writeLicense(thirdPartyFile, + writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Workiva Inc.', license: [ 'Licensed under the Apache License, Version 2.0 (the "License");', @@ -366,7 +366,7 @@ void main() { .childDirectory('third_party') .childFile('first_party.cc'); firstPartyFileInThirdParty.createSync(recursive: true); - _writeLicense(firstPartyFileInThirdParty); + writeLicense(firstPartyFileInThirdParty); final List output = await runCapturingPrint(runner, ['license-check']); @@ -383,10 +383,10 @@ void main() { test('fails for licenses that the tool does not expect', () async { final File good = root.childFile('good.cc'); good.createSync(); - _writeLicense(good); + writeLicense(good); final File bad = root.childDirectory('third_party').childFile('bad.cc'); bad.createSync(recursive: true); - _writeLicense(bad, license: [ + writeLicense(bad, license: [ 'This program is free software: you can redistribute it and/or modify', 'it under the terms of the GNU General Public License', ]); @@ -414,10 +414,10 @@ void main() { () async { final File good = root.childFile('good.cc'); good.createSync(); - _writeLicense(good); + writeLicense(good); final File bad = root.childDirectory('third_party').childFile('bad.cc'); bad.createSync(recursive: true); - _writeLicense( + writeLicense( bad, copyright: 'Copyright 2017 Some New Authors.', license: [ diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index 36753e8001f..e846a63fc68 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -49,7 +49,7 @@ void main() { /// Adds dummy 'dependencies:' entries for each package in [dependencies] /// to [package]. - void _addDependencies( + void addDependencies( RepositoryPackage package, Iterable dependencies) { final List lines = package.pubspecFile.readAsLinesSync(); final int dependenciesStartIndex = lines.indexOf('dependencies:'); @@ -62,7 +62,7 @@ void main() { /// Adds a 'dev_dependencies:' section with entries for each package in /// [dependencies] to [package]. - void _addDevDependenciesSection( + void addDevDependenciesSection( RepositoryPackage package, Iterable devDependencies) { final String originalContent = package.pubspecFile.readAsStringSync(); package.pubspecFile.writeAsStringSync(''' @@ -77,7 +77,7 @@ ${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} createFakePackage('foo', packagesDir, isFlutter: true); final RepositoryPackage packageBar = createFakePackage('bar', packagesDir, isFlutter: true); - _addDependencies(packageBar, ['foo']); + addDependencies(packageBar, ['foo']); final String originalPubspecContents = packageBar.pubspecFile.readAsStringSync(); @@ -105,16 +105,16 @@ ${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} final RepositoryPackage pluginAppFacing = createFakePlugin('bar', pluginGroup); - _addDependencies(simplePackage, [ + addDependencies(simplePackage, [ 'bar', 'bar_android', 'bar_platform_interface', ]); - _addDependencies(pluginAppFacing, [ + addDependencies(pluginAppFacing, [ 'bar_platform_interface', 'bar_android', ]); - _addDependencies(pluginImplementation, [ + addDependencies(pluginImplementation, [ 'bar_platform_interface', ]); @@ -160,7 +160,7 @@ ${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} final RepositoryPackage builderPackage = createFakePackage('foo_builder', packagesDir); - _addDevDependenciesSection(builderPackage, [ + addDevDependenciesSection(builderPackage, [ 'foo', ]); @@ -193,8 +193,8 @@ ${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} final RepositoryPackage targetPackage = createFakePackage('target', packagesDir); - _addDependencies(targetPackage, ['a', 'c']); - _addDevDependenciesSection(targetPackage, ['b']); + addDependencies(targetPackage, ['a', 'c']); + addDevDependenciesSection(targetPackage, ['b']); final List output = await runCapturingPrint(runner, ['make-deps-path-based', '--target-dependencies=c,a,b']); @@ -233,16 +233,16 @@ ${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} final RepositoryPackage pluginAppFacing = createFakePlugin('bar', pluginGroup); - _addDependencies(simplePackage, [ + addDependencies(simplePackage, [ 'bar', 'bar_android', 'bar_platform_interface', ]); - _addDependencies(pluginAppFacing, [ + addDependencies(pluginAppFacing, [ 'bar_platform_interface', 'bar_android', ]); - _addDependencies(pluginImplementation, [ + addDependencies(pluginImplementation, [ 'bar_platform_interface', ]); diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index d420184b612..f24d014bbfe 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -68,7 +68,7 @@ void _createFakeCMakeCache(RepositoryPackage plugin, Platform platform) { // TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of // doing all the process mocking and validation. void main() { - const String _kDestination = '--ios-destination'; + const String kDestination = '--ios-destination'; group('test native_test_command on Posix', () { late FileSystem fileSystem; @@ -95,7 +95,7 @@ void main() { // Returns a MockProcess to provide for "xcrun xcodebuild -list" for a // project that contains [targets]. - MockProcess _getMockXcodebuildListProcess(List targets) { + MockProcess getMockXcodebuildListProcess(List targets) { final Map projects = { 'project': { 'targets': targets, @@ -106,7 +106,7 @@ void main() { // Returns the ProcessCall to expect for checking the targets present in // the [package]'s [platform]/Runner.xcodeproj. - ProcessCall _getTargetCheckCall(Directory package, String platform) { + ProcessCall getTargetCheckCall(Directory package, String platform) { return ProcessCall( 'xcrun', [ @@ -124,7 +124,7 @@ void main() { // Returns the ProcessCall to expect for running the tests in the // workspace [platform]/Runner.xcworkspace, with the given extra flags. - ProcessCall _getRunTestCall( + ProcessCall getRunTestCall( Directory package, String platform, { String? destination, @@ -150,7 +150,7 @@ void main() { // Returns the ProcessCall to expect for build the Linux unit tests for the // given plugin. - ProcessCall _getLinuxBuildCall(RepositoryPackage plugin) { + ProcessCall getLinuxBuildCall(RepositoryPackage plugin) { return ProcessCall( 'cmake', [ @@ -212,7 +212,7 @@ void main() { final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), + getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), // Exit code 66 from testing indicates no tests. MockProcess(exitCode: 66), ]; @@ -229,8 +229,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos', + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos', extraFlags: ['-only-testing:RunnerUITests']), ])); }); @@ -243,7 +243,7 @@ void main() { }); final List output = await runCapturingPrint(runner, - ['native-test', '--ios', _kDestination, 'foo_destination']); + ['native-test', '--ios', kDestination, 'foo_destination']); expect( output, containsAllInOrder([ @@ -260,7 +260,7 @@ void main() { }); final List output = await runCapturingPrint(runner, - ['native-test', '--ios', _kDestination, 'foo_destination']); + ['native-test', '--ios', kDestination, 'foo_destination']); expect( output, containsAllInOrder([ @@ -279,14 +279,14 @@ void main() { final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; final List output = await runCapturingPrint(runner, [ 'native-test', '--ios', - _kDestination, + kDestination, 'foo_destination', ]); @@ -300,8 +300,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'ios'), - _getRunTestCall(pluginExampleDirectory, 'ios', + getTargetCheckCall(pluginExampleDirectory, 'ios'), + getRunTestCall(pluginExampleDirectory, 'ios', destination: 'foo_destination'), ])); }); @@ -316,7 +316,7 @@ void main() { processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -336,8 +336,8 @@ void main() { '--json', ], null), - _getTargetCheckCall(pluginExampleDirectory, 'ios'), - _getRunTestCall(pluginExampleDirectory, 'ios', + getTargetCheckCall(pluginExampleDirectory, 'ios'), + getRunTestCall(pluginExampleDirectory, 'ios', destination: 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A'), ])); }); @@ -386,7 +386,7 @@ void main() { final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -403,8 +403,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); }); @@ -918,7 +918,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(plugin), + getLinuxBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -958,7 +958,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(plugin), + getLinuxBuildCall(plugin), ProcessCall(releaseTestBinary.path, const [], null), ])); }); @@ -1017,7 +1017,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(plugin), + getLinuxBuildCall(plugin), ])); }); @@ -1058,7 +1058,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(plugin), + getLinuxBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -1102,7 +1102,7 @@ void main() { final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -1121,8 +1121,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos', + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos', extraFlags: ['-only-testing:RunnerTests']), ])); }); @@ -1137,7 +1137,7 @@ void main() { final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -1156,8 +1156,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos', + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos', extraFlags: ['-only-testing:RunnerUITests']), ])); }); @@ -1173,7 +1173,7 @@ void main() { // Simulate a project with unit tests but no integration tests... processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess(['RunnerTests']), + getMockXcodebuildListProcess(['RunnerTests']), ]; // ... then try to run only integration tests. @@ -1193,7 +1193,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1207,7 +1207,7 @@ void main() { final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess(['RunnerUITests']), + getMockXcodebuildListProcess(['RunnerUITests']), ]; Error? commandError; @@ -1232,7 +1232,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1269,7 +1269,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); }); @@ -1295,10 +1295,10 @@ void main() { pluginExampleDirectory.childDirectory('android'); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), // iOS list MockProcess(), // iOS run - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), // macOS list MockProcess(), // macOS run ]; @@ -1308,7 +1308,7 @@ void main() { '--android', '--ios', '--macos', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1325,11 +1325,11 @@ void main() { orderedEquals([ ProcessCall(androidFolder.childFile('gradlew').path, const ['testDebugUnitTest'], androidFolder.path), - _getTargetCheckCall(pluginExampleDirectory, 'ios'), - _getRunTestCall(pluginExampleDirectory, 'ios', + getTargetCheckCall(pluginExampleDirectory, 'ios'), + getRunTestCall(pluginExampleDirectory, 'ios', destination: 'foo_destination'), - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1342,7 +1342,7 @@ void main() { final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -1350,7 +1350,7 @@ void main() { 'native-test', '--ios', '--macos', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1364,8 +1364,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1378,7 +1378,7 @@ void main() { final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -1386,7 +1386,7 @@ void main() { 'native-test', '--ios', '--macos', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1400,8 +1400,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'ios'), - _getRunTestCall(pluginExampleDirectory, 'ios', + getTargetCheckCall(pluginExampleDirectory, 'ios'), + getRunTestCall(pluginExampleDirectory, 'ios', destination: 'foo_destination'), ])); }); @@ -1415,7 +1415,7 @@ void main() { '--ios', '--macos', '--windows', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1447,7 +1447,7 @@ void main() { 'native-test', '--macos', '--windows', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1477,7 +1477,7 @@ void main() { ); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -1598,7 +1598,7 @@ void main() { // Returns the ProcessCall to expect for build the Windows unit tests for // the given plugin. - ProcessCall _getWindowsBuildCall(RepositoryPackage plugin) { + ProcessCall getWindowsBuildCall(RepositoryPackage plugin) { return ProcessCall( _fakeCmakeCommand, [ @@ -1647,7 +1647,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(plugin), + getWindowsBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -1687,7 +1687,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(plugin), + getWindowsBuildCall(plugin), ProcessCall(debugTestBinary.path, const [], null), ])); }); @@ -1746,7 +1746,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(plugin), + getWindowsBuildCall(plugin), ])); }); @@ -1787,7 +1787,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(plugin), + getWindowsBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); diff --git a/script/tool/test/publish_command_test.dart b/script/tool/test/publish_command_test.dart index 19e8bdd233e..da5f9c871f0 100644 --- a/script/tool/test/publish_command_test.dart +++ b/script/tool/test/publish_command_test.dart @@ -33,7 +33,7 @@ void main() { // Map of package name to mock response. late Map> mockHttpResponses; - void _createMockCredentialFile() { + void createMockCredentialFile() { final String credentialPath = PublishCommand.getCredentialPath(); fileSystem.file(credentialPath) ..createSync(recursive: true) @@ -204,7 +204,7 @@ void main() { test( '--skip-confirmation flag automatically adds --force to --pub-publish-flags', () async { - _createMockCredentialFile(); + createMockCredentialFile(); final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); @@ -225,7 +225,7 @@ void main() { }); test('--force is only added once, regardless of plugin count', () async { - _createMockCredentialFile(); + createMockCredentialFile(); final RepositoryPackage plugin1 = createFakePlugin('plugin_a', packagesDir, examples: []); final RepositoryPackage plugin2 = @@ -393,7 +393,7 @@ void main() { test('does not ask for user input if the --skip-confirmation flag is on', () async { - _createMockCredentialFile(); + createMockCredentialFile(); createFakePlugin('foo', packagesDir, examples: []); final List output = diff --git a/script/tool/test/remove_dev_dependencies_test.dart b/script/tool/test/remove_dev_dependencies_test.dart index 6b212870c53..776cbf19783 100644 --- a/script/tool/test/remove_dev_dependencies_test.dart +++ b/script/tool/test/remove_dev_dependencies_test.dart @@ -27,7 +27,7 @@ void main() { runner.addCommand(command); }); - void _addToPubspec(RepositoryPackage package, String addition) { + void addToPubspec(RepositoryPackage package, String addition) { final String originalContent = package.pubspecFile.readAsStringSync(); package.pubspecFile.writeAsStringSync(''' $originalContent @@ -53,7 +53,7 @@ $addition final RepositoryPackage package = createFakePackage('a_package', packagesDir, version: '1.0.0'); - _addToPubspec(package, ''' + addToPubspec(package, ''' dev_dependencies: some_dependency: ^2.1.8 another_dependency: ^1.0.0 @@ -79,7 +79,7 @@ dev_dependencies: createFakePackage('a_package', packagesDir, version: '1.0.0'); final RepositoryPackage example = package.getExamples().first; - _addToPubspec(example, ''' + addToPubspec(example, ''' dev_dependencies: some_dependency: ^2.1.8 another_dependency: ^1.0.0 diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 598176bbe20..d485d81ceaf 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -681,8 +681,7 @@ void main() { }); group('missing change detection', () { - Future> _runWithMissingChangeDetection( - List extraArgs, + Future> runWithMissingChangeDetection(List extraArgs, {void Function(Error error)? errorHandler}) async { return runCapturingPrint( runner, @@ -712,7 +711,7 @@ void main() { ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -743,7 +742,7 @@ packages/plugin/lib/plugin.dart ]; Error? commandError; - final List output = await _runWithMissingChangeDetection( + final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }); @@ -780,7 +779,7 @@ packages/plugin/pubspec.yaml ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -810,7 +809,7 @@ tool/plugin/lib/plugin.dart ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -843,7 +842,7 @@ packages/plugin/CHANGELOG.md ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -874,7 +873,7 @@ packages/plugin/pubspec.yaml ]; final List output = - await _runWithMissingChangeDetection([ + await runWithMissingChangeDetection([ '--pr-labels=some label,override: no versioning needed,another-label' ]); @@ -906,7 +905,7 @@ packages/plugin/example/lib/foo.dart ]; Error? commandError; - final List output = await _runWithMissingChangeDetection( + final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }); @@ -942,7 +941,7 @@ packages/plugin/CHANGELOG.md ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -973,7 +972,7 @@ packages/another_plugin/CHANGELOG.md ]; Error? commandError; - final List output = await _runWithMissingChangeDetection( + final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }); @@ -1006,7 +1005,7 @@ packages/plugin/example/lib/foo.dart ]; final List output = - await _runWithMissingChangeDetection([ + await runWithMissingChangeDetection([ '--pr-labels=some label,override: no changelog needed,another-label' ]); @@ -1050,7 +1049,7 @@ packages/plugin/android/build.gradle ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -1082,7 +1081,7 @@ packages/plugin/run_tests.sh ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, From 70ff6821af9a0673ba913beec3e3aecd7cb239d8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 28 Sep 2022 16:51:15 -0400 Subject: [PATCH 220/249] [tool] Adds a `fix` command (#6512) --- script/tool/CHANGELOG.md | 4 ++ script/tool/lib/src/fix_command.dart | 51 +++++++++++++++++ script/tool/lib/src/main.dart | 2 + script/tool/test/fix_command_test.dart | 78 ++++++++++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 script/tool/lib/src/fix_command.dart create mode 100644 script/tool/test/fix_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 830d3ca931e..bf549d4e79d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.11.1 + +* Adds a `fix` command to run `dart fix --apply` in target packages. + ## 0.11 * Renames `publish-plugin` to `publish`. diff --git a/script/tool/lib/src/fix_command.dart b/script/tool/lib/src/fix_command.dart new file mode 100644 index 00000000000..2819609eabb --- /dev/null +++ b/script/tool/lib/src/fix_command.dart @@ -0,0 +1,51 @@ +// 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 'package:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to run Dart's "fix" command on packages. +class FixCommand extends PackageLoopingCommand { + /// Creates a fix command instance. + FixCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform); + + @override + final String name = 'fix'; + + @override + final String description = 'Fixes packages using dart fix.\n\n' + 'This command requires "dart" and "flutter" to be in your path, and ' + 'assumes that dependencies have already been fetched (e.g., by running ' + 'the analyze command first).'; + + @override + final bool hasLongOutput = false; + + @override + PackageLoopingType get packageLoopingType => + PackageLoopingType.includeAllSubpackages; + + @override + Future runForPackage(RepositoryPackage package) async { + final int exitCode = await processRunner.runAndStream( + 'dart', ['fix', '--apply'], + workingDir: package.directory); + if (exitCode != 0) { + printError('Unable to automatically fix package.'); + return PackageResult.fail(); + } + return PackageResult.success(); + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 078976d9737..414ca7f303c 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -17,6 +17,7 @@ import 'dependabot_check_command.dart'; import 'drive_examples_command.dart'; import 'federation_safety_check_command.dart'; import 'firebase_test_lab_command.dart'; +import 'fix_command.dart'; import 'format_command.dart'; import 'license_check_command.dart'; import 'lint_android_command.dart'; @@ -61,6 +62,7 @@ void main(List args) { ..addCommand(DriveExamplesCommand(packagesDir)) ..addCommand(FederationSafetyCheckCommand(packagesDir)) ..addCommand(FirebaseTestLabCommand(packagesDir)) + ..addCommand(FixCommand(packagesDir)) ..addCommand(FormatCommand(packagesDir)) ..addCommand(LicenseCheckCommand(packagesDir)) ..addCommand(LintAndroidCommand(packagesDir)) diff --git a/script/tool/test/fix_command_test.dart b/script/tool/test/fix_command_test.dart new file mode 100644 index 00000000000..16061d2206c --- /dev/null +++ b/script/tool/test/fix_command_test.dart @@ -0,0 +1,78 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/fix_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final FixCommand command = FixCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner('fix_command', 'Test for fix_command'); + runner.addCommand(command); + }); + + test('runs fix in top-level packages and subpackages', () async { + final RepositoryPackage package = createFakePackage('a', packagesDir); + final RepositoryPackage plugin = createFakePlugin('b', packagesDir); + + await runCapturingPrint(runner, ['fix']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('dart', const ['fix', '--apply'], package.path), + ProcessCall('dart', const ['fix', '--apply'], + package.getExamples().first.path), + ProcessCall('dart', const ['fix', '--apply'], plugin.path), + ProcessCall('dart', const ['fix', '--apply'], + plugin.getExamples().first.path), + ])); + }); + + test('fails if "dart fix" fails', () async { + createFakePlugin('foo', packagesDir); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, ['fix'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to automatically fix package.'), + ]), + ); + }); +} From d231b240f1bfe728fef49e927c690f44c8a272ab Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 30 Sep 2022 10:53:12 -0400 Subject: [PATCH 221/249] [tool] Only run postsubmit on changed packages (#6516) --- script/tool/CHANGELOG.md | 6 +- .../lib/src/common/git_version_finder.dart | 2 +- .../tool/lib/src/common/package_command.dart | 38 +++++++------ script/tool/pubspec.yaml | 2 +- .../test/common/package_command_test.dart | 57 ++++++++++++++----- 5 files changed, 71 insertions(+), 34 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index bf549d4e79d..f62509040a3 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,9 @@ -## 0.11.1 +## 12.0 +* Changes the behavior of `--packages-for-branch` on main/master to run for + packages changed in the last commit, rather than running for all packages. + This allows CI to test the same filtered set of packages in post-submit as are + tested in presubmit. * Adds a `fix` command to run `dart fix --apply` in target packages. ## 0.11 diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index eb8fba6b76b..b135424827a 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -103,7 +103,7 @@ class GitVersionFinder { ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], throwOnError: false); final String stdout = (baseShaFromMergeBase.stdout as String? ?? '').trim(); - final String stderr = (baseShaFromMergeBase.stdout as String? ?? '').trim(); + final String stderr = (baseShaFromMergeBase.stderr as String? ?? '').trim(); if (stderr.isNotEmpty || stdout.isEmpty) { baseShaFromMergeBase = await baseGitDir .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); diff --git a/script/tool/lib/src/common/package_command.dart b/script/tool/lib/src/common/package_command.dart index 60d7ecc8007..1d82c4e936b 100644 --- a/script/tool/lib/src/common/package_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -84,9 +84,8 @@ abstract class PackageCommand extends Command { 'Cannot be combined with $_packagesArg.\n', hide: true); argParser.addFlag(_packagesForBranchArg, - help: - 'This runs on all packages (equivalent to no package selection flag)\n' - 'on main (or master), and behaves like --run-on-changed-packages on ' + help: 'This runs on all packages changed in the last commit on main ' + '(or master), and behaves like --run-on-changed-packages on ' 'any other branch.\n\n' 'Cannot be combined with $_packagesArg.\n\n' 'This is intended for use in CI.\n', @@ -311,9 +310,9 @@ abstract class PackageCommand extends Command { Set packages = Set.from(getStringListArg(_packagesArg)); - final bool runOnChangedPackages; + final GitVersionFinder? changedFileFinder; if (getBoolArg(_runOnChangedPackagesArg)) { - runOnChangedPackages = true; + changedFileFinder = await retrieveVersionFinder(); } else if (getBoolArg(_packagesForBranchArg)) { final String? branch = await _getBranch(); if (branch == null) { @@ -321,24 +320,28 @@ abstract class PackageCommand extends Command { 'only be used in a git repository.'); throw ToolExit(exitInvalidArguments); } else { - runOnChangedPackages = branch != 'master' && branch != 'main'; - // Log the mode for auditing what was intended to run. - print('--$_packagesForBranchArg: running on ' - '${runOnChangedPackages ? 'changed' : 'all'} packages'); + // Configure the change finder the correct mode for the branch. + final bool lastCommitOnly = branch == 'main' || branch == 'master'; + if (lastCommitOnly) { + // Log the mode to make it easier to audit logs to see that the + // intended diff was used. + print('--$_packagesForBranchArg: running on default branch; ' + 'using parent commit as the diff base.'); + changedFileFinder = GitVersionFinder(await gitDir, 'HEAD~'); + } else { + changedFileFinder = await retrieveVersionFinder(); + } } } else { - runOnChangedPackages = false; + changedFileFinder = null; } - final Set excludedPackageNames = getExcludedPackageNames(); - - if (runOnChangedPackages) { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final String baseSha = await gitVersionFinder.getBaseSha(); + if (changedFileFinder != null) { + final String baseSha = await changedFileFinder.getBaseSha(); print( - 'Running for all packages that have changed relative to "$baseSha"\n'); + 'Running for all packages that have diffs relative to "$baseSha"\n'); final List changedFiles = - await gitVersionFinder.getChangedFiles(); + await changedFileFinder.getChangedFiles(); if (!_changesRequireFullTest(changedFiles)) { packages = _getChangedPackageNames(changedFiles); } @@ -362,6 +365,7 @@ abstract class PackageCommand extends Command { .childDirectory('third_party') .childDirectory('packages'); + final Set excludedPackageNames = getExcludedPackageNames(); for (final Directory dir in [ packagesDir, if (thirdPartyPackagesDirectory.existsSync()) thirdPartyPackagesDirectory, diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 246c2ade256..9e767ad724c 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.11.0 +version: 0.12.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_command_test.dart b/script/tool/test/common/package_command_test.dart index c3d1ee343ff..be21b86bff7 100644 --- a/script/tool/test/common/package_command_test.dart +++ b/script/tool/test/common/package_command_test.dart @@ -523,7 +523,7 @@ packages/plugin1/CHANGELOG output, containsAllInOrder([ contains( - 'Running for all packages that have changed relative to "main"'), + 'Running for all packages that have diffs relative to "main"'), ])); expect(command.plugins, unorderedEquals([plugin1.path])); @@ -732,13 +732,17 @@ packages/b_package/lib/src/foo.dart }); group('--packages-for-branch', () { - test('only tests changed packages on a branch', () async { + test('only tests changed packages relative to the merge base on a branch', + () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'packages/plugin1/plugin1.dart'), ]; processRunner.mockProcessesForExecutable['git-rev-parse'] = [ MockProcess(stdout: 'a-branch'), ]; + processRunner.mockProcessesForExecutable['git-merge-base'] = [ + MockProcess(stdout: 'abc123'), + ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); @@ -750,11 +754,20 @@ packages/b_package/lib/src/foo.dart expect( output, containsAllInOrder([ - contains('--packages-for-branch: running on changed packages'), + contains( + 'Running for all packages that have diffs relative to "abc123"'), ])); + // Ensure that it's diffing against the merge-base. + expect( + processRunner.recordedCalls, + contains( + const ProcessCall( + 'git-diff', ['--name-only', 'abc123', 'HEAD'], null), + )); }); - test('tests all packages on main', () async { + test('only tests changed packages relative to the previous commit on main', + () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'packages/plugin1/plugin1.dart'), ]; @@ -763,19 +776,27 @@ packages/b_package/lib/src/foo.dart ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); expect( output, containsAllInOrder([ - contains('--packages-for-branch: running on all packages'), + contains('--packages-for-branch: running on default branch; ' + 'using parent commit as the diff base'), + contains( + 'Running for all packages that have diffs relative to "HEAD~"'), ])); + // Ensure that it's diffing against the prior commit. + expect( + processRunner.recordedCalls, + contains( + const ProcessCall( + 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), + )); }); test('tests all packages on master', () async { @@ -787,19 +808,27 @@ packages/b_package/lib/src/foo.dart ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); expect( output, containsAllInOrder([ - contains('--packages-for-branch: running on all packages'), + contains('--packages-for-branch: running on default branch; ' + 'using parent commit as the diff base'), + contains( + 'Running for all packages that have diffs relative to "HEAD~"'), ])); + // Ensure that it's diffing against the prior commit. + expect( + processRunner.recordedCalls, + contains( + const ProcessCall( + 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), + )); }); test('throws if getting the branch fails', () async { From 28bdd6dff83fd54ecb80bbef9cc90c65e27e7250 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 30 Sep 2022 17:55:50 -0400 Subject: [PATCH 222/249] [tool] Improve changed-package run mode logging (#6521) --- .../tool/lib/src/common/package_command.dart | 9 ++-- .../test/common/package_command_test.dart | 51 ++++++++++++++++--- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/script/tool/lib/src/common/package_command.dart b/script/tool/lib/src/common/package_command.dart index 1d82c4e936b..0e83d03e984 100644 --- a/script/tool/lib/src/common/package_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -338,11 +338,14 @@ abstract class PackageCommand extends Command { if (changedFileFinder != null) { final String baseSha = await changedFileFinder.getBaseSha(); - print( - 'Running for all packages that have diffs relative to "$baseSha"\n'); final List changedFiles = await changedFileFinder.getChangedFiles(); - if (!_changesRequireFullTest(changedFiles)) { + if (_changesRequireFullTest(changedFiles)) { + print('Running for all packages, since a file has changed that could ' + 'affect the entire repository.'); + } else { + print( + 'Running for all packages that have diffs relative to "$baseSha"\n'); packages = _getChangedPackageNames(changedFiles); } } else if (getBoolArg(_runOnDirtyPackagesArg)) { diff --git a/script/tool/test/common/package_command_test.dart b/script/tool/test/common/package_command_test.dart index be21b86bff7..aa0a2025395 100644 --- a/script/tool/test/common/package_command_test.dart +++ b/script/tool/test/common/package_command_test.dart @@ -408,11 +408,18 @@ packages/plugin1/CHANGELOG createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + + final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('Running for all packages, since a file has changed ' + 'that could affect the entire repository.') + ])); }); test('all plugins should be tested if .ci.yaml changes', () async { @@ -426,11 +433,17 @@ packages/plugin1/CHANGELOG createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('Running for all packages, since a file has changed ' + 'that could affect the entire repository.') + ])); }); test('all plugins should be tested if anything in .ci/ changes', @@ -445,14 +458,20 @@ packages/plugin1/CHANGELOG createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('Running for all packages, since a file has changed ' + 'that could affect the entire repository.') + ])); }); - test('all plugins should be tested if anything in script changes.', + test('all plugins should be tested if anything in script/ changes.', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: ''' @@ -464,11 +483,17 @@ packages/plugin1/CHANGELOG createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('Running for all packages, since a file has changed ' + 'that could affect the entire repository.') + ])); }); test('all plugins should be tested if the root analysis options change.', @@ -483,11 +508,17 @@ packages/plugin1/CHANGELOG createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('Running for all packages, since a file has changed ' + 'that could affect the entire repository.') + ])); }); test('all plugins should be tested if formatting options change.', @@ -502,11 +533,17 @@ packages/plugin1/CHANGELOG createFakePlugin('plugin1', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('Running for all packages, since a file has changed ' + 'that could affect the entire repository.') + ])); }); test('Only changed plugin should be tested.', () async { From 159a3042b2d7c87cad119bd70b94456353b0c9bf Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 7 Oct 2022 18:35:03 -0400 Subject: [PATCH 223/249] [tool] Fix version mistake in CHANGELOG (#6552) --- script/tool/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index f62509040a3..b7864c9c050 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,4 @@ -## 12.0 +## 0.12.0 * Changes the behavior of `--packages-for-branch` on main/master to run for packages changed in the last commit, rather than running for all packages. @@ -6,7 +6,7 @@ tested in presubmit. * Adds a `fix` command to run `dart fix --apply` in target packages. -## 0.11 +## 0.11.0 * Renames `publish-plugin` to `publish`. * Renames arguments to `list`: From d32884097332c3a2b6cc259df1b43f64e30e4630 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Thu, 20 Oct 2022 16:47:37 -0700 Subject: [PATCH 224/249] [tool] Get dependencies in package examples before publish check. (#6596) --- script/tool/CHANGELOG.md | 5 ++ .../tool/lib/src/publish_check_command.dart | 16 ++++++ script/tool/pubspec.yaml | 2 +- .../tool/test/publish_check_command_test.dart | 57 +++++++++++++++++-- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index b7864c9c050..c492dc00905 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.12.1 + +* Modifies `publish_check_command.dart` to do a `dart pub get` in all examples + of the package being checked. Workaround for [dart-lang/pub#3618](https://github.com/dart-lang/pub/issues/3618). + ## 0.12.0 * Changes the behavior of `--packages-for-branch` on main/master to run for diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 38e1b7bdebb..14b240dc04c 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -130,7 +130,23 @@ class PublishCheckCommand extends PackageLoopingCommand { } } + // Run `dart pub get` on the examples of [package]. + Future _fetchExampleDeps(RepositoryPackage package) async { + for (final RepositoryPackage example in package.getExamples()) { + await processRunner.runAndStream( + 'dart', + ['pub', 'get'], + workingDir: example.directory, + ); + } + } + Future _hasValidPublishCheckRun(RepositoryPackage package) async { + // `pub publish` does not do `dart pub get` inside `example` directories + // of a package (but they're part of the analysis output!). + // Issue: https://github.com/flutter/flutter/issues/113788 + await _fetchExampleDeps(package); + print('Running pub publish --dry-run:'); final io.Process process = await processRunner.start( flutterCommand, diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 9e767ad724c..eecff3703b4 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.12.0 +version: 0.12.1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index e6c5b9cdebc..575f8509fd2 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -44,10 +44,16 @@ void main() { }); test('publish check all packages', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin_tools_test_package_a', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin_tools_test_package_b', packagesDir); + final RepositoryPackage plugin1 = createFakePlugin( + 'plugin_tools_test_package_a', + packagesDir, + examples: [], + ); + final RepositoryPackage plugin2 = createFakePlugin( + 'plugin_tools_test_package_b', + packagesDir, + examples: [], + ); await runCapturingPrint(runner, ['publish-check']); @@ -65,6 +71,49 @@ void main() { ])); }); + test('publish prepares dependencies of examples (when present)', () async { + final RepositoryPackage plugin1 = createFakePlugin( + 'plugin_tools_test_package_a', + packagesDir, + examples: ['example1', 'example2'], + ); + final RepositoryPackage plugin2 = createFakePlugin( + 'plugin_tools_test_package_b', + packagesDir, + examples: [], + ); + + await runCapturingPrint(runner, ['publish-check']); + + // For plugin1, these are the expected pub get calls that will happen + final Iterable pubGetCalls = + plugin1.getExamples().map((RepositoryPackage example) { + return ProcessCall( + 'dart', + const ['pub', 'get'], + example.path, + ); + }); + + expect(pubGetCalls, hasLength(2)); + expect( + processRunner.recordedCalls, + orderedEquals([ + // plugin1 has 2 examples, so there's some 'dart pub get' calls. + ...pubGetCalls, + ProcessCall( + 'flutter', + const ['pub', 'publish', '--', '--dry-run'], + plugin1.path), + // plugin2 has no examples, so there's no extra 'dart pub get' calls. + ProcessCall( + 'flutter', + const ['pub', 'publish', '--', '--dry-run'], + plugin2.path), + ]), + ); + }); + test('fail on negative test', () async { createFakePlugin('plugin_tools_test_package_a', packagesDir); From fc3e2b1d1ee4d0a60b29b6305b544b1456771907 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 26 Oct 2022 15:42:05 -0400 Subject: [PATCH 225/249] Update Gradle and AGP in examples to 7.0 (#6625) --- .../tool/lib/src/common/package_state_utils.dart | 15 +++++++++++++++ .../test/common/package_state_utils_test.dart | 16 ++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index a0c82400e1d..65f311974f3 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -173,10 +173,25 @@ Future _isDevChange(List pathComponents, pathComponents.first == 'run_tests.sh' || // Ignoring lints doesn't affect clients. pathComponents.contains('lint-baseline.xml') || + // Example build files are very unlikely to be interesting to clients. + _isExampleBuildFile(pathComponents) || + // Test-only gradle depenedencies don't affect clients. await _isGradleTestDependencyChange(pathComponents, git: git, repoPath: repoPath); } +bool _isExampleBuildFile(List pathComponents) { + if (!pathComponents.contains('example')) { + return false; + } + return pathComponents.contains('gradle-wrapper.properties') || + pathComponents.contains('gradle.properties') || + pathComponents.contains('build.gradle') || + pathComponents.contains('Runner.xcodeproj') || + pathComponents.contains('CMakeLists.txt') || + pathComponents.contains('pubspec.yaml'); +} + Future _isGradleTestDependencyChange(List pathComponents, {GitVersionFinder? git, String? repoPath}) async { if (git == null) { diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart index c20951876e3..c9ae5ba4c74 100644 --- a/script/tool/test/common/package_state_utils_test.dart +++ b/script/tool/test/common/package_state_utils_test.dart @@ -61,13 +61,25 @@ void main() { createFakePlugin('a_plugin', packagesDir); const List changedFiles = [ + 'packages/a_plugin/CHANGELOG.md', + // Analysis. 'packages/a_plugin/example/android/lint-baseline.xml', + // Tests. 'packages/a_plugin/example/android/src/androidTest/foo/bar/FooTest.java', 'packages/a_plugin/example/ios/RunnerTests/Foo.m', 'packages/a_plugin/example/ios/RunnerUITests/info.plist', - 'packages/a_plugin/tool/a_development_tool.dart', + // Test scripts. 'packages/a_plugin/run_tests.sh', - 'packages/a_plugin/CHANGELOG.md', + // Tools. + 'packages/a_plugin/tool/a_development_tool.dart', + // Example build files. + 'packages/a_plugin/example/android/build.gradle', + 'packages/a_plugin/example/android/gradle/wrapper/gradle-wrapper.properties', + 'packages/a_plugin/example/ios/Runner.xcodeproj/project.pbxproj', + 'packages/a_plugin/example/linux/flutter/CMakeLists.txt', + 'packages/a_plugin/example/macos/Runner.xcodeproj/project.pbxproj', + 'packages/a_plugin/example/windows/CMakeLists.txt', + 'packages/a_plugin/example/pubspec.yaml', ]; final PackageChangeState state = await checkPackageChangeState(package, From 60ffcafcd54fc27ce5ef89fbd31bffb2ebd97d13 Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Fri, 28 Oct 2022 17:09:30 +0200 Subject: [PATCH 226/249] [tool] Update tool to set macOS deployment target to 10.15. (#6605) --- .../src/create_all_plugins_app_command.dart | 87 +++++++++++++++++- .../create_all_plugins_app_command_test.dart | 90 +++++++++++++++++++ 2 files changed, 175 insertions(+), 2 deletions(-) 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 a23dc83d98f..ea7d5a5c438 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -6,22 +6,30 @@ import 'dart:io' as io; import 'package:file/file.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/package_command.dart'; +import 'common/process_runner.dart'; import 'common/repository_package.dart'; const String _outputDirectoryFlag = 'output-dir'; +const int _exitUpdateMacosPodfileFailed = 3; +const int _exitUpdateMacosPbxprojFailed = 4; +const int _exitGenNativeBuildFilesFailed = 5; + /// A command to create an application that builds all in a single application. class CreateAllPluginsAppCommand extends PackageCommand { /// Creates an instance of the builder command. CreateAllPluginsAppCommand( Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), Directory? pluginsRoot, - }) : super(packagesDir) { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { final Directory defaultDir = pluginsRoot ?? packagesDir.fileSystem.currentDirectory; argParser.addOption(_outputDirectoryFlag, @@ -61,10 +69,28 @@ class CreateAllPluginsAppCommand extends PackageCommand { print(''); } + await _genPubspecWithAllPlugins(); + + // Run `flutter pub get` to generate all native build files. + // TODO(stuartmorgan): This hangs on Windows for some reason. Since it's + // currently not needed on Windows, skip it there, but we should investigate + // further and/or implement https://github.com/flutter/flutter/issues/93407, + // and remove the need for this conditional. + if (!platform.isWindows) { + if (!await _genNativeBuildFiles()) { + printError( + "Failed to generate native build files via 'flutter pub get'"); + throw ToolExit(_exitGenNativeBuildFilesFailed); + } + } + await Future.wait(>[ - _genPubspecWithAllPlugins(), _updateAppGradle(), _updateManifest(), + _updateMacosPbxproj(), + // This step requires the native file generation triggered by + // flutter pub get above, so can't currently be run on Windows. + if (!platform.isWindows) _updateMacosPodfile(), ]); } @@ -259,4 +285,61 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} return buffer.toString(); } + + Future _genNativeBuildFiles() async { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + ['pub', 'get'], + workingDir: _appDirectory, + ); + return exitCode == 0; + } + + Future _updateMacosPodfile() async { + /// Only change the macOS deployment target if the host platform is macOS. + /// The Podfile is not generated on other platforms. + if (!platform.isMacOS) { + return; + } + + final File podfileFile = + app.platformDirectory(FlutterPlatform.macos).childFile('Podfile'); + if (!podfileFile.existsSync()) { + printError("Can't find Podfile for macOS"); + throw ToolExit(_exitUpdateMacosPodfileFailed); + } + + final StringBuffer newPodfile = StringBuffer(); + for (final String line in podfileFile.readAsLinesSync()) { + if (line.contains('platform :osx')) { + // macOS 10.15 is required by in_app_purchase. + newPodfile.writeln("platform :osx, '10.15'"); + } else { + newPodfile.writeln(line); + } + } + podfileFile.writeAsStringSync(newPodfile.toString()); + } + + Future _updateMacosPbxproj() async { + final File pbxprojFile = app + .platformDirectory(FlutterPlatform.macos) + .childDirectory('Runner.xcodeproj') + .childFile('project.pbxproj'); + if (!pbxprojFile.existsSync()) { + printError("Can't find project.pbxproj for macOS"); + throw ToolExit(_exitUpdateMacosPbxprojFailed); + } + + final StringBuffer newPbxproj = StringBuffer(); + for (final String line in pbxprojFile.readAsLinesSync()) { + if (line.contains('MACOSX_DEPLOYMENT_TARGET')) { + // macOS 10.15 is required by in_app_purchase. + newPbxproj.writeln(' MACOSX_DEPLOYMENT_TARGET = 10.15;'); + } else { + newPbxproj.writeln(line); + } + } + pbxprojFile.writeAsStringSync(newPbxproj.toString()); + } } diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 830dd59a8d4..cb2347fe9cc 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -7,10 +7,12 @@ 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/core.dart'; import 'package:flutter_plugin_tools/src/create_all_plugins_app_command.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { @@ -20,6 +22,7 @@ void main() { late FileSystem fileSystem; late Directory testRoot; late Directory packagesDir; + late RecordingProcessRunner processRunner; setUp(() { // Since the core of this command is a call to 'flutter create', the test @@ -28,9 +31,11 @@ void main() { fileSystem = const LocalFileSystem(); testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); + processRunner = RecordingProcessRunner(); command = CreateAllPluginsAppCommand( packagesDir, + processRunner: processRunner, pluginsRoot: testRoot, ); runner = CommandRunner( @@ -103,6 +108,91 @@ void main() { baselinePubspec.environment?[dartSdkKey]); }); + test('macOS deployment target is modified in Podfile', () async { + createFakePlugin('plugina', packagesDir); + + final File podfileFile = command.packagesDir.parent + .childDirectory('all_plugins') + .childDirectory('macos') + .childFile('Podfile'); + podfileFile.createSync(recursive: true); + podfileFile.writeAsStringSync(""" +platform :osx, '10.11' +# some other line +"""); + + await runCapturingPrint(runner, ['all-plugins-app']); + final List podfile = command.app + .platformDirectory(FlutterPlatform.macos) + .childFile('Podfile') + .readAsLinesSync(); + + expect( + podfile, + everyElement((String line) => + !line.contains('platform :osx') || line.contains("'10.15'"))); + }, + // Podfile is only generated (and thus only edited) on macOS. + skip: !io.Platform.isMacOS); + + test('macOS deployment target is modified in pbxproj', () async { + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['all-plugins-app']); + final List pbxproj = command.app + .platformDirectory(FlutterPlatform.macos) + .childDirectory('Runner.xcodeproj') + .childFile('project.pbxproj') + .readAsLinesSync(); + + expect( + pbxproj, + everyElement((String line) => + !line.contains('MACOSX_DEPLOYMENT_TARGET') || + line.contains('10.15'))); + }); + + test('calls flutter pub get', () async { + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['all-plugins-app']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(const LocalPlatform()), + const ['pub', 'get'], + testRoot.childDirectory('all_plugins').path), + ])); + }, + // See comment about Windows in create_all_plugins_app_command.dart + skip: io.Platform.isWindows); + + test('fails if flutter pub get fails', () async { + createFakePlugin('plugina', packagesDir); + + processRunner.mockProcessesForExecutable[ + getFlutterCommand(const LocalPlatform())] = [ + MockProcess(exitCode: 1) + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['all-plugins-app'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + "Failed to generate native build files via 'flutter pub get'"), + ])); + }, + // See comment about Windows in create_all_plugins_app_command.dart + skip: io.Platform.isWindows); + test('handles --output-dir', () async { createFakePlugin('plugina', packagesDir); From 1469b765ee387c81c79c12b7af170b711112819f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 31 Oct 2022 10:42:58 -0700 Subject: [PATCH 227/249] [tool] Rename all-plugins-app command (#6600) --- script/tool/CHANGELOG.md | 6 + ...t => create_all_packages_app_command.dart} | 23 +-- script/tool/lib/src/main.dart | 4 +- script/tool/pubspec.yaml | 2 +- ...create_all_packages_app_command_test.dart} | 159 +++++++++++------- 5 files changed, 117 insertions(+), 77 deletions(-) rename script/tool/lib/src/{create_all_plugins_app_command.dart => create_all_packages_app_command.dart} (95%) rename script/tool/test/{create_all_plugins_app_command_test.dart => create_all_packages_app_command_test.dart} (63%) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index c492dc00905..e5ad322aeea 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.13.0 + +* Renames `all-plugins-app` to `create-all-packages-app` to clarify what it + actually does. Also renames the project directory it creates from + `all_plugins` to `all_packages`. + ## 0.12.1 * Modifies `publish_check_command.dart` to do a `dart pub get` in all examples diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_packages_app_command.dart similarity index 95% rename from script/tool/lib/src/create_all_plugins_app_command.dart rename to script/tool/lib/src/create_all_packages_app_command.dart index ea7d5a5c438..142a992972c 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_packages_app_command.dart @@ -17,14 +17,16 @@ import 'common/repository_package.dart'; const String _outputDirectoryFlag = 'output-dir'; +const String _projectName = 'all_packages'; + const int _exitUpdateMacosPodfileFailed = 3; const int _exitUpdateMacosPbxprojFailed = 4; const int _exitGenNativeBuildFilesFailed = 5; /// A command to create an application that builds all in a single application. -class CreateAllPluginsAppCommand extends PackageCommand { +class CreateAllPackagesAppCommand extends PackageCommand { /// Creates an instance of the builder command. - CreateAllPluginsAppCommand( + CreateAllPackagesAppCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Directory? pluginsRoot, @@ -34,24 +36,25 @@ class CreateAllPluginsAppCommand extends PackageCommand { pluginsRoot ?? packagesDir.fileSystem.currentDirectory; argParser.addOption(_outputDirectoryFlag, defaultsTo: defaultDir.path, - help: 'The path the directory to create the "all_plugins" project in.\n' + help: + 'The path the directory to create the "$_projectName" project in.\n' 'Defaults to the repository root.'); } /// The location to create the synthesized app project. Directory get _appDirectory => packagesDir.fileSystem .directory(getStringArg(_outputDirectoryFlag)) - .childDirectory('all_plugins'); + .childDirectory(_projectName); /// The synthesized app project. RepositoryPackage get app => RepositoryPackage(_appDirectory); @override String get description => - 'Generate Flutter app that includes all plugins in packages.'; + 'Generate Flutter app that includes all target packagas.'; @override - String get name => 'all-plugins-app'; + String get name => 'create-all-packages-app'; @override Future run() async { @@ -100,7 +103,7 @@ class CreateAllPluginsAppCommand extends PackageCommand { [ 'create', '--template=app', - '--project-name=all_plugins', + '--project-name=$_projectName', '--android-language=java', _appDirectory.path, ], @@ -160,9 +163,9 @@ class CreateAllPluginsAppCommand extends PackageCommand { final StringBuffer newManifest = StringBuffer(); for (final String line in manifestFile.readAsLinesSync()) { - if (line.contains('package="com.example.all_plugins"')) { + if (line.contains('package="com.example.$_projectName"')) { newManifest - ..writeln('package="com.example.all_plugins"') + ..writeln('package="com.example.$_projectName"') ..writeln('xmlns:tools="http://schemas.android.com/tools">') ..writeln() ..writeln( @@ -191,7 +194,7 @@ class CreateAllPluginsAppCommand extends PackageCommand { final Map pluginDeps = await _getValidPathDependencies(); final Pubspec pubspec = Pubspec( - 'all_plugins', + _projectName, description: 'Flutter app containing all 1st party plugins.', version: Version.parse('1.0.0+1'), environment: { diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 414ca7f303c..2d48f079b30 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -11,7 +11,7 @@ import 'package:file/local.dart'; import 'analyze_command.dart'; import 'build_examples_command.dart'; import 'common/core.dart'; -import 'create_all_plugins_app_command.dart'; +import 'create_all_packages_app_command.dart'; import 'custom_test_command.dart'; import 'dependabot_check_command.dart'; import 'drive_examples_command.dart'; @@ -56,7 +56,7 @@ void main(List args) { 'Productivity utils for hosting multiple plugins within one repository.') ..addCommand(AnalyzeCommand(packagesDir)) ..addCommand(BuildExamplesCommand(packagesDir)) - ..addCommand(CreateAllPluginsAppCommand(packagesDir)) + ..addCommand(CreateAllPackagesAppCommand(packagesDir)) ..addCommand(CustomTestCommand(packagesDir)) ..addCommand(DependabotCheckCommand(packagesDir)) ..addCommand(DriveExamplesCommand(packagesDir)) diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index eecff3703b4..fb1bbfe5978 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.12.1 +version: 0.13.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_packages_app_command_test.dart similarity index 63% rename from script/tool/test/create_all_plugins_app_command_test.dart rename to script/tool/test/create_all_packages_app_command_test.dart index cb2347fe9cc..54551cbc371 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_packages_app_command_test.dart @@ -8,7 +8,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/create_all_plugins_app_command.dart'; +import 'package:flutter_plugin_tools/src/create_all_packages_app_command.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; @@ -16,43 +16,58 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$CreateAllPluginsAppCommand', () { - late CommandRunner runner; - late CreateAllPluginsAppCommand command; - late FileSystem fileSystem; - late Directory testRoot; - late Directory packagesDir; - late RecordingProcessRunner processRunner; + late CommandRunner runner; + late CreateAllPackagesAppCommand command; + late FileSystem fileSystem; + late Directory testRoot; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + + setUp(() { + // Since the core of this command is a call to 'flutter create', the test + // has to use the real filesystem. Put everything possible in a unique + // temporary to minimize effect on the host system. + fileSystem = const LocalFileSystem(); + testRoot = fileSystem.systemTempDirectory.createTempSync(); + packagesDir = testRoot.childDirectory('packages'); + processRunner = RecordingProcessRunner(); + + command = CreateAllPackagesAppCommand( + packagesDir, + processRunner: processRunner, + pluginsRoot: testRoot, + ); + runner = CommandRunner( + 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); + runner.addCommand(command); + }); + + tearDown(() { + testRoot.deleteSync(recursive: true); + }); + group('non-macOS host', () { setUp(() { - // Since the core of this command is a call to 'flutter create', the test - // has to use the real filesystem. Put everything possible in a unique - // temporary to minimize effect on the host system. - fileSystem = const LocalFileSystem(); - testRoot = fileSystem.systemTempDirectory.createTempSync(); - packagesDir = testRoot.childDirectory('packages'); - processRunner = RecordingProcessRunner(); - - command = CreateAllPluginsAppCommand( + command = CreateAllPackagesAppCommand( packagesDir, processRunner: processRunner, + // Set isWindows or not based on the actual host, so that + // `flutterCommand` works, since these tests actually call 'flutter'. + // The important thing is that isMacOS always returns false. + platform: MockPlatform(isWindows: const LocalPlatform().isWindows), pluginsRoot: testRoot, ); runner = CommandRunner( - 'create_all_test', 'Test for $CreateAllPluginsAppCommand'); + 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); runner.addCommand(command); }); - tearDown(() { - testRoot.deleteSync(recursive: true); - }); - test('pubspec includes all plugins', () async { createFakePlugin('plugina', packagesDir); createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); - await runCapturingPrint(runner, ['all-plugins-app']); + await runCapturingPrint(runner, ['create-all-packages-app']); final List pubspec = command.app.pubspecFile.readAsLinesSync(); expect( @@ -69,7 +84,7 @@ void main() { createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); - await runCapturingPrint(runner, ['all-plugins-app']); + await runCapturingPrint(runner, ['create-all-packages-app']); final List pubspec = command.app.pubspecFile.readAsLinesSync(); expect( @@ -100,7 +115,7 @@ void main() { createFakePlugin('plugina', packagesDir); - await runCapturingPrint(runner, ['all-plugins-app']); + await runCapturingPrint(runner, ['create-all-packages-app']); final Pubspec generatedPubspec = command.app.parsePubspec(); const String dartSdkKey = 'sdk'; @@ -108,37 +123,10 @@ void main() { baselinePubspec.environment?[dartSdkKey]); }); - test('macOS deployment target is modified in Podfile', () async { - createFakePlugin('plugina', packagesDir); - - final File podfileFile = command.packagesDir.parent - .childDirectory('all_plugins') - .childDirectory('macos') - .childFile('Podfile'); - podfileFile.createSync(recursive: true); - podfileFile.writeAsStringSync(""" -platform :osx, '10.11' -# some other line -"""); - - await runCapturingPrint(runner, ['all-plugins-app']); - final List podfile = command.app - .platformDirectory(FlutterPlatform.macos) - .childFile('Podfile') - .readAsLinesSync(); - - expect( - podfile, - everyElement((String line) => - !line.contains('platform :osx') || line.contains("'10.15'"))); - }, - // Podfile is only generated (and thus only edited) on macOS. - skip: !io.Platform.isMacOS); - test('macOS deployment target is modified in pbxproj', () async { createFakePlugin('plugina', packagesDir); - await runCapturingPrint(runner, ['all-plugins-app']); + await runCapturingPrint(runner, ['create-all-packages-app']); final List pbxproj = command.app .platformDirectory(FlutterPlatform.macos) .childDirectory('Runner.xcodeproj') @@ -155,7 +143,7 @@ platform :osx, '10.11' test('calls flutter pub get', () async { createFakePlugin('plugina', packagesDir); - await runCapturingPrint(runner, ['all-plugins-app']); + await runCapturingPrint(runner, ['create-all-packages-app']); expect( processRunner.recordedCalls, @@ -163,10 +151,10 @@ platform :osx, '10.11' ProcessCall( getFlutterCommand(const LocalPlatform()), const ['pub', 'get'], - testRoot.childDirectory('all_plugins').path), + testRoot.childDirectory('all_packages').path), ])); }, - // See comment about Windows in create_all_plugins_app_command.dart + // See comment about Windows in create_all_packages_app_command.dart skip: io.Platform.isWindows); test('fails if flutter pub get fails', () async { @@ -178,7 +166,7 @@ platform :osx, '10.11' ]; Error? commandError; final List output = await runCapturingPrint( - runner, ['all-plugins-app'], errorHandler: (Error e) { + runner, ['create-all-packages-app'], errorHandler: (Error e) { commandError = e; }); @@ -190,7 +178,7 @@ platform :osx, '10.11' "Failed to generate native build files via 'flutter pub get'"), ])); }, - // See comment about Windows in create_all_plugins_app_command.dart + // See comment about Windows in create_all_packages_app_command.dart skip: io.Platform.isWindows); test('handles --output-dir', () async { @@ -198,11 +186,13 @@ platform :osx, '10.11' final Directory customOutputDir = fileSystem.systemTempDirectory.createTempSync(); - await runCapturingPrint(runner, - ['all-plugins-app', '--output-dir=${customOutputDir.path}']); + await runCapturingPrint(runner, [ + 'create-all-packages-app', + '--output-dir=${customOutputDir.path}' + ]); - expect( - command.app.path, customOutputDir.childDirectory('all_plugins').path); + expect(command.app.path, + customOutputDir.childDirectory('all_packages').path); }); test('logs exclusions', () async { @@ -210,8 +200,8 @@ platform :osx, '10.11' createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); - final List output = await runCapturingPrint( - runner, ['all-plugins-app', '--exclude=pluginb,pluginc']); + final List output = await runCapturingPrint(runner, + ['create-all-packages-app', '--exclude=pluginb,pluginc']); expect( output, @@ -222,4 +212,45 @@ platform :osx, '10.11' ])); }); }); + + group('macOS host', () { + setUp(() { + command = CreateAllPackagesAppCommand( + packagesDir, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + pluginsRoot: testRoot, + ); + runner = CommandRunner( + 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); + runner.addCommand(command); + }); + + test('macOS deployment target is modified in Podfile', () async { + createFakePlugin('plugina', packagesDir); + + final File podfileFile = RepositoryPackage( + command.packagesDir.parent.childDirectory('all_packages')) + .platformDirectory(FlutterPlatform.macos) + .childFile('Podfile'); + podfileFile.createSync(recursive: true); + podfileFile.writeAsStringSync(""" +platform :osx, '10.11' +# some other line +"""); + + await runCapturingPrint(runner, ['create-all-packages-app']); + final List podfile = command.app + .platformDirectory(FlutterPlatform.macos) + .childFile('Podfile') + .readAsLinesSync(); + + expect( + podfile, + everyElement((String line) => + !line.contains('platform :osx') || line.contains("'10.15'"))); + }, + // Podfile is only generated (and thus only edited) on macOS. + skip: !io.Platform.isMacOS); + }); } From 6a0c9ef00d043a2b2e63a83aa3591cda0e1f3cdb Mon Sep 17 00:00:00 2001 From: Phil Quitslund Date: Fri, 4 Nov 2022 06:31:18 -0700 Subject: [PATCH 228/249] [ci] Removes unnecessary overrides. (#6669) * remove unnecessary overrides * Update mock so tools_tests checks pass. * Update CHANGELOG NEXT entries. * Pin git to 2.0.x Co-authored-by: David Iglesias Teixeira --- script/tool/CHANGELOG.md | 6 + script/tool/pubspec.yaml | 3 +- .../common/package_command_test.mocks.dart | 313 +++++++++++++----- 3 files changed, 236 insertions(+), 86 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index e5ad322aeea..a346ac093c6 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## NEXT + +* Pins `package:git` dependency to `2.0.x` until `dart >=2.18.0` becomes our + oldest legacy. +* Updates test mocks. + ## 0.13.0 * Renames `all-plugins-app` to `create-all-packages-app` to clarify what it diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index fb1bbfe5978..e450a1114e8 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -9,7 +9,8 @@ dependencies: collection: ^1.15.0 colorize: ^3.0.0 file: ^6.1.0 - git: ^2.0.0 + # Pin git to 2.0.x until dart >=2.18 is legacy + git: '>=2.0.0 <2.1.0' http: ^0.13.3 http_multi_server: ^3.0.1 meta: ^1.3.0 diff --git a/script/tool/test/common/package_command_test.mocks.dart b/script/tool/test/common/package_command_test.mocks.dart index b7f7807b3b0..79c5d4df1a8 100644 --- a/script/tool/test/common/package_command_test.mocks.dart +++ b/script/tool/test/common/package_command_test.mocks.dart @@ -1,7 +1,8 @@ -// Mocks generated by Mockito 5.0.7 from annotations -// in flutter_plugin_tools/test/common_test.dart. +// Mocks generated by Mockito 5.3.2 from annotations +// in flutter_plugin_tools/test/common/package_command_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i6; import 'dart:io' as _i4; @@ -13,18 +14,47 @@ import 'package:git/src/tag.dart' as _i7; import 'package:git/src/tree_entry.dart' as _i9; import 'package:mockito/mockito.dart' as _i1; +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references -// ignore_for_file: unnecessary_parenthesis - +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class -// ignore_for_file: avoid_redundant_argument_values - -class _FakeCommit extends _i1.Fake implements _i2.Commit {} +class _FakeCommit_0 extends _i1.SmartFake implements _i2.Commit { + _FakeCommit_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeBranchReference extends _i1.Fake implements _i3.BranchReference {} +class _FakeBranchReference_1 extends _i1.SmartFake + implements _i3.BranchReference { + _FakeBranchReference_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeProcessResult extends _i1.Fake implements _i4.ProcessResult {} +class _FakeProcessResult_2 extends _i1.SmartFake implements _i4.ProcessResult { + _FakeProcessResult_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} /// A class which mocks [GitDir]. /// @@ -35,109 +65,222 @@ class MockGitDir extends _i1.Mock implements _i5.GitDir { } @override - String get path => - (super.noSuchMethod(Invocation.getter(#path), returnValue: '') as String); + String get path => (super.noSuchMethod( + Invocation.getter(#path), + returnValue: '', + ) as String); @override _i6.Future commitCount([String? branchName = r'HEAD']) => - (super.noSuchMethod(Invocation.method(#commitCount, [branchName]), - returnValue: Future.value(0)) as _i6.Future); + (super.noSuchMethod( + Invocation.method( + #commitCount, + [branchName], + ), + returnValue: _i6.Future.value(0), + ) as _i6.Future); @override _i6.Future<_i2.Commit> commitFromRevision(String? revision) => - (super.noSuchMethod(Invocation.method(#commitFromRevision, [revision]), - returnValue: Future<_i2.Commit>.value(_FakeCommit())) - as _i6.Future<_i2.Commit>); + (super.noSuchMethod( + Invocation.method( + #commitFromRevision, + [revision], + ), + returnValue: _i6.Future<_i2.Commit>.value(_FakeCommit_0( + this, + Invocation.method( + #commitFromRevision, + [revision], + ), + )), + ) as _i6.Future<_i2.Commit>); @override _i6.Future> commits([String? branchName = r'HEAD']) => - (super.noSuchMethod(Invocation.method(#commits, [branchName]), - returnValue: - Future>.value({})) - as _i6.Future>); + (super.noSuchMethod( + Invocation.method( + #commits, + [branchName], + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); @override _i6.Future<_i3.BranchReference?> branchReference(String? branchName) => - (super.noSuchMethod(Invocation.method(#branchReference, [branchName]), - returnValue: - Future<_i3.BranchReference?>.value(_FakeBranchReference())) - as _i6.Future<_i3.BranchReference?>); + (super.noSuchMethod( + Invocation.method( + #branchReference, + [branchName], + ), + returnValue: _i6.Future<_i3.BranchReference?>.value(), + ) as _i6.Future<_i3.BranchReference?>); @override _i6.Future> branches() => (super.noSuchMethod( - Invocation.method(#branches, []), - returnValue: - Future>.value(<_i3.BranchReference>[])) - as _i6.Future>); + Invocation.method( + #branches, + [], + ), + returnValue: _i6.Future>.value( + <_i3.BranchReference>[]), + ) as _i6.Future>); @override - _i6.Stream<_i7.Tag> tags() => - (super.noSuchMethod(Invocation.method(#tags, []), - returnValue: Stream<_i7.Tag>.empty()) as _i6.Stream<_i7.Tag>); + _i6.Stream<_i7.Tag> tags() => (super.noSuchMethod( + Invocation.method( + #tags, + [], + ), + returnValue: _i6.Stream<_i7.Tag>.empty(), + ) as _i6.Stream<_i7.Tag>); @override - _i6.Future> showRef( - {bool? heads = false, bool? tags = false}) => + _i6.Future> showRef({ + bool? heads = false, + bool? tags = false, + }) => (super.noSuchMethod( - Invocation.method(#showRef, [], {#heads: heads, #tags: tags}), - returnValue: Future>.value( - <_i8.CommitReference>[])) - as _i6.Future>); - @override - _i6.Future<_i3.BranchReference> currentBranch() => - (super.noSuchMethod(Invocation.method(#currentBranch, []), - returnValue: - Future<_i3.BranchReference>.value(_FakeBranchReference())) - as _i6.Future<_i3.BranchReference>); - @override - _i6.Future> lsTree(String? treeish, - {bool? subTreesOnly = false, String? path}) => + Invocation.method( + #showRef, + [], + { + #heads: heads, + #tags: tags, + }, + ), + returnValue: _i6.Future>.value( + <_i8.CommitReference>[]), + ) as _i6.Future>); + @override + _i6.Future<_i3.BranchReference> currentBranch() => (super.noSuchMethod( + Invocation.method( + #currentBranch, + [], + ), + returnValue: + _i6.Future<_i3.BranchReference>.value(_FakeBranchReference_1( + this, + Invocation.method( + #currentBranch, + [], + ), + )), + ) as _i6.Future<_i3.BranchReference>); + @override + _i6.Future> lsTree( + String? treeish, { + bool? subTreesOnly = false, + String? path, + }) => (super.noSuchMethod( - Invocation.method(#lsTree, [treeish], - {#subTreesOnly: subTreesOnly, #path: path}), - returnValue: Future>.value(<_i9.TreeEntry>[])) - as _i6.Future>); + Invocation.method( + #lsTree, + [treeish], + { + #subTreesOnly: subTreesOnly, + #path: path, + }, + ), + returnValue: _i6.Future>.value(<_i9.TreeEntry>[]), + ) as _i6.Future>); @override _i6.Future createOrUpdateBranch( - String? branchName, String? treeSha, String? commitMessage) => + String? branchName, + String? treeSha, + String? commitMessage, + ) => (super.noSuchMethod( - Invocation.method( - #createOrUpdateBranch, [branchName, treeSha, commitMessage]), - returnValue: Future.value('')) as _i6.Future); + Invocation.method( + #createOrUpdateBranch, + [ + branchName, + treeSha, + commitMessage, + ], + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i6.Future commitTree(String? treeSha, String? commitMessage, - {List? parentCommitShas}) => + _i6.Future commitTree( + String? treeSha, + String? commitMessage, { + List? parentCommitShas, + }) => (super.noSuchMethod( - Invocation.method(#commitTree, [treeSha, commitMessage], - {#parentCommitShas: parentCommitShas}), - returnValue: Future.value('')) as _i6.Future); + Invocation.method( + #commitTree, + [ + treeSha, + commitMessage, + ], + {#parentCommitShas: parentCommitShas}, + ), + returnValue: _i6.Future.value(''), + ) as _i6.Future); @override _i6.Future> writeObjects(List? paths) => - (super.noSuchMethod(Invocation.method(#writeObjects, [paths]), - returnValue: - Future>.value({})) - as _i6.Future>); + (super.noSuchMethod( + Invocation.method( + #writeObjects, + [paths], + ), + returnValue: _i6.Future>.value({}), + ) as _i6.Future>); @override - _i6.Future<_i4.ProcessResult> runCommand(Iterable? args, - {bool? throwOnError = true}) => + _i6.Future<_i4.ProcessResult> runCommand( + Iterable? args, { + bool? throwOnError = true, + }) => (super.noSuchMethod( - Invocation.method(#runCommand, [args], {#throwOnError: throwOnError}), - returnValue: - Future<_i4.ProcessResult>.value(_FakeProcessResult())) as _i6 - .Future<_i4.ProcessResult>); + Invocation.method( + #runCommand, + [args], + {#throwOnError: throwOnError}, + ), + returnValue: _i6.Future<_i4.ProcessResult>.value(_FakeProcessResult_2( + this, + Invocation.method( + #runCommand, + [args], + {#throwOnError: throwOnError}, + ), + )), + ) as _i6.Future<_i4.ProcessResult>); @override - _i6.Future isWorkingTreeClean() => - (super.noSuchMethod(Invocation.method(#isWorkingTreeClean, []), - returnValue: Future.value(false)) as _i6.Future); + _i6.Future isWorkingTreeClean() => (super.noSuchMethod( + Invocation.method( + #isWorkingTreeClean, + [], + ), + returnValue: _i6.Future.value(false), + ) as _i6.Future); @override _i6.Future<_i2.Commit?> updateBranch( - String? branchName, - _i6.Future Function(_i4.Directory)? populater, - String? commitMessage) => + String? branchName, + _i6.Future Function(_i4.Directory)? populater, + String? commitMessage, + ) => (super.noSuchMethod( - Invocation.method( - #updateBranch, [branchName, populater, commitMessage]), - returnValue: Future<_i2.Commit?>.value(_FakeCommit())) - as _i6.Future<_i2.Commit?>); + Invocation.method( + #updateBranch, + [ + branchName, + populater, + commitMessage, + ], + ), + returnValue: _i6.Future<_i2.Commit?>.value(), + ) as _i6.Future<_i2.Commit?>); @override - _i6.Future<_i2.Commit?> updateBranchWithDirectoryContents(String? branchName, - String? sourceDirectoryPath, String? commitMessage) => + _i6.Future<_i2.Commit?> updateBranchWithDirectoryContents( + String? branchName, + String? sourceDirectoryPath, + String? commitMessage, + ) => (super.noSuchMethod( - Invocation.method(#updateBranchWithDirectoryContents, - [branchName, sourceDirectoryPath, commitMessage]), - returnValue: Future<_i2.Commit?>.value(_FakeCommit())) - as _i6.Future<_i2.Commit?>); + Invocation.method( + #updateBranchWithDirectoryContents, + [ + branchName, + sourceDirectoryPath, + commitMessage, + ], + ), + returnValue: _i6.Future<_i2.Commit?>.value(), + ) as _i6.Future<_i2.Commit?>); } From d404da6e85c689338b42d464dcf58714518b58d0 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 21 Nov 2022 13:49:06 -0500 Subject: [PATCH 229/249] [ci] Improve analysis_options alignment with flutter/packages (#6728) * Add more options that are in flutter/packages * Fix unnecessary awaits * More option alignment * Add and locally supress avoid_implementing_value_types * Fix release-info for test-only changes * Fix update-release-info handling of 'minimal' * Update release metadata --- .../src/common/package_looping_command.dart | 2 +- .../lib/src/common/package_state_utils.dart | 1 + .../lib/src/update_release_info_command.dart | 3 ++ .../tool/lib/src/version_check_command.dart | 3 +- .../common/package_looping_command_test.dart | 2 +- .../update_release_info_command_test.dart | 31 ++++++++++++++++++- 6 files changed, 37 insertions(+), 5 deletions(-) diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index d8b1cf001d1..ccfeea0e473 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -369,7 +369,7 @@ abstract class PackageLoopingCommand extends PackageCommand { } } - return await runForPackage(package); + return runForPackage(package); } void _printSuccess(String message) { diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index 65f311974f3..5aabd75a4d3 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -107,6 +107,7 @@ Future checkPackageChangeState( bool _isTestChange(List pathComponents) { return pathComponents.contains('test') || + pathComponents.contains('integration_test') || pathComponents.contains('androidTest') || pathComponents.contains('RunnerTests') || pathComponents.contains('RunnerUITests'); diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart index 67aa994d963..8d7ceb84d31 100644 --- a/script/tool/lib/src/update_release_info_command.dart +++ b/script/tool/lib/src/update_release_info_command.dart @@ -140,6 +140,9 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand { if (!state.hasChanges) { return PackageResult.skip('No changes to package'); } + if (!state.needsVersionChange && !state.needsChangelogChange) { + return PackageResult.skip('No non-exempt changes to package'); + } if (state.needsVersionChange) { versionChange = _VersionIncrementType.bugfix; } diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index b3be672066d..bb53620f06d 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -285,8 +285,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final String gitPath = path.style == p.Style.windows ? p.posix.joinAll(path.split(relativePath)) : relativePath; - return await _gitVersionFinder.getPackageVersion(gitPath, - gitRef: _mergeBase); + return _gitVersionFinder.getPackageVersion(gitPath, gitRef: _mergeBase); } /// Returns the state of the verison of [package] relative to the comparison diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index c858df0022c..f90d58e1227 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -143,7 +143,7 @@ void main() { runner = CommandRunner('test_package_looping_command', 'Test for base package looping functionality'); runner.addCommand(command); - return await runCapturingPrint( + return runCapturingPrint( runner, [command.name, ...arguments], errorHandler: errorHandler, diff --git a/script/tool/test/update_release_info_command_test.dart b/script/tool/test/update_release_info_command_test.dart index 8cd2e9591e7..cfec93823ff 100644 --- a/script/tool/test/update_release_info_command_test.dart +++ b/script/tool/test/update_release_info_command_test.dart @@ -388,7 +388,7 @@ $originalChangelog'''; createFakePackage('a_package', packagesDir, version: '1.0.1'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: ''' -packages/different_package/test/plugin_test.dart +packages/different_package/lib/foo.dart '''), ]; final String originalChangelog = package.changelogFile.readAsStringSync(); @@ -411,6 +411,35 @@ packages/different_package/test/plugin_test.dart ])); }); + test('skips for "minimal" when there are only test changes', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/a_package/test/a_test.dart +packages/a_package/example/integration_test/another_test.dart +'''), + ]; + final String originalChangelog = package.changelogFile.readAsStringSync(); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minimal', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.1'); + expect(package.changelogFile.readAsStringSync(), originalChangelog); + expect( + output, + containsAllInOrder([ + contains('No non-exempt changes to package'), + contains('Skipped 1 package') + ])); + }); + test('fails if CHANGELOG.md is missing', () async { createFakePackage('a_package', packagesDir, includeCommonFiles: false); From 3306027234b9d7af8867a814c05d2937932530bf Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sat, 10 Dec 2022 20:09:32 -0500 Subject: [PATCH 230/249] [tools] Recognize Pigeon tests in version-check (#6813) Pigeon has an usual test structure since it generates test code to run in a dummy plugin; add that structure to the list of recognized tests so that changes to its platform tests won't be flagged by `version-check`. --- script/tool/CHANGELOG.md | 3 ++- script/tool/lib/src/common/package_state_utils.dart | 4 +++- script/tool/pubspec.yaml | 2 +- script/tool/test/common/package_state_utils_test.dart | 3 +++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a346ac093c6..021259083af 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 13.1 +* Updates `version-check` to recognize Pigeon's platform test structure. * Pins `package:git` dependency to `2.0.x` until `dart >=2.18.0` becomes our oldest legacy. * Updates test mocks. diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index 5aabd75a4d3..464dac6c18d 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -110,7 +110,9 @@ bool _isTestChange(List pathComponents) { pathComponents.contains('integration_test') || pathComponents.contains('androidTest') || pathComponents.contains('RunnerTests') || - pathComponents.contains('RunnerUITests'); + pathComponents.contains('RunnerUITests') || + // Pigeon's custom platform tests. + pathComponents.first == 'platform_tests'; } // True if the given file is an example file other than the one that will be diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index e450a1114e8..2879ff12502 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.13.0 +version: 0.13.1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart index c9ae5ba4c74..86029cdf73a 100644 --- a/script/tool/test/common/package_state_utils_test.dart +++ b/script/tool/test/common/package_state_utils_test.dart @@ -80,6 +80,9 @@ void main() { 'packages/a_plugin/example/macos/Runner.xcodeproj/project.pbxproj', 'packages/a_plugin/example/windows/CMakeLists.txt', 'packages/a_plugin/example/pubspec.yaml', + // Pigeon platform tests, which have an unusual structure. + 'packages/a_plugin/platform_tests/shared_test_plugin_code/lib/integration_tests.dart', + 'packages/a_plugin/platform_tests/test_plugin/windows/test_plugin.cpp', ]; final PackageChangeState state = await checkPackageChangeState(package, From a50448ac47249b82e19e4ccf7a8cece31d816965 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 14 Dec 2022 15:28:44 -0500 Subject: [PATCH 231/249] [various] Enable avoid_print (#6842) * [various] Enable avoid_print Enables the `avoid_print` lint, and fixes violations (mostly by opting example files out of it). * Version bumps * Add tooling analysis option file that was accidentally omitted * Fix typo in analysis_options found by adding tool sub-options * Revert most version bumps * Fix ios_platform_images --- script/tool/analysis_options.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 script/tool/analysis_options.yaml diff --git a/script/tool/analysis_options.yaml b/script/tool/analysis_options.yaml new file mode 100644 index 00000000000..efd40175a20 --- /dev/null +++ b/script/tool/analysis_options.yaml @@ -0,0 +1,5 @@ +include: ../../analysis_options.yaml + +linter: + rules: + avoid_print: false # The tool is a CLI, so printing is normal From c1ec012a2212e36cdc10744d7fbfcdcea2ebcde1 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 16 Dec 2022 13:05:49 -0800 Subject: [PATCH 232/249] [flutter_plugin_tools] If `clang-format` does not run, fall back to other executables in PATH (#6853) * If clang-format does not run, fall back to other executables in PATH * Review edits --- script/tool/CHANGELOG.md | 6 ++- .../src/create_all_packages_app_command.dart | 2 +- script/tool/lib/src/format_command.dart | 53 +++++++++++++++---- script/tool/lib/src/main.dart | 2 +- script/tool/pubspec.yaml | 2 +- script/tool/test/format_command_test.dart | 38 +++++++++++++ 6 files changed, 90 insertions(+), 13 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 021259083af..072d661f8d9 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,8 @@ -## 13.1 +## 0.13.2 + +* Falls back to other executables in PATH when `clang-format` does not run. + +## 0.13.1 * Updates `version-check` to recognize Pigeon's platform test structure. * Pins `package:git` dependency to `2.0.x` until `dart >=2.18.0` becomes our diff --git a/script/tool/lib/src/create_all_packages_app_command.dart b/script/tool/lib/src/create_all_packages_app_command.dart index 142a992972c..12ec17da139 100644 --- a/script/tool/lib/src/create_all_packages_app_command.dart +++ b/script/tool/lib/src/create_all_packages_app_command.dart @@ -230,7 +230,7 @@ class CreateAllPackagesAppCommand extends PackageCommand { String _pubspecToString(Pubspec pubspec) { return ''' -### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. +### Generated file. Do not edit. Run `dart pub global run flutter_plugin_tools gen-pubspec` to update. name: ${pubspec.name} description: ${pubspec.description} publish_to: none diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index cc6936c566e..8198f6d36ab 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -104,8 +104,8 @@ class FormatCommand extends PackageCommand { print('These files are not formatted correctly (see diff below):'); LineSplitter.split(stdout).map((String line) => ' $line').forEach(print); - print('\nTo fix run "pub global activate flutter_plugin_tools && ' - 'pub global run flutter_plugin_tools format" or copy-paste ' + print('\nTo fix run "dart pub global activate flutter_plugin_tools && ' + 'dart pub global run flutter_plugin_tools format" or copy-paste ' 'this command into your terminal:'); final io.ProcessResult diff = await processRunner.run( @@ -128,16 +128,11 @@ class FormatCommand extends PackageCommand { final Iterable clangFiles = _getPathsWithExtensions( files, {'.h', '.m', '.mm', '.cc', '.cpp'}); if (clangFiles.isNotEmpty) { - final String clangFormat = getStringArg('clang-format'); - if (!await _hasDependency(clangFormat)) { - printError('Unable to run "clang-format". Make sure that it is in your ' - 'path, or provide a full path with --clang-format.'); - throw ToolExit(_exitDependencyMissing); - } + final String clangFormat = await _findValidClangFormat(); print('Formatting .cc, .cpp, .h, .m, and .mm files...'); final int exitCode = await _runBatched( - getStringArg('clang-format'), ['-i', '--style=file'], + clangFormat, ['-i', '--style=file'], files: clangFiles); if (exitCode != 0) { printError( @@ -147,6 +142,26 @@ class FormatCommand extends PackageCommand { } } + Future _findValidClangFormat() async { + final String clangFormatArg = getStringArg('clang-format'); + if (await _hasDependency(clangFormatArg)) { + return clangFormatArg; + } + + // There is a known issue where "chromium/depot_tools/clang-format" + // fails with "Problem while looking for clang-format in Chromium source tree". + // Loop through all "clang-format"s in PATH until a working one is found, + // for example "/usr/local/bin/clang-format" or a "brew" installed version. + for (final String clangFormatPath in await _whichAll('clang-format')) { + if (await _hasDependency(clangFormatPath)) { + return clangFormatPath; + } + } + printError('Unable to run "clang-format". Make sure that it is in your ' + 'path, or provide a full path with --clang-format.'); + throw ToolExit(_exitDependencyMissing); + } + Future _formatJava( Iterable files, String googleFormatterPath) async { final Iterable javaFiles = @@ -279,6 +294,26 @@ class FormatCommand extends PackageCommand { return true; } + /// Returns all instances of [command] executable found on user path. + Future> _whichAll(String command) async { + try { + final io.ProcessResult result = + await processRunner.run('which', ['-a', command]); + + if (result.exitCode != 0) { + return []; + } + + final String stdout = result.stdout.trim() as String; + if (stdout.isEmpty) { + return []; + } + return LineSplitter.split(stdout).toList(); + } on io.ProcessException { + return []; + } + } + /// Runs [command] on [arguments] on all of the files in [files], batched as /// necessary to avoid OS command-line length limits. /// diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 2d48f079b30..d5ab7f88089 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -52,7 +52,7 @@ void main(List args) { } final CommandRunner commandRunner = CommandRunner( - 'pub global run flutter_plugin_tools', + 'dart pub global run flutter_plugin_tools', 'Productivity utils for hosting multiple plugins within one repository.') ..addCommand(AnalyzeCommand(packagesDir)) ..addCommand(BuildExamplesCommand(packagesDir)) diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 2879ff12502..ea20364d51d 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.13.1 +version: 0.13.2 dependencies: args: ^2.1.0 diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index 9a865053a2b..1aadafbd3d8 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -332,6 +332,44 @@ void main() { ])); }); + test('falls back to working clang-format in the path', () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + final RepositoryPackage plugin = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + processRunner.mockProcessesForExecutable['clang-format'] = [ + MockProcess(exitCode: 1) + ]; + processRunner.mockProcessesForExecutable['which'] = [ + MockProcess( + stdout: '/usr/local/bin/clang-format\n/path/to/working-clang-format') + ]; + processRunner.mockProcessesForExecutable['/usr/local/bin/clang-format'] = + [MockProcess(exitCode: 1)]; + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + containsAll([ + const ProcessCall( + '/path/to/working-clang-format', ['--version'], null), + ProcessCall( + '/path/to/working-clang-format', + [ + '-i', + '--style=file', + ...getPackagesDirRelativePaths(plugin, files) + ], + packagesDir.path), + ])); + }); + test('honors --clang-format flag', () async { const List files = [ 'windows/foo_plugin.cpp', From 1a1865e10a18dcc726ce1842e6aea0f5b5feb809 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 9 Jan 2023 10:02:11 -0800 Subject: [PATCH 233/249] [tool] Don't add Guava in the all-packages app (#6747) It's not clear why we are adding an outdated version of Guava; it is likely cruft, so this test removing it. --- script/tool/lib/src/create_all_packages_app_command.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/script/tool/lib/src/create_all_packages_app_command.dart b/script/tool/lib/src/create_all_packages_app_command.dart index 12ec17da139..4660413bc93 100644 --- a/script/tool/lib/src/create_all_packages_app_command.dart +++ b/script/tool/lib/src/create_all_packages_app_command.dart @@ -138,9 +138,6 @@ class CreateAllPackagesAppCommand extends PackageCommand { if (line.contains('defaultConfig {')) { newGradle.writeln(' multiDexEnabled true'); } else if (line.contains('dependencies {')) { - newGradle.writeln( - " implementation 'com.google.guava:guava:27.0.1-android'\n", - ); // Tests for https://github.com/flutter/flutter/issues/43383 newGradle.writeln( " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", From 5dae5989182021eae6eaf2946a14b7c27544d4bb Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 10 Jan 2023 06:17:39 -0800 Subject: [PATCH 234/249] [various] Enable `avoid_dynamic_calls` (#6834) * Enable the option * Fix camera * Fix webview * Remove unnecessary 'call's from camera tests * Fix maps * Fix sign-in * fix image_picker * Fix IAP * Fix shared_preferences * Fix url_launcher_android * Version bumps * Fix tool * Re-apply webview test fix * Re-bump versions * Fix one new tool issue --- .../lib/src/common/git_version_finder.dart | 3 ++- .../lib/src/common/pub_version_finder.dart | 12 ++++++----- .../src/create_all_packages_app_command.dart | 21 +++++++++---------- .../lib/src/dependabot_check_command.dart | 5 +++-- script/tool/lib/src/format_command.dart | 4 ++-- .../tool/lib/src/pubspec_check_command.dart | 15 ++++++------- script/tool/lib/src/readme_check_command.dart | 3 ++- .../test/common/git_version_finder_test.dart | 8 ++++--- .../common/package_looping_command_test.dart | 4 +++- 9 files changed, 42 insertions(+), 33 deletions(-) diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index b135424827a..3965ae0ace4 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -88,7 +88,8 @@ class GitVersionFinder { if (fileContent.trim().isEmpty) { return null; } - final String? versionString = loadYaml(fileContent)['version'] as String?; + final YamlMap fileYaml = loadYaml(fileContent) as YamlMap; + final String? versionString = fileYaml['version'] as String?; return versionString == null ? null : Version.parse(versionString); } diff --git a/script/tool/lib/src/common/pub_version_finder.dart b/script/tool/lib/src/common/pub_version_finder.dart index 572cb913aa7..c24ec429f8a 100644 --- a/script/tool/lib/src/common/pub_version_finder.dart +++ b/script/tool/lib/src/common/pub_version_finder.dart @@ -44,11 +44,13 @@ class PubVersionFinder { 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(); + final Map responseBody = + json.decode(response.body) as Map; + final List versions = (responseBody['versions']! as List) + .cast() + .map( + (final String versionString) => Version.parse(versionString)) + .toList(); return PubVersionFinderResponse( versions: versions, diff --git a/script/tool/lib/src/create_all_packages_app_command.dart b/script/tool/lib/src/create_all_packages_app_command.dart index 4660413bc93..e7719e9f664 100644 --- a/script/tool/lib/src/create_all_packages_app_command.dart +++ b/script/tool/lib/src/create_all_packages_app_command.dart @@ -244,24 +244,23 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} ###'''; } - String _pubspecMapString(Map values) { + String _pubspecMapString(Map values) { final StringBuffer buffer = StringBuffer(); - for (final MapEntry entry in values.entries) { + for (final MapEntry entry in values.entries) { buffer.writeln(); - if (entry.value is VersionConstraint) { - String value = entry.value.toString(); + final Object? entryValue = entry.value; + if (entryValue is VersionConstraint) { + String value = entryValue.toString(); // Range constraints require quoting. if (value.startsWith('>') || value.startsWith('<')) { value = "'$value'"; } buffer.write(' ${entry.key}: $value'); - } else if (entry.value is SdkDependency) { - final SdkDependency dep = entry.value as SdkDependency; - buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); - } else if (entry.value is PathDependency) { - final PathDependency dep = entry.value as PathDependency; - String depPath = dep.path; + } else if (entryValue is SdkDependency) { + buffer.write(' ${entry.key}: \n sdk: ${entryValue.sdk}'); + } else if (entryValue is PathDependency) { + String depPath = entryValue.path; if (path.style == p.Style.windows) { // Posix-style path separators are preferred in pubspec.yaml (and // using a consistent format makes unit testing simpler), so convert. @@ -278,7 +277,7 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} buffer.write(' ${entry.key}: \n path: $depPath'); } else { throw UnimplementedError( - 'Not available for type: ${entry.value.runtimeType}', + 'Not available for type: ${entryValue.runtimeType}', ); } } diff --git a/script/tool/lib/src/dependabot_check_command.dart b/script/tool/lib/src/dependabot_check_command.dart index 5aa762e916e..77b44e11b59 100644 --- a/script/tool/lib/src/dependabot_check_command.dart +++ b/script/tool/lib/src/dependabot_check_command.dart @@ -58,8 +58,9 @@ class DependabotCheckCommand extends PackageLoopingCommand { const String typeKey = 'package-ecosystem'; const String dirKey = 'directory'; _gradleDirs = entries - .where((dynamic entry) => entry[typeKey] == 'gradle') - .map((dynamic entry) => (entry as YamlMap)[dirKey] as String) + .cast() + .where((YamlMap entry) => entry[typeKey] == 'gradle') + .map((YamlMap entry) => entry[dirKey] as String) .toSet(); } diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 8198f6d36ab..43c450cbcf7 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -298,13 +298,13 @@ class FormatCommand extends PackageCommand { Future> _whichAll(String command) async { try { final io.ProcessResult result = - await processRunner.run('which', ['-a', command]); + await processRunner.run('which', ['-a', command]); if (result.exitCode != 0) { return []; } - final String stdout = result.stdout.trim() as String; + final String stdout = (result.stdout as String).trim(); if (stdout.isEmpty) { return []; } diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 79ef1e1d3e5..5682ba05768 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -244,8 +244,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { required RepositoryPackage package, }) { if (_isImplementationPackage(package)) { - final String? implements = - pubspec.flutter!['plugin']!['implements'] as String?; + final YamlMap pluginSection = pubspec.flutter!['plugin'] as YamlMap; + final String? implements = pluginSection['implements'] as String?; final String expectedImplements = package.directory.parent.basename; if (implements == null) { return 'Missing "implements: $expectedImplements" in "plugin" section.'; @@ -265,19 +265,20 @@ class PubspecCheckCommand extends PackageLoopingCommand { Pubspec pubspec, { required RepositoryPackage package, }) { - final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms']; - if (platformsEntry == null) { + final YamlMap pluginSection = pubspec.flutter!['plugin'] as YamlMap; + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; + if (platforms == null) { logWarning('Does not implement any platforms'); return null; } - final YamlMap platforms = platformsEntry as YamlMap; final String packageName = package.directory.basename; // Validate that the default_package entries look correct (e.g., no typos). final Set defaultPackages = {}; - for (final MapEntry platformEntry in platforms.entries) { + for (final MapEntry platformEntry in platforms.entries) { + final YamlMap platformDetails = platformEntry.value! as YamlMap; final String? defaultPackage = - platformEntry.value['default_package'] as String?; + platformDetails['default_package'] as String?; if (defaultPackage != null) { defaultPackages.add(defaultPackage); if (!defaultPackage.startsWith('${packageName}_')) { diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index e3fbc7bc454..cbbb8b835a1 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -234,7 +234,8 @@ class ReadmeCheckCommand extends PackageLoopingCommand { } // Validate that the supported OS lists match. - final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms']; + final YamlMap pluginSection = pubspec.flutter!['plugin'] as YamlMap; + final dynamic platformsEntry = pluginSection['platforms']; if (platformsEntry == null) { logWarning('Plugin not support any platforms'); return null; diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart index d5a5dd4fe87..538b72a9002 100644 --- a/script/tool/test/common/git_version_finder_test.dart +++ b/script/tool/test/common/git_version_finder_test.dart @@ -22,12 +22,14 @@ void main() { gitDir = MockGitDir(); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0] as List?); + final List arguments = + invocation.positionalArguments[0]! as List; + gitDirCommands.add(arguments); final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { + if (arguments[0] == 'diff') { when(mockProcessResult.stdout as String?) .thenReturn(gitDiffResponse); - } else if (invocation.positionalArguments[0][0] == 'merge-base') { + } else if (arguments[0] == 'merge-base') { when(mockProcessResult.stdout as String?) .thenReturn(mergeBaseResponse); } diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index f90d58e1227..34f346c62fe 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -110,8 +110,10 @@ void main() { final MockGitDir gitDir = MockGitDir(); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { + if (arguments[0] == 'diff') { when(mockProcessResult.stdout as String?) .thenReturn(gitDiffResponse); } From d6f71e464cb2560567ec8ed09449c522041cd431 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Tue, 10 Jan 2023 07:45:00 -0800 Subject: [PATCH 235/249] == override parameters are non-nullable (#6900) --- script/tool/test/util.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index a8cb527d923..913242b6ea6 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -453,7 +453,7 @@ class ProcessCall { final String? workingDir; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return other is ProcessCall && executable == other.executable && listsEqual(args, other.args) && From bbab3499278a4cbc548a87d8ff9de3e208f23bb1 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 11 Jan 2023 14:11:35 -0800 Subject: [PATCH 236/249] [tool] Replace `flutter format` (#6946) `flutter format` is deprecated on `master`, and prints a warning saying to switch to `dart format` instead. This updates `format` to make that switch. --- script/tool/CHANGELOG.md | 5 +++++ script/tool/lib/src/format_command.dart | 6 ++---- script/tool/pubspec.yaml | 2 +- script/tool/test/format_command_test.dart | 17 +++++++++-------- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 072d661f8d9..ffb56bd7a7a 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.13.2+1 + +* Replaces deprecated `flutter format` with `dart format` in `format` + implementation. + ## 0.13.2 * Falls back to other executables in PATH when `clang-format` does not run. diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 43c450cbcf7..e4236878658 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -191,10 +191,8 @@ class FormatCommand extends PackageCommand { _getPathsWithExtensions(files, {'.dart'}); if (dartFiles.isNotEmpty) { print('Formatting .dart files...'); - // `flutter format` doesn't require the project to actually be a Flutter - // project. - final int exitCode = await _runBatched(flutterCommand, ['format'], - files: dartFiles); + final int exitCode = + await _runBatched('dart', ['format'], files: dartFiles); if (exitCode != 0) { printError('Failed to format Dart files: exit code $exitCode.'); throw ToolExit(_exitFlutterFormatFailed); diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index ea20364d51d..5438726d5bc 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.13.2 +version: 0.13.2+1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index 1aadafbd3d8..634a996bccc 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -98,7 +98,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - getFlutterCommand(mockPlatform), + 'dart', ['format', ...getPackagesDirRelativePaths(plugin, files)], packagesDir.path), ])); @@ -132,7 +132,7 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - getFlutterCommand(mockPlatform), + 'dart', [ 'format', ...getPackagesDirRelativePaths(plugin, formattedFiles) @@ -141,7 +141,7 @@ void main() { ])); }); - test('fails if flutter format fails', () async { + test('fails if dart format fails', () async { const List files = [ 'lib/a.dart', 'lib/src/b.dart', @@ -149,8 +149,9 @@ void main() { ]; createFakePlugin('a_plugin', packagesDir, extraFiles: files); - processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess(exitCode: 1)]; + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1) + ]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { @@ -465,7 +466,7 @@ void main() { ], packagesDir.path), ProcessCall( - getFlutterCommand(mockPlatform), + 'dart', [ 'format', ...getPackagesDirRelativePaths(plugin, dartFiles) @@ -594,7 +595,7 @@ void main() { processRunner.recordedCalls, contains( ProcessCall( - getFlutterCommand(mockPlatform), + 'dart', [ 'format', '$pluginName\\$extraFile', @@ -651,7 +652,7 @@ void main() { processRunner.recordedCalls, contains( ProcessCall( - getFlutterCommand(mockPlatform), + 'dart', [ 'format', '$pluginName/$extraFile', From 459e80606be1c39d45c105d01181222140cf2536 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 13 Jan 2023 03:12:28 -0800 Subject: [PATCH 237/249] [tool] Fix false positives in update-exceprts (#6950) When determining whether or not to fail with `--fail-on-change`, only look at .md files. In some cases, running the necessary commands (e.g., `flutter pub get`) may change unrelated files, causing fales positive failures. Only changed documentation files should be flagged. Also log the specific files that were detected as changed, to aid in debugging any future false positives. Fixes https://github.com/flutter/flutter/issues/111592 Fixes https://github.com/flutter/flutter/issues/111590 --- .../tool/lib/src/update_excerpts_command.dart | 18 +++++++++---- .../test/update_excerpts_command_test.dart | 25 +++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart index 5a59104d4e7..e65bed846cb 100644 --- a/script/tool/lib/src/update_excerpts_command.dart +++ b/script/tool/lib/src/update_excerpts_command.dart @@ -206,20 +206,28 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { .renameSync(package.pubspecFile.path); } - /// Checks the git state, returning an error string unless nothing has + /// Checks the git state, returning an error string if any .md files have /// changed. Future _validateRepositoryState() async { - final io.ProcessResult modifiedFiles = await processRunner.run( + final io.ProcessResult checkFiles = await processRunner.run( 'git', ['ls-files', '--modified'], workingDir: packagesDir, logOnError: true, ); - if (modifiedFiles.exitCode != 0) { + if (checkFiles.exitCode != 0) { return 'Unable to determine local file state'; } - final String stdout = modifiedFiles.stdout as String; - return stdout.trim().isEmpty ? null : 'Snippets are out of sync'; + final String stdout = checkFiles.stdout as String; + final List changedFiles = stdout.trim().split('\n'); + final Iterable changedMDFiles = + changedFiles.where((String filePath) => filePath.endsWith('.md')); + if (changedMDFiles.isNotEmpty) { + return 'Snippets are out of sync in the following files: ' + '${changedMDFiles.join(', ')}'; + } + + return null; } } diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart index 79f53d8779b..5a2f0f34041 100644 --- a/script/tool/test/update_excerpts_command_test.dart +++ b/script/tool/test/update_excerpts_command_test.dart @@ -232,11 +232,11 @@ void main() { ])); }); - test('fails if files are changed with --fail-on-change', () async { + test('fails if READMEs are changed with --fail-on-change', () async { createFakePlugin('a_plugin', packagesDir, extraFiles: [kReadmeExcerptConfigPath]); - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; + const String changedFilePath = 'packages/a_plugin/README.md'; processRunner.mockProcessesForExecutable['git'] = [ MockProcess(stdout: changedFilePath), ]; @@ -253,6 +253,27 @@ void main() { output, containsAllInOrder([ contains('README.md is out of sync with its source excerpts'), + contains('Snippets are out of sync in the following files: ' + 'packages/a_plugin/README.md'), + ])); + }); + + test('passes if unrelated files are changed with --fail-on-change', () async { + createFakePlugin('a_plugin', packagesDir, + extraFiles: [kReadmeExcerptConfigPath]); + + const String changedFilePath = 'packages/a_plugin/linux/CMakeLists.txt'; + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess(stdout: changedFilePath), + ]; + + final List output = await runCapturingPrint( + runner, ['update-excerpts', '--fail-on-change']); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), ])); }); From 5aa3f1c5da0a688d164e259e3ca84fd4ebeddb55 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 13 Jan 2023 12:49:26 -0800 Subject: [PATCH 238/249] [tool] Check for search paths in Swift plugins (#6954) * Rename command, bump version * Update tests to write actual podspecs * Add new check * Analyzer fix * Unhdo file move --- script/tool/CHANGELOG.md | 6 + script/tool/lib/src/main.dart | 4 +- ...ommand.dart => podspec_check_command.dart} | 81 +++- script/tool/pubspec.yaml | 2 +- .../tool/test/lint_podspecs_command_test.dart | 222 --------- .../tool/test/podspec_check_command_test.dart | 428 ++++++++++++++++++ 6 files changed, 510 insertions(+), 233 deletions(-) rename script/tool/lib/src/{lint_podspecs_command.dart => podspec_check_command.dart} (54%) delete mode 100644 script/tool/test/lint_podspecs_command_test.dart create mode 100644 script/tool/test/podspec_check_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index ffb56bd7a7a..55b5aeb7222 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.13.3 + +* Renames `podspecs` to `podspec-check`. The old name will continue to work. +* Adds validation of the Swift-in-Obj-C-projects workaround in the podspecs of + iOS plugin implementations that use Swift. + ## 0.13.2+1 * Replaces deprecated `flutter format` with `dart format` in `format` diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index d5ab7f88089..0083e0cbb8e 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -21,10 +21,10 @@ import 'fix_command.dart'; import 'format_command.dart'; import 'license_check_command.dart'; import 'lint_android_command.dart'; -import 'lint_podspecs_command.dart'; import 'list_command.dart'; import 'make_deps_path_based_command.dart'; import 'native_test_command.dart'; +import 'podspec_check_command.dart'; import 'publish_check_command.dart'; import 'publish_command.dart'; import 'pubspec_check_command.dart'; @@ -66,7 +66,7 @@ void main(List args) { ..addCommand(FormatCommand(packagesDir)) ..addCommand(LicenseCheckCommand(packagesDir)) ..addCommand(LintAndroidCommand(packagesDir)) - ..addCommand(LintPodspecsCommand(packagesDir)) + ..addCommand(PodspecCheckCommand(packagesDir)) ..addCommand(ListCommand(packagesDir)) ..addCommand(NativeTestCommand(packagesDir)) ..addCommand(MakeDepsPathBasedCommand(packagesDir)) diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/podspec_check_command.dart similarity index 54% rename from script/tool/lib/src/lint_podspecs_command.dart rename to script/tool/lib/src/podspec_check_command.dart index 198dd947211..4cda7210a8e 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/podspec_check_command.dart @@ -6,7 +6,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:file/file.dart'; -import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'common/core.dart'; @@ -20,23 +19,24 @@ const int _exitPodNotInstalled = 3; /// Lint the CocoaPod podspecs and run unit tests. /// /// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. -class LintPodspecsCommand extends PackageLoopingCommand { +class PodspecCheckCommand extends PackageLoopingCommand { /// Creates an instance of the linter command. - LintPodspecsCommand( + PodspecCheckCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), }) : super(packagesDir, processRunner: processRunner, platform: platform); @override - final String name = 'podspecs'; + final String name = 'podspec-check'; @override - List get aliases => ['podspec']; + List get aliases => ['podspec', 'podspecs']; @override final String description = - 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' + 'Runs "pod lib lint" on all iOS and macOS plugin podspecs, as well as ' + 'making sure the podspecs follow repository standards.\n\n' 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; @override @@ -69,9 +69,32 @@ class LintPodspecsCommand extends PackageLoopingCommand { for (final File podspec in podspecs) { if (!await _lintPodspec(podspec)) { - errors.add(p.basename(podspec.path)); + errors.add(podspec.basename); } } + + if (await _hasIOSSwiftCode(package)) { + print('iOS Swift code found, checking for search paths settings...'); + for (final File podspec in podspecs) { + if (_isPodspecMissingSearchPaths(podspec)) { + const String workaroundBlock = r''' + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } +'''; + final String path = + getRelativePosixPath(podspec, from: package.directory); + printError('$path is missing seach path configuration. Any iOS ' + 'plugin implementation that contains Swift implementation code ' + 'needs to contain the following:\n\n' + '$workaroundBlock\n' + 'For more details, see https://github.com/flutter/flutter/issues/118418.'); + errors.add(podspec.basename); + } + } + } + return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); @@ -92,7 +115,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { // Do not run the static analyzer on plugins with known analyzer issues. final String podspecPath = podspec.path; - final String podspecBasename = p.basename(podspecPath); + final String podspecBasename = podspec.basename; print('Linting $podspecBasename'); // Lint plugin as framework (use_frameworks!). @@ -126,4 +149,46 @@ class LintPodspecsCommand extends PackageLoopingCommand { return processRunner.run('pod', arguments, workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); } + + /// Returns true if there is any iOS plugin implementation code written in + /// Swift. + Future _hasIOSSwiftCode(RepositoryPackage package) async { + return getFilesForPackage(package).any((File entity) { + final String relativePath = + getRelativePosixPath(entity, from: package.directory); + // Ignore example code. + if (relativePath.startsWith('example/')) { + return false; + } + final String filePath = entity.path; + return path.extension(filePath) == '.swift'; + }); + } + + /// Returns true if [podspec] could apply to iOS, but does not have the + /// workaround for search paths that makes Swift plugins build correctly in + /// Objective-C applications. See + /// https://github.com/flutter/flutter/issues/118418 for context and details. + /// + /// This does not check that the plugin has Swift code, and thus whether the + /// workaround is needed, only whether or not it is there. + bool _isPodspecMissingSearchPaths(File podspec) { + final String directory = podspec.parent.basename; + // All macOS Flutter apps are Swift, so macOS-only podspecs don't need the + // workaround. If it's anywhere other than macos/, err or the side of + // assuming it's required. + if (directory == 'macos') { + return false; + } + + // This errs on the side of being too strict, to minimize the chance of + // accidental incorrect configuration. If we ever need more flexibility + // due to a false negative we can adjust this as necessary. + final RegExp workaround = RegExp(r''' +\s*s\.(?:ios\.)?xcconfig = {[^}]* +\s*'LIBRARY_SEARCH_PATHS' => '\$\(TOOLCHAIN_DIR\)/usr/lib/swift/\$\(PLATFORM_NAME\)/ \$\(SDKROOT\)/usr/lib/swift', +\s*'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',[^}]* +\s*}''', dotAll: true); + return !workaround.hasMatch(podspec.readAsStringSync()); + } } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 5438726d5bc..abf2a61f4cf 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.13.2+1 +version: 0.13.3 dependencies: args: ^2.1.0 diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart deleted file mode 100644 index 097bcff338a..00000000000 --- a/script/tool/test/lint_podspecs_command_test.dart +++ /dev/null @@ -1,222 +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: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/core.dart'; -import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$LintPodspecsCommand', () { - FileSystem fileSystem; - late Directory packagesDir; - late CommandRunner runner; - late MockPlatform mockPlatform; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - mockPlatform = MockPlatform(isMacOS: true); - processRunner = RecordingProcessRunner(); - final LintPodspecsCommand command = LintPodspecsCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = - CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); - runner.addCommand(command); - }); - - test('only runs on macOS', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - mockPlatform.isMacOS = false; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspecs'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - processRunner.recordedCalls, - equals([]), - ); - - expect( - output, - containsAllInOrder( - [contains('only supported on macOS')], - )); - }); - - test('runs pod lib lint on a podspec', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin1', - packagesDir, - extraFiles: [ - 'ios/plugin1.podspec', - 'bogus.dart', // Ignore non-podspecs. - ], - ); - - processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess(stdout: 'Foo', stderr: 'Bar'), - MockProcess(), - ]; - - final List output = - await runCapturingPrint(runner, ['podspecs']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', const ['pod'], packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin - .platformDirectory(FlutterPlatform.ios) - .childFile('plugin1.podspec') - .path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - '--use-libraries' - ], - packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin - .platformDirectory(FlutterPlatform.ios) - .childFile('plugin1.podspec') - .path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - ], - packagesDir.path), - ]), - ); - - expect(output, contains('Linting plugin1.podspec')); - expect(output, contains('Foo')); - expect(output, contains('Bar')); - }); - - test('fails if pod is missing', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - - // Simulate failure from `which pod`. - processRunner.mockProcessesForExecutable['which'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspecs'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains('Unable to find "pod". Make sure it is in your path.'), - ], - )); - }); - - test('fails if linting as a framework fails', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - - // Simulate failure from `pod`. - processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspecs'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains('The following packages had errors:'), - contains('plugin1:\n' - ' plugin1.podspec') - ], - )); - }); - - test('fails if linting as a static library fails', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - - // Simulate failure from the second call to `pod`. - processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess(), - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspecs'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains('The following packages had errors:'), - contains('plugin1:\n' - ' plugin1.podspec') - ], - )); - }); - - test('skips when there are no podspecs', () async { - createFakePlugin('plugin1', packagesDir); - - final List output = - await runCapturingPrint(runner, ['podspecs']); - - expect( - output, - containsAllInOrder( - [contains('SKIPPING: No podspecs.')], - )); - }); - }); -} diff --git a/script/tool/test/podspec_check_command_test.dart b/script/tool/test/podspec_check_command_test.dart new file mode 100644 index 00000000000..c31ffd46a4b --- /dev/null +++ b/script/tool/test/podspec_check_command_test.dart @@ -0,0 +1,428 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/podspec_check_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +/// Adds a fake podspec to [plugin]'s [platform] directory. +/// +/// If [includeSwiftWorkaround] is set, the xcconfig additions to make Swift +/// libraries work in apps that have no Swift will be included. If +/// [scopeSwiftWorkaround] is set, it will be specific to the iOS configuration. +void _writeFakePodspec(RepositoryPackage plugin, String platform, + {bool includeSwiftWorkaround = false, bool scopeSwiftWorkaround = false}) { + final String pluginName = plugin.directory.basename; + final File file = plugin.directory + .childDirectory(platform) + .childFile('$pluginName.podspec'); + final String swiftWorkaround = includeSwiftWorkaround + ? ''' + s.${scopeSwiftWorkaround ? 'ios.' : ''}xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '\$(TOOLCHAIN_DIR)/usr/lib/swift/\$(PLATFORM_NAME)/ \$(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } +''' + : ''; + file.createSync(recursive: true); + file.writeAsStringSync(''' +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'shared_preferences_foundation' + s.version = '0.0.1' + s.summary = 'iOS and macOS implementation of the shared_preferences plugin.' + s.description = <<-DESC +Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. + DESC + s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_foundation' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_foundation' } + s.source_files = 'Classes/**/*' + s.ios.dependency 'Flutter' + s.osx.dependency 'FlutterMacOS' + s.ios.deployment_target = '9.0' + s.osx.deployment_target = '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + $swiftWorkaround + s.swift_version = '5.0' + +end +'''); +} + +void main() { + group('PodspecCheckCommand', () { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late MockPlatform mockPlatform; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + mockPlatform = MockPlatform(isMacOS: true); + processRunner = RecordingProcessRunner(); + final PodspecCheckCommand command = PodspecCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = + CommandRunner('podspec_test', 'Test for $PodspecCheckCommand'); + runner.addCommand(command); + }); + + test('only runs on macOS', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + mockPlatform.isMacOS = false; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + processRunner.recordedCalls, + equals([]), + ); + + expect( + output, + containsAllInOrder( + [contains('only supported on macOS')], + )); + }); + + test('runs pod lib lint on a podspec', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin1', + packagesDir, + extraFiles: [ + 'bogus.dart', // Ignore non-podspecs. + ], + ); + _writeFakePodspec(plugin, 'ios'); + + processRunner.mockProcessesForExecutable['pod'] = [ + MockProcess(stdout: 'Foo', stderr: 'Bar'), + MockProcess(), + ]; + + final List output = + await runCapturingPrint(runner, ['podspec-check']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', const ['pod'], packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + plugin + .platformDirectory(FlutterPlatform.ios) + .childFile('plugin1.podspec') + .path, + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + '--use-libraries' + ], + packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + plugin + .platformDirectory(FlutterPlatform.ios) + .childFile('plugin1.podspec') + .path, + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + ], + packagesDir.path), + ]), + ); + + expect(output, contains('Linting plugin1.podspec')); + expect(output, contains('Foo')); + expect(output, contains('Bar')); + }); + + test('fails if pod is missing', () async { + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir); + _writeFakePodspec(plugin, 'ios'); + + // Simulate failure from `which pod`. + processRunner.mockProcessesForExecutable['which'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains('Unable to find "pod". Make sure it is in your path.'), + ], + )); + }); + + test('fails if linting as a framework fails', () async { + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir); + _writeFakePodspec(plugin, 'ios'); + + // Simulate failure from `pod`. + processRunner.mockProcessesForExecutable['pod'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains('The following packages had errors:'), + contains('plugin1:\n' + ' plugin1.podspec') + ], + )); + }); + + test('fails if linting as a static library fails', () async { + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir); + _writeFakePodspec(plugin, 'ios'); + + // Simulate failure from the second call to `pod`. + processRunner.mockProcessesForExecutable['pod'] = [ + MockProcess(), + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains('The following packages had errors:'), + contains('plugin1:\n' + ' plugin1.podspec') + ], + )); + }); + + test('fails if an iOS Swift plugin is missing the search paths workaround', + () async { + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, + extraFiles: ['ios/Classes/SomeSwift.swift']); + _writeFakePodspec(plugin, 'ios'); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains(r''' + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + }'''), + contains('The following packages had errors:'), + contains('plugin1:\n' + ' plugin1.podspec') + ], + )); + }); + + test( + 'fails if a shared-source Swift plugin is missing the search paths workaround', + () async { + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, + extraFiles: ['darwin/Classes/SomeSwift.swift']); + _writeFakePodspec(plugin, 'darwin'); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains(r''' + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + }'''), + contains('The following packages had errors:'), + contains('plugin1:\n' + ' plugin1.podspec') + ], + )); + }); + + test('does not require the search paths workaround for macOS plugins', + () async { + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, + extraFiles: ['macos/Classes/SomeSwift.swift']); + _writeFakePodspec(plugin, 'macos'); + + final List output = + await runCapturingPrint(runner, ['podspec-check']); + + expect( + output, + containsAllInOrder( + [ + contains('Ran for 1 package(s)'), + ], + )); + }); + + test('does not require the search paths workaround for ObjC iOS plugins', + () async { + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, + extraFiles: [ + 'ios/Classes/SomeObjC.h', + 'ios/Classes/SomeObjC.m' + ]); + _writeFakePodspec(plugin, 'ios'); + + final List output = + await runCapturingPrint(runner, ['podspec-check']); + + expect( + output, + containsAllInOrder( + [ + contains('Ran for 1 package(s)'), + ], + )); + }); + + test('passes if the search paths workaround is present', () async { + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, + extraFiles: ['ios/Classes/SomeSwift.swift']); + _writeFakePodspec(plugin, 'ios', includeSwiftWorkaround: true); + + final List output = + await runCapturingPrint(runner, ['podspec-check']); + + expect( + output, + containsAllInOrder( + [ + contains('Ran for 1 package(s)'), + ], + )); + }); + + test('passes if the search paths workaround is present for iOS only', + () async { + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, + extraFiles: ['ios/Classes/SomeSwift.swift']); + _writeFakePodspec(plugin, 'ios', + includeSwiftWorkaround: true, scopeSwiftWorkaround: true); + + final List output = + await runCapturingPrint(runner, ['podspec-check']); + + expect( + output, + containsAllInOrder( + [ + contains('Ran for 1 package(s)'), + ], + )); + }); + + test('does not require the search paths workaround for Swift example code', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'ios/Classes/SomeObjC.h', + 'ios/Classes/SomeObjC.m', + 'example/ios/Runner/AppDelegate.swift', + ]); + _writeFakePodspec(plugin, 'ios'); + + final List output = + await runCapturingPrint(runner, ['podspec-check']); + + expect( + output, + containsAllInOrder( + [ + contains('Ran for 1 package(s)'), + ], + )); + }); + + test('skips when there are no podspecs', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = + await runCapturingPrint(runner, ['podspec-check']); + + expect( + output, + containsAllInOrder( + [contains('SKIPPING: No podspecs.')], + )); + }); + }); +} From 7203521989be7f20b1186db479e414590b8b5976 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Jan 2023 10:28:41 -0800 Subject: [PATCH 239/249] [tool/ci] Add minimum supported SDK validation (#7028) Adds options to `pubspec.yaml` to check that the minimum supported SDK range for Flutter/Dart is at least a given version, to add CI enforcement that we're updating all of our support claims when we update our tested versions (rather than it being something we have to remember to do), and enables it in CI. As part of enabling it, fixes some violations: - path_provider_foundation had been temporarily dropped back to 2.10 as part of pushing out a regression fix. - a number of examples were missing Flutter constraints even though they used Flutter. - the non-Flutter `plugin_platform_interface` package hadn't been update since I hadn't thought about Dart-only constraints in the past. --- script/tool/CHANGELOG.md | 5 + .../tool/lib/src/pubspec_check_command.dart | 76 +++++++- script/tool/pubspec.yaml | 2 +- .../tool/test/pubspec_check_command_test.dart | 173 +++++++++++++++++- 4 files changed, 248 insertions(+), 8 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 55b5aeb7222..34def6ecf67 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.13.4 + +* Adds the ability to validate minimum supported Dart/Flutter versions in + `pubspec-check`. + ## 0.13.3 * Renames `podspecs` to `podspec-check`. The old name will continue to work. diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 5682ba05768..aefa316a41f 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -5,6 +5,7 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; @@ -29,7 +30,23 @@ class PubspecCheckCommand extends PackageLoopingCommand { processRunner: processRunner, platform: platform, gitDir: gitDir, - ); + ) { + argParser.addOption( + _minMinDartVersionFlag, + help: + 'The minimum Dart version to allow as the minimum SDK constraint.\n\n' + 'This is only enforced for non-Flutter packages; Flutter packages ' + 'use --$_minMinFlutterVersionFlag', + ); + argParser.addOption( + _minMinFlutterVersionFlag, + help: + 'The minimum Flutter version to allow as the minimum SDK constraint.', + ); + } + + static const String _minMinDartVersionFlag = 'min-min-dart-version'; + static const String _minMinFlutterVersionFlag = 'min-min-flutter-version'; // Section order for plugins. Because the 'flutter' section is critical // information for plugins, and usually small, it goes near the top unlike in @@ -100,6 +117,24 @@ class PubspecCheckCommand extends PackageLoopingCommand { printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); } + final String minMinDartVersionString = getStringArg(_minMinDartVersionFlag); + final String minMinFlutterVersionString = + getStringArg(_minMinFlutterVersionFlag); + final String? minVersionError = _checkForMinimumVersionError( + pubspec, + package, + minMinDartVersion: minMinDartVersionString.isEmpty + ? null + : Version.parse(minMinDartVersionString), + minMinFlutterVersion: minMinFlutterVersionString.isEmpty + ? null + : Version.parse(minMinFlutterVersionString), + ); + if (minVersionError != null) { + printError('$indentation$minVersionError'); + passing = false; + } + if (isPlugin) { final String? implementsError = _checkForImplementsError(pubspec, package: package); @@ -320,4 +355,43 @@ class PubspecCheckCommand extends PackageLoopingCommand { final String suffix = packageName.substring(parentName.length); return !nonImplementationSuffixes.contains(suffix); } + + /// Validates that a Flutter package has a minimum SDK version constraint of + /// at least [minMinFlutterVersion] (if provided), or that a non-Flutter + /// package has a minimum SDK version constraint of [minMinDartVersion] + /// (if provided). + /// + /// Returns an error string if validation fails. + String? _checkForMinimumVersionError( + Pubspec pubspec, + RepositoryPackage package, { + Version? minMinDartVersion, + Version? minMinFlutterVersion, + }) { + final VersionConstraint? dartConstraint = pubspec.environment?['sdk']; + final VersionConstraint? flutterConstraint = + pubspec.environment?['flutter']; + + if (flutterConstraint != null) { + // Validate Flutter packages against the Flutter requirement. + if (minMinFlutterVersion != null) { + final Version? constraintMin = + flutterConstraint is VersionRange ? flutterConstraint.min : null; + if ((constraintMin ?? Version(0, 0, 0)) < minMinFlutterVersion) { + return 'Minimum allowed Flutter version $constraintMin is less than $minMinFlutterVersion'; + } + } + } else { + // Validate non-Flutter packages against the Dart requirement. + if (minMinDartVersion != null) { + final Version? constraintMin = + dartConstraint is VersionRange ? dartConstraint.min : null; + if ((constraintMin ?? Version(0, 0, 0)) < minMinDartVersion) { + return 'Minimum allowed Dart version $constraintMin is less than $minMinDartVersion'; + } + } + } + + return null; + } } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index abf2a61f4cf..a8df2a9cd23 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.13.3 +version: 0.13.4 dependencies: args: ^2.1.0 diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 2c254ca9498..7a9c0cec7cb 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -60,12 +60,16 @@ ${publishable ? '' : "publish_to: 'none'"} '''; } -String _environmentSection() { - return ''' -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" -'''; +String _environmentSection({ + String dartConstraint = '>=2.12.0 <3.0.0', + String? flutterConstraint = '>=2.0.0', +}) { + return [ + 'environment:', + ' sdk: "$dartConstraint"', + if (flutterConstraint != null) ' flutter: "$flutterConstraint"', + '', + ].join('\n'); } String _flutterSection({ @@ -931,6 +935,163 @@ ${_devDependenciesSection()} ]), ); }); + + test('fails when a Flutter package has a too-low minimum Flutter version', + () async { + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, + isFlutter: true, examples: []); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection(flutterConstraint: '>=2.10.0')} +${_dependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + '--min-min-flutter-version', + '3.0.0' + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Minimum allowed Flutter version 2.10.0 is less than 3.0.0'), + ]), + ); + }); + + test( + 'passes when a Flutter package requires exactly the minimum Flutter version', + () async { + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, + isFlutter: true, examples: []); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection(flutterConstraint: '>=3.0.0')} +${_dependenciesSection()} +'''); + + final List output = await runCapturingPrint(runner, + ['pubspec-check', '--min-min-flutter-version', '3.0.0']); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + + test( + 'passes when a Flutter package requires a higher minimum Flutter version', + () async { + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, + isFlutter: true, examples: []); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection(flutterConstraint: '>=3.3.0')} +${_dependenciesSection()} +'''); + + final List output = await runCapturingPrint(runner, + ['pubspec-check', '--min-min-flutter-version', '3.0.0']); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + + test('fails when a non-Flutter package has a too-low minimum Dart version', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, examples: []); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection(dartConstraint: '>=2.14.0 <3.0.0', flutterConstraint: null)} +${_dependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check', '--min-min-dart-version', '2.17.0'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Minimum allowed Dart version 2.14.0 is less than 2.17.0'), + ]), + ); + }); + + test( + 'passes when a non-Flutter package requires exactly the minimum Dart version', + () async { + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, + isFlutter: true, examples: []); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection(dartConstraint: '>=2.17.0 <3.0.0', flutterConstraint: null)} +${_dependenciesSection()} +'''); + + final List output = await runCapturingPrint(runner, + ['pubspec-check', '--min-min-dart-version', '2.17.0']); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + + test( + 'passes when a non-Flutter package requires a higher minimum Dart version', + () async { + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, + isFlutter: true, examples: []); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection(dartConstraint: '>=2.18.0 <3.0.0', flutterConstraint: null)} +${_dependenciesSection()} +'''); + + final List output = await runCapturingPrint(runner, + ['pubspec-check', '--min-min-dart-version', '2.17.0']); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); }); group('test pubspec_check_command on Windows', () { From 07c367cfd257eece83d99cf9ad29524bc32842a1 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 30 Jan 2023 13:32:05 -0800 Subject: [PATCH 240/249] [tool] Improve main-branch detection (#7038) * [tool] Improve main-branch detection Currently main-branch detection for `--packages-for-branch` looks at branch names, but this no longer works on LUCI which now seems to be checking out specific hashes rather than branches. This updates the behavior so that it will treat any hash that is an ancestor of `main` as being part of `main`, which should allow post-submit detection to work under LUCI. Fixes https://github.com/flutter/flutter/issues/119330 * Fix throw * Fix typos * Update comment --- script/tool/CHANGELOG.md | 5 ++ .../tool/lib/src/common/package_command.dart | 34 +++++++++-- script/tool/pubspec.yaml | 2 +- .../test/common/package_command_test.dart | 56 ++++++++++++++++--- 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 34def6ecf67..3c4905ad707 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.13.4+1 + +* Makes `--packages-for-branch` detect any commit on `main` as being `main`, + so that it works with pinned checkouts (e.g., on LUCI). + ## 0.13.4 * Adds the ability to validate minimum supported Dart/Flutter versions in diff --git a/script/tool/lib/src/common/package_command.dart b/script/tool/lib/src/common/package_command.dart index 0e83d03e984..0e084c4c2d5 100644 --- a/script/tool/lib/src/common/package_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -316,17 +316,28 @@ abstract class PackageCommand extends Command { } else if (getBoolArg(_packagesForBranchArg)) { final String? branch = await _getBranch(); if (branch == null) { - printError('Unabled to determine branch; --$_packagesForBranchArg can ' + printError('Unable to determine branch; --$_packagesForBranchArg can ' 'only be used in a git repository.'); throw ToolExit(exitInvalidArguments); } else { // Configure the change finder the correct mode for the branch. - final bool lastCommitOnly = branch == 'main' || branch == 'master'; + // Log the mode to make it easier to audit logs to see that the + // intended diff was used (or why). + final bool lastCommitOnly; + if (branch == 'main' || branch == 'master') { + print('--$_packagesForBranchArg: running on default branch.'); + lastCommitOnly = true; + } else if (await _isCheckoutFromBranch('main')) { + print( + '--$_packagesForBranchArg: running on a commit from default branch.'); + lastCommitOnly = true; + } else { + print('--$_packagesForBranchArg: running on branch "$branch".'); + lastCommitOnly = false; + } if (lastCommitOnly) { - // Log the mode to make it easier to audit logs to see that the - // intended diff was used. - print('--$_packagesForBranchArg: running on default branch; ' - 'using parent commit as the diff base.'); + print( + '--$_packagesForBranchArg: using parent commit as the diff base.'); changedFileFinder = GitVersionFinder(await gitDir, 'HEAD~'); } else { changedFileFinder = await retrieveVersionFinder(); @@ -522,6 +533,17 @@ abstract class PackageCommand extends Command { return packages; } + // Returns true if the current checkout is on an ancestor of [branch]. + // + // This is used because CI may check out a specific hash rather than a branch, + // in which case branch-name detection won't work. + Future _isCheckoutFromBranch(String branchName) async { + final io.ProcessResult result = await (await gitDir).runCommand( + ['merge-base', '--is-ancestor', 'HEAD', branchName], + throwOnError: false); + return result.exitCode == 0; + } + Future _getBranch() async { final io.ProcessResult branchResult = await (await gitDir).runCommand( ['rev-parse', '--abbrev-ref', 'HEAD'], diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index a8df2a9cd23..73429638a40 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.13.4 +version: 0.13.4+1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_command_test.dart b/script/tool/test/common/package_command_test.dart index aa0a2025395..ed76408bb30 100644 --- a/script/tool/test/common/package_command_test.dart +++ b/script/tool/test/common/package_command_test.dart @@ -778,7 +778,8 @@ packages/b_package/lib/src/foo.dart MockProcess(stdout: 'a-branch'), ]; processRunner.mockProcessesForExecutable['git-merge-base'] = [ - MockProcess(stdout: 'abc123'), + MockProcess(exitCode: 1), // --is-ancestor check + MockProcess(stdout: 'abc123'), // finding merge base ]; final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir); @@ -791,6 +792,7 @@ packages/b_package/lib/src/foo.dart expect( output, containsAllInOrder([ + contains('--packages-for-branch: running on branch "a-branch"'), contains( 'Running for all packages that have diffs relative to "abc123"'), ])); @@ -822,8 +824,9 @@ packages/b_package/lib/src/foo.dart expect( output, containsAllInOrder([ - contains('--packages-for-branch: running on default branch; ' - 'using parent commit as the diff base'), + contains('--packages-for-branch: running on default branch.'), + contains( + '--packages-for-branch: using parent commit as the diff base'), contains( 'Running for all packages that have diffs relative to "HEAD~"'), ])); @@ -836,7 +839,45 @@ packages/b_package/lib/src/foo.dart )); }); - test('tests all packages on master', () async { + test( + 'only tests changed packages relative to the previous commit if ' + 'running on a specific hash from main', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'HEAD'), + ]; + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, unorderedEquals([plugin1.path])); + expect( + output, + containsAllInOrder([ + contains( + '--packages-for-branch: running on a commit from default branch.'), + contains( + '--packages-for-branch: using parent commit as the diff base'), + contains( + 'Running for all packages that have diffs relative to "HEAD~"'), + ])); + // Ensure that it's diffing against the prior commit. + expect( + processRunner.recordedCalls, + contains( + const ProcessCall( + 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), + )); + }); + + test( + 'only tests changed packages relative to the previous commit on master', + () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'packages/plugin1/plugin1.dart'), ]; @@ -854,8 +895,9 @@ packages/b_package/lib/src/foo.dart expect( output, containsAllInOrder([ - contains('--packages-for-branch: running on default branch; ' - 'using parent commit as the diff base'), + contains('--packages-for-branch: running on default branch.'), + contains( + '--packages-for-branch: using parent commit as the diff base'), contains( 'Running for all packages that have diffs relative to "HEAD~"'), ])); @@ -887,7 +929,7 @@ packages/b_package/lib/src/foo.dart expect( output, containsAllInOrder([ - contains('Unabled to determine branch'), + contains('Unable to determine branch'), ])); }); }); From c1bd41bc1a3039f1aeefcb0cdd5ad9255b0af145 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 31 Jan 2023 09:37:45 -0800 Subject: [PATCH 241/249] [various] Standardize the extension for Pigeon-generated Dart (#7029) * Standardize on .g.dart * Remove unused exclusion patterns * Mark pigeons/ as dev-only in the tooling * Version bumps * Add missed files * More new import fixes --- script/tool/lib/src/common/package_state_utils.dart | 3 +++ script/tool/test/common/package_state_utils_test.dart | 2 ++ 2 files changed, 5 insertions(+) diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index 464dac6c18d..fbba75c6116 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -171,6 +171,9 @@ Future _isDevChange(List pathComponents, // The top-level "tool" directory is for non-client-facing utility // code, such as test scripts. pathComponents.first == 'tool' || + // The top-level "pigeons" directory is the repo convention for storing + // pigeon input files. + pathComponents.first == 'pigeons' || // Entry point for the 'custom-test' command, which is only for CI and // local testing. pathComponents.first == 'run_tests.sh' || diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart index 86029cdf73a..9b6429a084c 100644 --- a/script/tool/test/common/package_state_utils_test.dart +++ b/script/tool/test/common/package_state_utils_test.dart @@ -68,6 +68,8 @@ void main() { 'packages/a_plugin/example/android/src/androidTest/foo/bar/FooTest.java', 'packages/a_plugin/example/ios/RunnerTests/Foo.m', 'packages/a_plugin/example/ios/RunnerUITests/info.plist', + // Pigeon input. + 'packages/a_plugin/pigeons/messages.dart', // Test scripts. 'packages/a_plugin/run_tests.sh', // Tools. From dc54dd71160f36514898e18cac8c2faaace7a8d8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 31 Jan 2023 16:52:49 -0800 Subject: [PATCH 242/249] [tool] More main-branch detection improvement (#7067) Follow-up to https://github.com/flutter/plugins/pull/7038 to try to make it work on LUCI. Looking at the checkout steps of a LUCI run, it looks like we do a full `fetch` of `origin`, but likely only have a `master` branch locally. Rather than rely on a specific local branch name existing, this allows for checking `origin` (and just in case, since it's another common remote name, `upstream`). Hopefully fixes https://github.com/flutter/flutter/issues/119330 --- .../tool/lib/src/common/package_command.dart | 26 ++++++++++-- script/tool/pubspec.yaml | 2 +- .../test/common/package_command_test.dart | 40 +++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/script/tool/lib/src/common/package_command.dart b/script/tool/lib/src/common/package_command.dart index 0e084c4c2d5..8a2bbfc4005 100644 --- a/script/tool/lib/src/common/package_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -538,10 +538,28 @@ abstract class PackageCommand extends Command { // This is used because CI may check out a specific hash rather than a branch, // in which case branch-name detection won't work. Future _isCheckoutFromBranch(String branchName) async { - final io.ProcessResult result = await (await gitDir).runCommand( - ['merge-base', '--is-ancestor', 'HEAD', branchName], - throwOnError: false); - return result.exitCode == 0; + // The target branch may not exist locally; try some common remote names for + // the branch as well. + final List candidateBranchNames = [ + branchName, + 'origin/$branchName', + 'upstream/$branchName', + ]; + for (final String branch in candidateBranchNames) { + final io.ProcessResult result = await (await gitDir).runCommand( + ['merge-base', '--is-ancestor', 'HEAD', branch], + throwOnError: false); + if (result.exitCode == 0) { + return true; + } else if (result.exitCode == 1) { + // 1 indicates that the branch was successfully checked, but it's not + // an ancestor. + return false; + } + // Any other return code is an error, such as `branch` not being a valid + // name in the repository, so try other name variants. + } + return false; } Future _getBranch() async { diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 73429638a40..52d23b8f72a 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.13.4+1 +version: 0.13.4+2 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_command_test.dart b/script/tool/test/common/package_command_test.dart index ed76408bb30..3620f8fd63a 100644 --- a/script/tool/test/common/package_command_test.dart +++ b/script/tool/test/common/package_command_test.dart @@ -875,6 +875,46 @@ packages/b_package/lib/src/foo.dart )); }); + test( + 'only tests changed packages relative to the previous commit if ' + 'running on a specific hash from origin/main', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'HEAD'), + ]; + processRunner.mockProcessesForExecutable['git-merge-base'] = [ + MockProcess(exitCode: 128), // Fail with a non-1 exit code for 'main' + MockProcess(), // Succeed for the variant. + ]; + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, unorderedEquals([plugin1.path])); + expect( + output, + containsAllInOrder([ + contains( + '--packages-for-branch: running on a commit from default branch.'), + contains( + '--packages-for-branch: using parent commit as the diff base'), + contains( + 'Running for all packages that have diffs relative to "HEAD~"'), + ])); + // Ensure that it's diffing against the prior commit. + expect( + processRunner.recordedCalls, + contains( + const ProcessCall( + 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), + )); + }); + test( 'only tests changed packages relative to the previous commit on master', () async { From 0658dd94fddccf0bf3b5872c28535f0a756e9b5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:11:08 +0000 Subject: [PATCH 243/249] [local_auth]: Bump core from 1.8.0 to 1.9.0 in /packages/local_auth/local_auth_android/android (#6393) * [local_auth]: Bump core Bumps core from 1.8.0 to 1.9.0. --- updated-dependencies: - dependency-name: androidx.core:core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Bump compilesdkversion * Bump fragment and update changelog * Bump gradle version * Bump compileSdkVersion * Bump all plugins compilesdkversion * Bump gradle version of example apps * Update changelog --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: camsim99 --- script/tool/lib/src/create_all_packages_app_command.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/tool/lib/src/create_all_packages_app_command.dart b/script/tool/lib/src/create_all_packages_app_command.dart index e7719e9f664..b56086ac1d0 100644 --- a/script/tool/lib/src/create_all_packages_app_command.dart +++ b/script/tool/lib/src/create_all_packages_app_command.dart @@ -130,8 +130,8 @@ class CreateAllPackagesAppCommand extends PackageCommand { // minSdkVersion 19 is required by WebView. newGradle.writeln('minSdkVersion 20'); } else if (line.contains('compileSdkVersion')) { - // compileSdkVersion 32 is required by webview_flutter. - newGradle.writeln('compileSdkVersion 32'); + // compileSdkVersion 33 is required by local_auth. + newGradle.writeln('compileSdkVersion 33'); } else { newGradle.writeln(line); } From d2d520f5e01fb8d25e3f8d3fab09f9ea89d225c9 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Tue, 7 Feb 2023 13:41:14 -0500 Subject: [PATCH 244/249] Update release tooling to give a workaround for predictable failing case https://github.com/flutter/flutter/issues/120116 (#7111) --- script/tool/lib/src/update_release_info_command.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart index 8d7ceb84d31..240ae72eed7 100644 --- a/script/tool/lib/src/update_release_info_command.dart +++ b/script/tool/lib/src/update_release_info_command.dart @@ -107,6 +107,10 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand { break; case _versionMinimal: final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + // If the line below fails with "Not a valid object name FETCH_HEAD" + // run "git fetch", FETCH_HEAD is a temporary reference that only exists + // after a fetch. This can happen when a branch is made locally and + // pushed but never fetched. _changedFiles = await gitVersionFinder.getChangedFiles(); // Anothing other than a fixed change is null. _versionChange = null; From 91d967c60de8806f4f70a6f3a8e3d4615122927a Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 13 Feb 2023 11:54:41 -0500 Subject: [PATCH 245/249] Update docs --- script/tool/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/script/tool/README.md b/script/tool/README.md index 9f0ac84145f..6a167ce2504 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -9,11 +9,9 @@ Note: The commands in tools are designed to run at the root of the repository or ## Getting Started -In flutter/plugins, the tool is run from source. In flutter/packages, the +In flutter/packages, the tool is run from source. In flutter/plugins, the [published version](https://pub.dev/packages/flutter_plugin_tools) is used -instead. (It is marked as Discontinued since it is no longer maintained as -a general-purpose tool, but updates are still published for use in -flutter/packages.) +instead. The commands in tools require the Flutter-bundled version of Dart to be the first `dart` loaded in the path. @@ -23,7 +21,7 @@ When updating sample code excerpts (`update-excerpts`) for the README.md files, there is some [extra setup for submodules](#update-readmemd-from-example-sources) that is necessary. -### From Source (flutter/plugins only) +### From Source (flutter/packages only) Set up: @@ -163,7 +161,7 @@ the automated process fails. Please, read on the Flutter Wiki first. ```sh -cd +cd git checkout dart run ./script/tool/bin/flutter_plugin_tools.dart publish --packages ``` @@ -182,7 +180,7 @@ control present. ## Updating the Tool -For flutter/plugins, just changing the source here is all that's needed. +For flutter/packages, just changing the source here is all that's needed. For changes that are relevant to flutter/packages, you will also need to: - Update the tool's pubspec.yaml and CHANGELOG From 6db4a95ce850ebc57b878f82df885ac8871da0bb Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 13 Feb 2023 12:01:14 -0500 Subject: [PATCH 246/249] Update invocations to run from local source --- .ci/scripts/build_examples_win32.sh | 2 +- .ci/scripts/create_all_packages_app.sh | 2 +- .ci/scripts/custom_package_tests.sh | 2 +- .ci/scripts/dart_unit_tests_win32.sh | 3 ++- .ci/scripts/drive_examples_win32.sh | 2 +- .ci/scripts/native_test_win32.sh | 2 +- .ci/scripts/prepare_tool.sh | 5 ++--- .cirrus.yml | 2 +- .github/workflows/release.yml | 10 ++++++---- script/tool_runner.sh | 21 ++++++++++++++------- 10 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.ci/scripts/build_examples_win32.sh b/.ci/scripts/build_examples_win32.sh index ff30ca93eec..bcf57a4b311 100755 --- a/.ci/scripts/build_examples_win32.sh +++ b/.ci/scripts/build_examples_win32.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart pub global run flutter_plugin_tools build-examples --windows \ +dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --windows \ --packages-for-branch --log-timing diff --git a/.ci/scripts/create_all_packages_app.sh b/.ci/scripts/create_all_packages_app.sh index 8c45a351bef..8399e5e38a3 100644 --- a/.ci/scripts/create_all_packages_app.sh +++ b/.ci/scripts/create_all_packages_app.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart pub global run flutter_plugin_tools create-all-packages-app \ +dart ./script/tool/bin/flutter_plugin_tools.dart create-all-packages-app \ --output-dir=. --exclude script/configs/exclude_all_packages_app.yaml diff --git a/.ci/scripts/custom_package_tests.sh b/.ci/scripts/custom_package_tests.sh index 92d953bb5fd..6b37bfbf1a9 100755 --- a/.ci/scripts/custom_package_tests.sh +++ b/.ci/scripts/custom_package_tests.sh @@ -8,6 +8,6 @@ # script/configs/linux_only_custom_test.yaml # Custom tests need Chrome. (They run in linux-custom_package_tests) -dart pub global run flutter_plugin_tools custom-test \ +dart ./script/tool/bin/flutter_plugin_tools.dart custom-test \ --packages-for-branch --log-timing \ --exclude=script/configs/linux_only_custom_test.yaml diff --git a/.ci/scripts/dart_unit_tests_win32.sh b/.ci/scripts/dart_unit_tests_win32.sh index ece00a74037..bd1ba77b591 100644 --- a/.ci/scripts/dart_unit_tests_win32.sh +++ b/.ci/scripts/dart_unit_tests_win32.sh @@ -3,5 +3,6 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart pub global run flutter_plugin_tools test --exclude=script/configs/windows_unit_tests_exceptions.yaml \ +dart ./script/tool/bin/flutter_plugin_tools.dart test \ + --exclude=script/configs/windows_unit_tests_exceptions.yaml \ --packages-for-branch --log-timing diff --git a/.ci/scripts/drive_examples_win32.sh b/.ci/scripts/drive_examples_win32.sh index d06c192ab55..c3e2e7bc544 100644 --- a/.ci/scripts/drive_examples_win32.sh +++ b/.ci/scripts/drive_examples_win32.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart pub global run flutter_plugin_tools drive-examples --windows \ +dart ./script/tool/bin/flutter_plugin_tools.dart drive-examples --windows \ --exclude=script/configs/exclude_integration_win32.yaml --packages-for-branch --log-timing diff --git a/.ci/scripts/native_test_win32.sh b/.ci/scripts/native_test_win32.sh index 7bfe8402248..37cf54e55c5 100755 --- a/.ci/scripts/native_test_win32.sh +++ b/.ci/scripts/native_test_win32.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart pub global run flutter_plugin_tools native-test --windows \ +dart ./script/tool/bin/flutter_plugin_tools.dart native-test --windows \ --no-integration --packages-for-branch --log-timing diff --git a/.ci/scripts/prepare_tool.sh b/.ci/scripts/prepare_tool.sh index dd1785d16f8..f93694bf1ff 100755 --- a/.ci/scripts/prepare_tool.sh +++ b/.ci/scripts/prepare_tool.sh @@ -6,6 +6,5 @@ # To set FETCH_HEAD for "git merge-base" to work git fetch origin main -# Pinned version of the plugin tools, to avoid breakage in this repository -# when pushing updates from flutter/plugins. -dart pub global activate flutter_plugin_tools 0.13.4+2 +cd script/tool +dart pub get diff --git a/.cirrus.yml b/.cirrus.yml index 5af55f23721..f76c94d72f9 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -4,7 +4,7 @@ gcp_credentials: ENCRYPTED[!3a93d98d7c95a41f5033834ef30e50928fc5d81239dc632b153c only_if: $CIRRUS_TAG == '' && ($CIRRUS_PR != '' || $CIRRUS_BRANCH == 'main') env: CHANNEL: "master" # Default to master when not explicitly set by a task. - PLUGIN_TOOL_COMMAND: "dart pub global run flutter_plugin_tools" + PLUGIN_TOOL_COMMAND: "dart ./script/tool/bin/flutter_plugin_tools.dart" install_chrome_linux_template: &INSTALL_CHROME_LINUX env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f4a109a49d..bbb153386ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,9 +31,10 @@ jobs: with: fetch-depth: 0 # Fetch all history so the tool can get all the tags to determine version. - name: Set up tools - run: dart pub global activate flutter_plugin_tools 0.13.4+2 + run: dart pub get + working-directory: ${{ github.workspace }}/script/tool - # # This workflow should be the last to run. So wait for all the other tests to succeed. + # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests uses: lewagon/wait-on-check-action@3a563271c3f8d1611ed7352809303617ee7e54ac with: @@ -42,11 +43,12 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 180 # seconds allowed-conclusions: success,neutral + # verbose:true will produce too many logs that hang github actions web UI. + verbose: false - name: run release run: | git config --global user.name ${{ secrets.USER_NAME }} git config --global user.email ${{ secrets.USER_EMAIL }} - dart pub global run flutter_plugin_tools publish --all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin + dart ./script/tool/lib/src/main.dart publish --all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"} - diff --git a/script/tool_runner.sh b/script/tool_runner.sh index 9b87f98e30d..221071550cc 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -2,15 +2,22 @@ # 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. + set -e -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -REPO_DIR="$(dirname "$SCRIPT_DIR")" +# WARNING! Do not remove this script, or change its behavior, unless you have +# verified that it will not break the flutter/flutter analysis run of this +# repository: https://github.com/flutter/flutter/blob/master/dev/bots/test.dart + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" +readonly TOOL_PATH="$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" + +# Ensure that the tool dependencies have been fetched. +(pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null # The tool expects to be run from the repo root. -# PACKAGE_SHARDING is (optionally) set from Cirrus. See .cirrus.yml cd "$REPO_DIR" -dart pub global run flutter_plugin_tools "$@" \ - --packages-for-branch \ - --log-timing \ - $PACKAGE_SHARDING +# Run from the in-tree source. +# PACKAGE_SHARDING is (optionally) set from Cirrus. See .cirrus.yml +dart run "$TOOL_PATH" "$@" --packages-for-branch --log-timing $PACKAGE_SHARDING From 265c846f31624655b8eed119fa3e7d82ef9eeddb Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 13 Feb 2023 12:04:19 -0500 Subject: [PATCH 247/249] Enable tests of tooling --- .ci/scripts/plugin_tools_tests.sh | 7 +++++++ .ci/targets/plugin_tools_tests.yaml | 5 +++++ .cirrus.yml | 7 +++++++ 3 files changed, 19 insertions(+) create mode 100644 .ci/scripts/plugin_tools_tests.sh create mode 100644 .ci/targets/plugin_tools_tests.yaml diff --git a/.ci/scripts/plugin_tools_tests.sh b/.ci/scripts/plugin_tools_tests.sh new file mode 100644 index 00000000000..96eec4349f0 --- /dev/null +++ b/.ci/scripts/plugin_tools_tests.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# 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. + +cd script/tool +dart pub run test diff --git a/.ci/targets/plugin_tools_tests.yaml b/.ci/targets/plugin_tools_tests.yaml new file mode 100644 index 00000000000..265e74bdd06 --- /dev/null +++ b/.ci/targets/plugin_tools_tests.yaml @@ -0,0 +1,5 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: tool unit tests + script: .ci/scripts/plugin_tools_tests.sh diff --git a/.cirrus.yml b/.cirrus.yml index f76c94d72f9..8142aae22ae 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -84,6 +84,10 @@ task: namespace: default matrix: ### Platform-agnostic tasks ### + - name: Linux plugin_tools_tests + script: + - cd script/tool + - dart pub run test # Repository rules and best-practice enforcement. # Only channel-agnostic tests should go here since it is only run once # (on Flutter master). @@ -127,6 +131,9 @@ task: matrix: CHANNEL: "master" CHANNEL: "stable" + analyze_tool_script: + - cd script/tool + - dart analyze --fatal-infos analyze_script: # DO NOT change the custom-analysis argument here without changing the Dart repo. # See the comment in script/configs/custom_analysis.yaml for details. From 314b35b77856b3d8f8dfa37e90978fc55dfcc4d1 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 13 Feb 2023 12:04:50 -0500 Subject: [PATCH 248/249] Metadata update --- script/tool/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 52d23b8f72a..b183a324322 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages -repository: https://github.com/flutter/plugins/tree/main/script/tool +repository: https://github.com/flutter/packages/tree/main/script/tool version: 0.13.4+2 dependencies: From e0ff330069770ad8cba1134f59081e2dfd0d3399 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 13 Feb 2023 12:14:04 -0500 Subject: [PATCH 249/249] Version bump --- script/tool/CHANGELOG.md | 9 +++++++++ script/tool/pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 3c4905ad707..31adba72697 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.13.4+3 + +* Moves source to flutter/packages. + +## 0.13.4+2 + +* Expands the `--packages-for-branch` detection of main to include ancestors + of `origin/main` and `upstream/main`. + ## 0.13.4+1 * Makes `--packages-for-branch` detect any commit on `main` as being `main`, diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index b183a324322..f9bd415b9e7 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/packages/tree/main/script/tool -version: 0.13.4+2 +version: 0.13.4+3 dependencies: args: ^2.1.0