[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
This commit is contained in:
stuartmorgan
2021-08-18 06:51:10 -07:00
committed by GitHub
parent 954804f68d
commit 721421a091
9 changed files with 495 additions and 40 deletions

View File

@ -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<int> runCommand(
String target, {
List<String> arguments = const <String>[],
}) {
return processRunner.runAndStream(
gradleWrapper.path,
<String>[target, ...arguments],
workingDir: androidDirectory,
);
}
}

View File

@ -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.

View File

@ -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<void> _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(<String>['Unable to build example apk']);
}
await _configureFirebaseProject();
if (!await _runGradle(androidDirectory, 'app:assembleAndroidTest')) {
if (!await _runGradle(project, 'app:assembleAndroidTest')) {
return PackageResult.fail(<String>['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<bool> _ensureGradleWrapperExists(Directory androidDirectory) async {
if (!androidDirectory.childFile(_gradleWrapper).existsSync()) {
Future<bool> _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<bool> _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,
<String>[
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: <String>[
'-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;

View File

@ -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<PackageResult> 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(<String>['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();
}
}

View File

@ -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<String> args) {
..addCommand(FirebaseTestLabCommand(packagesDir))
..addCommand(FormatCommand(packagesDir))
..addCommand(LicenseCheckCommand(packagesDir))
..addCommand(LintAndroidCommand(packagesDir))
..addCommand(LintPodspecsCommand(packagesDir))
..addCommand(ListCommand(packagesDir))
..addCommand(NativeTestCommand(packagesDir))

View File

@ -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<String> _iosDestinationFlags = <String>[];
@ -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, <String>['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,
<String>[
'app:connectedAndroidTest',
'-Pandroid.testInstrumentationRunnerArguments.$filter',
],
workingDir: androidDirectory);
final int exitCode = await project.runCommand(
'app:connectedAndroidTest',
arguments: <String>[
'-Pandroid.testInstrumentationRunnerArguments.$filter',
],
);
if (exitCode != 0) {
printError('$exampleName integration tests failed.');
failed = true;