mirror of
https://github.com/flutter/packages.git
synced 2025-07-01 07:08:10 +08:00
[ci] Add a legacy Android build-all test (#4005)
Adds the ability to replace portions of the `flutter create`d app with saved copies, and adds a second build-all phase for Android that uses a Flutter 2.0.6-created `android/` directory (AGP 4.1, Gradle 6.7) to catch issues like https://github.com/flutter/flutter/issues/125621 and https://github.com/flutter/flutter/issues/125482 prior to release. Includes some incidental cleanup: - Extracts a helper method for adjusting files, so that this doesn't add even more copies of basically identical code. - (This was motivated by an earlier version of the PR that added modifications to several more files, which I ended up undoing, but the cleanup seemed worth keeping.) - Adds missing unit test coverage. - Reworks the unit tests to use a mock process manager and dummy files, instead of each test actually calling `flutter create`, which made each new test add several seconds to the unit test suite. - While this reduces the integration-style coverage, in practice the integration tests of the repo tooling is the CI itself, so the unit tests should be true unit tests. - Changes the non-legacy test to Kotlin; we were still testing with Java even though Kotlin has been the default for quite a while, so we weren't testing what most new users would actually be running. Since we now have a legacy test, I used Java there to cover both. - Removes some dead code for modifying the AndroidManifest.xml; when trying to set up unit tests for it I discovered that it no longer matches anything in an actual project. It dates back to the original command, and seems to have been a camera-related hack, which we clearly no longer need since it wasn't working and camera still works in build-all. This is captured somewhat in the README in the legacy project directory, but to document the approach here: originally I was going to add flags to change individual items (AGP version, Gradle version), but quickly ran into the fact that selective downgrading is extremely fragile. E.g.,: - The Kotlin version set in current projects doesn't work when downgrading AGP and Gradle. - The app template can unconditionally use anything (e.g., `namespace`) that the AGP version it uses supports, so arbitrary future breakage is possible. It's also less useful as a real test of what a plugin client's project likely looks like. Starting with a complete platform directory, and doing whatever the minimal changes are to keep it working, will likely reflect a common real-world scenario. On the flip side, the reason this doesn't use a complete 2.0.6 project, but instead is based on specific platform directories, is that we don't want to waste time manually maintaining, e.g., old Dart code that is irrelevant to the goal of the test. For now this only uses Android because that's where we've seen problems in practice, but we can alway add other legacy platform tests later if we find a need. Fixes https://github.com/flutter/flutter/issues/125689
This commit is contained in:
@ -11,10 +11,21 @@ import 'package:file/file.dart';
|
||||
/// childFileWithSubcomponents(rootDir, ['foo', 'bar', 'baz.txt'])
|
||||
/// creates a File representing /rootDir/foo/bar/baz.txt.
|
||||
File childFileWithSubcomponents(Directory base, List<String> components) {
|
||||
Directory dir = base;
|
||||
final String basename = components.removeLast();
|
||||
return childDirectoryWithSubcomponents(base, components).childFile(basename);
|
||||
}
|
||||
|
||||
/// Returns a [Directory] created by appending everything in [components]
|
||||
/// to [base] as subdirectories.
|
||||
///
|
||||
/// Example:
|
||||
/// childFileWithSubcomponents(rootDir, ['foo', 'bar'])
|
||||
/// creates a File representing /rootDir/foo/bar/.
|
||||
Directory childDirectoryWithSubcomponents(
|
||||
Directory base, List<String> components) {
|
||||
Directory dir = base;
|
||||
for (final String directoryName in components) {
|
||||
dir = dir.childDirectory(directoryName);
|
||||
}
|
||||
return dir.childFile(basename);
|
||||
return dir;
|
||||
}
|
||||
|
@ -2,26 +2,27 @@
|
||||
// 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: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/file_utils.dart';
|
||||
import 'common/package_command.dart';
|
||||
import 'common/process_runner.dart';
|
||||
import 'common/repository_package.dart';
|
||||
|
||||
const String _outputDirectoryFlag = 'output-dir';
|
||||
/// The name of the build-all-packages project, as passed to `flutter create`.
|
||||
@visibleForTesting
|
||||
const String allPackagesProjectName = 'all_packages';
|
||||
|
||||
const String _projectName = 'all_packages';
|
||||
|
||||
const int _exitUpdateMacosPodfileFailed = 3;
|
||||
const int _exitUpdateMacosPbxprojFailed = 4;
|
||||
const int _exitGenNativeBuildFilesFailed = 5;
|
||||
const int _exitFlutterCreateFailed = 3;
|
||||
const int _exitGenNativeBuildFilesFailed = 4;
|
||||
const int _exitMissingFile = 5;
|
||||
const int _exitMissingLegacySource = 6;
|
||||
|
||||
/// A command to create an application that builds all in a single application.
|
||||
class CreateAllPackagesAppCommand extends PackageCommand {
|
||||
@ -29,22 +30,29 @@ class CreateAllPackagesAppCommand extends PackageCommand {
|
||||
CreateAllPackagesAppCommand(
|
||||
Directory packagesDir, {
|
||||
ProcessRunner processRunner = const ProcessRunner(),
|
||||
Directory? pluginsRoot,
|
||||
Platform platform = const LocalPlatform(),
|
||||
}) : super(packagesDir, processRunner: processRunner, platform: platform) {
|
||||
final Directory defaultDir =
|
||||
pluginsRoot ?? packagesDir.fileSystem.currentDirectory;
|
||||
argParser.addOption(_outputDirectoryFlag,
|
||||
defaultsTo: defaultDir.path,
|
||||
help:
|
||||
'The path the directory to create the "$_projectName" project in.\n'
|
||||
defaultsTo: packagesDir.parent.path,
|
||||
help: 'The path the directory to create the "$allPackagesProjectName" '
|
||||
'project in.\n'
|
||||
'Defaults to the repository root.');
|
||||
argParser.addOption(_legacySourceFlag,
|
||||
help: 'A partial project directory to use as a source for replacing '
|
||||
'portions of the created app. All top-level directories in the '
|
||||
'source will replace the corresponding directories in the output '
|
||||
'directory post-create.\n\n'
|
||||
'The replacement will be done before any tool-driven '
|
||||
'modifications.');
|
||||
}
|
||||
|
||||
static const String _legacySourceFlag = 'legacy-source';
|
||||
static const String _outputDirectoryFlag = 'output-dir';
|
||||
|
||||
/// The location to create the synthesized app project.
|
||||
Directory get _appDirectory => packagesDir.fileSystem
|
||||
.directory(getStringArg(_outputDirectoryFlag))
|
||||
.childDirectory(_projectName);
|
||||
.childDirectory(allPackagesProjectName);
|
||||
|
||||
/// The synthesized app project.
|
||||
RepositoryPackage get app => RepositoryPackage(_appDirectory);
|
||||
@ -60,7 +68,15 @@ class CreateAllPackagesAppCommand extends PackageCommand {
|
||||
Future<void> run() async {
|
||||
final int exitCode = await _createApp();
|
||||
if (exitCode != 0) {
|
||||
throw ToolExit(exitCode);
|
||||
printError('Failed to `flutter create`: $exitCode');
|
||||
throw ToolExit(_exitFlutterCreateFailed);
|
||||
}
|
||||
|
||||
final String? legacySource = getNullableStringArg(_legacySourceFlag);
|
||||
if (legacySource != null) {
|
||||
final Directory legacyDir =
|
||||
packagesDir.fileSystem.directory(legacySource);
|
||||
await _replaceWithLegacy(target: _appDirectory, source: legacyDir);
|
||||
}
|
||||
|
||||
final Set<String> excluded = getExcludedPackageNames();
|
||||
@ -89,7 +105,6 @@ class CreateAllPackagesAppCommand extends PackageCommand {
|
||||
|
||||
await Future.wait(<Future<void>>[
|
||||
_updateAppGradle(),
|
||||
_updateManifest(),
|
||||
_updateMacosPbxproj(),
|
||||
// This step requires the native file generation triggered by
|
||||
// flutter pub get above, so can't currently be run on Windows.
|
||||
@ -98,20 +113,101 @@ class CreateAllPackagesAppCommand extends PackageCommand {
|
||||
}
|
||||
|
||||
Future<int> _createApp() async {
|
||||
final io.ProcessResult result = io.Process.runSync(
|
||||
return processRunner.runAndStream(
|
||||
flutterCommand,
|
||||
<String>[
|
||||
'create',
|
||||
'--template=app',
|
||||
'--project-name=$_projectName',
|
||||
'--android-language=java',
|
||||
'--project-name=$allPackagesProjectName',
|
||||
_appDirectory.path,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
print(result.stdout);
|
||||
print(result.stderr);
|
||||
return result.exitCode;
|
||||
Future<void> _replaceWithLegacy(
|
||||
{required Directory target, required Directory source}) async {
|
||||
if (!source.existsSync()) {
|
||||
printError('No such legacy source directory: ${source.path}');
|
||||
throw ToolExit(_exitMissingLegacySource);
|
||||
}
|
||||
for (final FileSystemEntity entity in source.listSync()) {
|
||||
final String basename = entity.basename;
|
||||
print('Replacing $basename with legacy version...');
|
||||
if (entity is Directory) {
|
||||
target.childDirectory(basename).deleteSync(recursive: true);
|
||||
} else {
|
||||
target.childFile(basename).deleteSync();
|
||||
}
|
||||
_copyDirectory(source: source, target: target);
|
||||
}
|
||||
}
|
||||
|
||||
void _copyDirectory({required Directory target, required Directory source}) {
|
||||
target.createSync(recursive: true);
|
||||
for (final FileSystemEntity entity in source.listSync(recursive: true)) {
|
||||
final List<String> subcomponents =
|
||||
p.split(p.relative(entity.path, from: source.path));
|
||||
if (entity is Directory) {
|
||||
childDirectoryWithSubcomponents(target, subcomponents)
|
||||
.createSync(recursive: true);
|
||||
} else if (entity is File) {
|
||||
final File targetFile =
|
||||
childFileWithSubcomponents(target, subcomponents);
|
||||
targetFile.parent.createSync(recursive: true);
|
||||
entity.copySync(targetFile.path);
|
||||
} else {
|
||||
throw UnimplementedError('Unsupported entity: $entity');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrites [file], replacing any lines contain a key in [replacements] with
|
||||
/// the lines in the corresponding value, and adding any lines in [additions]'
|
||||
/// values after lines containing the key.
|
||||
void _adjustFile(
|
||||
File file, {
|
||||
Map<String, List<String>> replacements = const <String, List<String>>{},
|
||||
Map<String, List<String>> additions = const <String, List<String>>{},
|
||||
Map<RegExp, List<String>> regexReplacements =
|
||||
const <RegExp, List<String>>{},
|
||||
}) {
|
||||
if (replacements.isEmpty && additions.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (!file.existsSync()) {
|
||||
printError('Unable to find ${file.path} for updating.');
|
||||
throw ToolExit(_exitMissingFile);
|
||||
}
|
||||
|
||||
final StringBuffer output = StringBuffer();
|
||||
for (final String line in file.readAsLinesSync()) {
|
||||
List<String>? replacementLines;
|
||||
for (final MapEntry<String, List<String>> replacement
|
||||
in replacements.entries) {
|
||||
if (line.contains(replacement.key)) {
|
||||
replacementLines = replacement.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (replacementLines == null) {
|
||||
for (final MapEntry<RegExp, List<String>> replacement
|
||||
in regexReplacements.entries) {
|
||||
final RegExpMatch? match = replacement.key.firstMatch(line);
|
||||
if (match != null) {
|
||||
replacementLines = replacement.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
(replacementLines ?? <String>[line]).forEach(output.writeln);
|
||||
|
||||
for (final String targetString in additions.keys) {
|
||||
if (line.contains(targetString)) {
|
||||
additions[targetString]!.forEach(output.writeln);
|
||||
}
|
||||
}
|
||||
}
|
||||
file.writeAsStringSync(output.toString());
|
||||
}
|
||||
|
||||
Future<void> _updateAppGradle() async {
|
||||
@ -119,59 +215,50 @@ class CreateAllPackagesAppCommand extends PackageCommand {
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle');
|
||||
if (!gradleFile.existsSync()) {
|
||||
throw ToolExit(64);
|
||||
|
||||
// Ensure that there is a dependencies section, so the dependencies addition
|
||||
// below will work.
|
||||
final String content = gradleFile.readAsStringSync();
|
||||
if (!content.contains('\ndependencies {')) {
|
||||
gradleFile.writeAsStringSync('''
|
||||
$content
|
||||
dependencies {}
|
||||
''');
|
||||
}
|
||||
|
||||
final StringBuffer newGradle = StringBuffer();
|
||||
for (final String line in gradleFile.readAsLinesSync()) {
|
||||
if (line.contains('minSdkVersion')) {
|
||||
const String lifecycleDependency =
|
||||
" implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'";
|
||||
|
||||
_adjustFile(
|
||||
gradleFile,
|
||||
replacements: <String, List<String>>{
|
||||
// minSdkVersion 21 is required by camera_android.
|
||||
newGradle.writeln('minSdkVersion 21');
|
||||
} else if (line.contains('compileSdkVersion')) {
|
||||
'minSdkVersion': <String>['minSdkVersion 21'],
|
||||
// compileSdkVersion 33 is required by local_auth.
|
||||
newGradle.writeln('compileSdkVersion 33');
|
||||
} else {
|
||||
newGradle.writeln(line);
|
||||
}
|
||||
if (line.contains('defaultConfig {')) {
|
||||
newGradle.writeln(' multiDexEnabled true');
|
||||
} else if (line.contains('dependencies {')) {
|
||||
'compileSdkVersion': <String>['compileSdkVersion 33'],
|
||||
},
|
||||
additions: <String, List<String>>{
|
||||
'defaultConfig {': <String>[' multiDexEnabled true'],
|
||||
},
|
||||
regexReplacements: <RegExp, List<String>>{
|
||||
// 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<void> _updateManifest() async {
|
||||
final File manifestFile = app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childDirectory('src')
|
||||
.childDirectory('main')
|
||||
.childFile('AndroidManifest.xml');
|
||||
if (!manifestFile.existsSync()) {
|
||||
throw ToolExit(64);
|
||||
}
|
||||
|
||||
final StringBuffer newManifest = StringBuffer();
|
||||
for (final String line in manifestFile.readAsLinesSync()) {
|
||||
if (line.contains('package="com.example.$_projectName"')) {
|
||||
newManifest
|
||||
..writeln('package="com.example.$_projectName"')
|
||||
..writeln('xmlns:tools="http://schemas.android.com/tools">')
|
||||
..writeln()
|
||||
..writeln(
|
||||
'<uses-sdk tools:overrideLibrary="io.flutter.plugins.camera"/>',
|
||||
);
|
||||
} else {
|
||||
newManifest.writeln(line);
|
||||
}
|
||||
}
|
||||
manifestFile.writeAsStringSync(newManifest.toString());
|
||||
// Handling of 'dependencies' is more complex since it hasn't been very
|
||||
// stable across template versions.
|
||||
// - Handle an empty, collapsed dependencies section.
|
||||
RegExp(r'^dependencies\s+{\s*}$'): <String>[
|
||||
'dependencies {',
|
||||
lifecycleDependency,
|
||||
'}',
|
||||
],
|
||||
// - Handle a normal dependencies section.
|
||||
RegExp(r'^dependencies\s+{$'): <String>[
|
||||
'dependencies {',
|
||||
lifecycleDependency,
|
||||
],
|
||||
// - See below for handling of the case where there is no dependencies
|
||||
// section.
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _genPubspecWithAllPlugins() async {
|
||||
@ -190,7 +277,7 @@ class CreateAllPackagesAppCommand extends PackageCommand {
|
||||
final Map<String, PathDependency> pluginDeps =
|
||||
await _getValidPathDependencies();
|
||||
final Pubspec pubspec = Pubspec(
|
||||
_projectName,
|
||||
allPackagesProjectName,
|
||||
description: 'Flutter app containing all 1st party plugins.',
|
||||
version: Version.parse('1.0.0+1'),
|
||||
environment: <String, VersionConstraint>{
|
||||
@ -300,23 +387,15 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)}
|
||||
return;
|
||||
}
|
||||
|
||||
final File podfileFile =
|
||||
final File podfile =
|
||||
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')) {
|
||||
_adjustFile(
|
||||
podfile,
|
||||
replacements: <String, List<String>>{
|
||||
// macOS 10.15 is required by in_app_purchase.
|
||||
newPodfile.writeln("platform :osx, '10.15'");
|
||||
} else {
|
||||
newPodfile.writeln(line);
|
||||
}
|
||||
}
|
||||
podfileFile.writeAsStringSync(newPodfile.toString());
|
||||
'platform :osx': <String>["platform :osx, '10.15'"],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateMacosPbxproj() async {
|
||||
@ -324,20 +403,14 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)}
|
||||
.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')) {
|
||||
_adjustFile(
|
||||
pbxprojFile,
|
||||
replacements: <String, List<String>>{
|
||||
// macOS 10.15 is required by in_app_purchase.
|
||||
newPbxproj.writeln(' MACOSX_DEPLOYMENT_TARGET = 10.15;');
|
||||
} else {
|
||||
newPbxproj.writeln(line);
|
||||
}
|
||||
}
|
||||
pbxprojFile.writeAsStringSync(newPbxproj.toString());
|
||||
'MACOSX_DEPLOYMENT_TARGET': <String>[
|
||||
' MACOSX_DEPLOYMENT_TARGET = 10.15;'
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,25 +8,51 @@ 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();
|
||||
group('childFileWithSubcomponents', () {
|
||||
test('works on Posix', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem();
|
||||
|
||||
final Directory base = fileSystem.directory('/').childDirectory('base');
|
||||
final File file =
|
||||
childFileWithSubcomponents(base, <String>['foo', 'bar', 'baz.txt']);
|
||||
final Directory base = fileSystem.directory('/').childDirectory('base');
|
||||
final File file =
|
||||
childFileWithSubcomponents(base, <String>['foo', 'bar', 'baz.txt']);
|
||||
|
||||
expect(file.absolute.path, '/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, <String>['foo', 'bar', 'baz.txt']);
|
||||
|
||||
expect(file.absolute.path, r'C:\base\foo\bar\baz.txt');
|
||||
});
|
||||
});
|
||||
|
||||
test('works on Windows', () async {
|
||||
final FileSystem fileSystem =
|
||||
MemoryFileSystem(style: FileSystemStyle.windows);
|
||||
group('childDirectoryWithSubcomponents', () {
|
||||
test('works on Posix', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem();
|
||||
|
||||
final Directory base = fileSystem.directory(r'C:\').childDirectory('base');
|
||||
final File file =
|
||||
childFileWithSubcomponents(base, <String>['foo', 'bar', 'baz.txt']);
|
||||
final Directory base = fileSystem.directory('/').childDirectory('base');
|
||||
final Directory dir =
|
||||
childDirectoryWithSubcomponents(base, <String>['foo', 'bar', 'baz']);
|
||||
|
||||
expect(file.absolute.path, r'C:\base\foo\bar\baz.txt');
|
||||
expect(dir.absolute.path, '/base/foo/bar/baz');
|
||||
});
|
||||
|
||||
test('works on Windows', () async {
|
||||
final FileSystem fileSystem =
|
||||
MemoryFileSystem(style: FileSystemStyle.windows);
|
||||
|
||||
final Directory base =
|
||||
fileSystem.directory(r'C:\').childDirectory('base');
|
||||
final Directory dir =
|
||||
childDirectoryWithSubcomponents(base, <String>['foo', 'bar', 'baz']);
|
||||
|
||||
expect(dir.absolute.path, r'C:\base\foo\bar\baz');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ 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/create_all_packages_app_command.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
@ -18,16 +18,15 @@ import 'util.dart';
|
||||
void main() {
|
||||
late CommandRunner<void> runner;
|
||||
late CreateAllPackagesAppCommand command;
|
||||
late Platform mockPlatform;
|
||||
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();
|
||||
mockPlatform = MockPlatform(isMacOS: true);
|
||||
fileSystem = MemoryFileSystem();
|
||||
testRoot = fileSystem.systemTempDirectory.createTempSync();
|
||||
packagesDir = testRoot.childDirectory('packages');
|
||||
processRunner = RecordingProcessRunner();
|
||||
@ -35,34 +34,142 @@ void main() {
|
||||
command = CreateAllPackagesAppCommand(
|
||||
packagesDir,
|
||||
processRunner: processRunner,
|
||||
pluginsRoot: testRoot,
|
||||
platform: mockPlatform,
|
||||
);
|
||||
runner = CommandRunner<void>(
|
||||
'create_all_test', 'Test for $CreateAllPackagesAppCommand');
|
||||
runner.addCommand(command);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
testRoot.deleteSync(recursive: true);
|
||||
});
|
||||
/// Simulates enough of `flutter create`s output to allow the modifications
|
||||
/// made by the command to work.
|
||||
void writeFakeFlutterCreateOutput(
|
||||
Directory outputDirectory, {
|
||||
String dartSdkConstraint = '>=3.0.0 <4.0.0',
|
||||
String? appBuildGradleDependencies,
|
||||
bool androidOnly = false,
|
||||
}) {
|
||||
final RepositoryPackage package = RepositoryPackage(
|
||||
outputDirectory.childDirectory(allPackagesProjectName));
|
||||
|
||||
// Android
|
||||
final String dependencies = appBuildGradleDependencies ??
|
||||
r'''
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
''';
|
||||
package
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle')
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('''
|
||||
android {
|
||||
namespace 'dev.flutter.packages.foo.example'
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId "dev.flutter.packages.foo.example"
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion 32
|
||||
}
|
||||
}
|
||||
|
||||
$dependencies
|
||||
''');
|
||||
|
||||
if (androidOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-platform-specific
|
||||
package.pubspecFile
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('''
|
||||
name: $allPackagesProjectName
|
||||
description: Flutter app containing all 1st party plugins.
|
||||
publish_to: none
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: '$dartSdkConstraint'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
###
|
||||
''');
|
||||
|
||||
// macOS
|
||||
final Directory macOS = package.platformDirectory(FlutterPlatform.macos);
|
||||
macOS.childDirectory('Runner.xcodeproj').childFile('project.pbxproj')
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('''
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
''');
|
||||
macOS.childFile('Podfile')
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('''
|
||||
# platform :osx, '10.14'
|
||||
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
''');
|
||||
}
|
||||
|
||||
group('non-macOS host', () {
|
||||
setUp(() {
|
||||
mockPlatform = MockPlatform(isLinux: true);
|
||||
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,
|
||||
platform: mockPlatform,
|
||||
);
|
||||
runner = CommandRunner<void>(
|
||||
'create_all_test', 'Test for $CreateAllPackagesAppCommand');
|
||||
runner.addCommand(command);
|
||||
});
|
||||
|
||||
test('calls "flutter create"', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>['create-all-packages-app']);
|
||||
|
||||
expect(
|
||||
processRunner.recordedCalls,
|
||||
contains(ProcessCall(
|
||||
getFlutterCommand(mockPlatform),
|
||||
<String>[
|
||||
'create',
|
||||
'--template=app',
|
||||
'--project-name=$allPackagesProjectName',
|
||||
testRoot.childDirectory(allPackagesProjectName).path,
|
||||
],
|
||||
null)));
|
||||
});
|
||||
|
||||
test('pubspec includes all plugins', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
createFakePlugin('pluginb', packagesDir);
|
||||
createFakePlugin('pluginc', packagesDir);
|
||||
@ -80,6 +187,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('pubspec has overrides for all plugins', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
createFakePlugin('pluginb', packagesDir);
|
||||
createFakePlugin('pluginc', packagesDir);
|
||||
@ -97,33 +205,186 @@ void main() {
|
||||
]));
|
||||
});
|
||||
|
||||
test('pubspec preserves existing Dart SDK version', () async {
|
||||
const String baselineProjectName = 'baseline';
|
||||
final Directory baselineProjectDirectory =
|
||||
testRoot.childDirectory(baselineProjectName);
|
||||
io.Process.runSync(
|
||||
getFlutterCommand(const LocalPlatform()),
|
||||
<String>[
|
||||
'create',
|
||||
'--template=app',
|
||||
'--project-name=$baselineProjectName',
|
||||
baselineProjectDirectory.path,
|
||||
],
|
||||
);
|
||||
final Pubspec baselinePubspec =
|
||||
RepositoryPackage(baselineProjectDirectory).parsePubspec();
|
||||
test('legacy files are copied when requested', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
// Make a fake legacy source with all the necessary files, replacing one
|
||||
// of them.
|
||||
final Directory legacyDir = testRoot.childDirectory('legacy');
|
||||
final RepositoryPackage legacySource =
|
||||
RepositoryPackage(legacyDir.childDirectory(allPackagesProjectName));
|
||||
writeFakeFlutterCreateOutput(legacyDir, androidOnly: true);
|
||||
const String legacyAppBuildGradleContents = 'Fake legacy content';
|
||||
final File legacyGradleFile = legacySource
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childFile('build.gradle');
|
||||
legacyGradleFile.writeAsStringSync(legacyAppBuildGradleContents);
|
||||
|
||||
await runCapturingPrint(runner, <String>[
|
||||
'create-all-packages-app',
|
||||
'--legacy-source=${legacySource.path}',
|
||||
]);
|
||||
|
||||
final File buildGradle = command.app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childFile('build.gradle');
|
||||
|
||||
expect(buildGradle.readAsStringSync(), legacyAppBuildGradleContents);
|
||||
});
|
||||
|
||||
test('legacy directory replaces, rather than overlaying', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
final File extraFile =
|
||||
RepositoryPackage(testRoot.childDirectory(allPackagesProjectName))
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childFile('extra_file');
|
||||
extraFile.createSync(recursive: true);
|
||||
// Make a fake legacy source with all the necessary files, but not
|
||||
// including the extra file.
|
||||
final Directory legacyDir = testRoot.childDirectory('legacy');
|
||||
final RepositoryPackage legacySource =
|
||||
RepositoryPackage(legacyDir.childDirectory(allPackagesProjectName));
|
||||
writeFakeFlutterCreateOutput(legacyDir, androidOnly: true);
|
||||
|
||||
await runCapturingPrint(runner, <String>[
|
||||
'create-all-packages-app',
|
||||
'--legacy-source=${legacySource.path}',
|
||||
]);
|
||||
|
||||
expect(extraFile.existsSync(), false);
|
||||
});
|
||||
|
||||
test('legacy files are modified as needed by the tool', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
// Make a fake legacy source with all the necessary files, replacing one
|
||||
// of them.
|
||||
final Directory legacyDir = testRoot.childDirectory('legacy');
|
||||
final RepositoryPackage legacySource =
|
||||
RepositoryPackage(legacyDir.childDirectory(allPackagesProjectName));
|
||||
writeFakeFlutterCreateOutput(legacyDir, androidOnly: true);
|
||||
const String legacyAppBuildGradleContents = '''
|
||||
# This is the legacy file
|
||||
android {
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
defaultConfig {
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
}
|
||||
}
|
||||
''';
|
||||
final File legacyGradleFile = legacySource
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle');
|
||||
legacyGradleFile.writeAsStringSync(legacyAppBuildGradleContents);
|
||||
|
||||
await runCapturingPrint(runner, <String>[
|
||||
'create-all-packages-app',
|
||||
'--legacy-source=${legacySource.path}',
|
||||
]);
|
||||
|
||||
final List<String> buildGradle = command.app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle')
|
||||
.readAsLinesSync();
|
||||
|
||||
expect(
|
||||
buildGradle,
|
||||
containsAll(<Matcher>[
|
||||
contains('This is the legacy file'),
|
||||
contains('minSdkVersion 21'),
|
||||
contains('compileSdkVersion 33'),
|
||||
]));
|
||||
});
|
||||
|
||||
test('pubspec preserves existing Dart SDK version', () async {
|
||||
const String existingSdkConstraint = '>=1.0.0 <99.0.0';
|
||||
writeFakeFlutterCreateOutput(testRoot,
|
||||
dartSdkConstraint: existingSdkConstraint);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>['create-all-packages-app']);
|
||||
final Pubspec generatedPubspec = command.app.parsePubspec();
|
||||
|
||||
const String dartSdkKey = 'sdk';
|
||||
expect(generatedPubspec.environment?[dartSdkKey],
|
||||
baselinePubspec.environment?[dartSdkKey]);
|
||||
expect(generatedPubspec.environment?[dartSdkKey].toString(),
|
||||
existingSdkConstraint);
|
||||
});
|
||||
|
||||
test('Android app gradle is modified as expected', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>['create-all-packages-app']);
|
||||
|
||||
final List<String> buildGradle = command.app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle')
|
||||
.readAsLinesSync();
|
||||
|
||||
expect(
|
||||
buildGradle,
|
||||
containsAll(<Matcher>[
|
||||
contains('minSdkVersion 21'),
|
||||
contains('compileSdkVersion 33'),
|
||||
contains('multiDexEnabled true'),
|
||||
contains('androidx.lifecycle:lifecycle-runtime'),
|
||||
]));
|
||||
});
|
||||
|
||||
// The template's app/build.gradle does not always have a dependencies
|
||||
// section; ensure that the dependency is added if there is not one.
|
||||
test('Android lifecyle dependency is added with no dependencies', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot, appBuildGradleDependencies: '');
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>['create-all-packages-app']);
|
||||
|
||||
final List<String> buildGradle = command.app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle')
|
||||
.readAsLinesSync();
|
||||
|
||||
expect(
|
||||
buildGradle,
|
||||
containsAllInOrder(<Matcher>[
|
||||
equals('dependencies {'),
|
||||
contains('androidx.lifecycle:lifecycle-runtime'),
|
||||
equals('}'),
|
||||
]));
|
||||
});
|
||||
|
||||
// Some versions of the template's app/build.gradle has an empty
|
||||
// dependencies section; ensure that the dependency is added in that case.
|
||||
test('Android lifecyle dependency is added with empty dependencies',
|
||||
() async {
|
||||
writeFakeFlutterCreateOutput(testRoot,
|
||||
appBuildGradleDependencies: 'dependencies {}');
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>['create-all-packages-app']);
|
||||
|
||||
final List<String> buildGradle = command.app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle')
|
||||
.readAsLinesSync();
|
||||
|
||||
expect(
|
||||
buildGradle,
|
||||
containsAllInOrder(<Matcher>[
|
||||
equals('dependencies {'),
|
||||
contains('androidx.lifecycle:lifecycle-runtime'),
|
||||
equals('}'),
|
||||
]));
|
||||
});
|
||||
|
||||
test('macOS deployment target is modified in pbxproj', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>['create-all-packages-app']);
|
||||
@ -141,27 +402,50 @@ void main() {
|
||||
});
|
||||
|
||||
test('calls flutter pub get', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>['create-all-packages-app']);
|
||||
|
||||
expect(
|
||||
processRunner.recordedCalls,
|
||||
orderedEquals(<ProcessCall>[
|
||||
ProcessCall(
|
||||
getFlutterCommand(const LocalPlatform()),
|
||||
const <String>['pub', 'get'],
|
||||
testRoot.childDirectory('all_packages').path),
|
||||
]));
|
||||
},
|
||||
// See comment about Windows in create_all_packages_app_command.dart
|
||||
skip: io.Platform.isWindows);
|
||||
contains(ProcessCall(
|
||||
getFlutterCommand(mockPlatform),
|
||||
const <String>['pub', 'get'],
|
||||
testRoot.childDirectory(allPackagesProjectName).path)));
|
||||
});
|
||||
|
||||
test('fails if flutter pub get fails', () async {
|
||||
test('fails if flutter create fails', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
processRunner.mockProcessesForExecutable[
|
||||
getFlutterCommand(const LocalPlatform())] = <FakeProcessInfo>[
|
||||
processRunner
|
||||
.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
|
||||
<FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(exitCode: 1), <String>['create'])
|
||||
];
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['create-all-packages-app'], errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Failed to `flutter create`'),
|
||||
]));
|
||||
});
|
||||
|
||||
test('fails if flutter pub get fails', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
processRunner
|
||||
.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
|
||||
<FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(), <String>['create']),
|
||||
FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get'])
|
||||
];
|
||||
Error? commandError;
|
||||
@ -182,20 +466,22 @@ void main() {
|
||||
skip: io.Platform.isWindows);
|
||||
|
||||
test('handles --output-dir', () async {
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
final Directory customOutputDir =
|
||||
fileSystem.systemTempDirectory.createTempSync();
|
||||
writeFakeFlutterCreateOutput(customOutputDir);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>[
|
||||
'create-all-packages-app',
|
||||
'--output-dir=${customOutputDir.path}'
|
||||
]);
|
||||
|
||||
expect(command.app.path,
|
||||
customOutputDir.childDirectory('all_packages').path);
|
||||
customOutputDir.childDirectory(allPackagesProjectName).path);
|
||||
});
|
||||
|
||||
test('logs exclusions', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
createFakePlugin('pluginb', packagesDir);
|
||||
createFakePlugin('pluginc', packagesDir);
|
||||
@ -219,7 +505,6 @@ void main() {
|
||||
packagesDir,
|
||||
processRunner: processRunner,
|
||||
platform: MockPlatform(isMacOS: true),
|
||||
pluginsRoot: testRoot,
|
||||
);
|
||||
runner = CommandRunner<void>(
|
||||
'create_all_test', 'Test for $CreateAllPackagesAppCommand');
|
||||
@ -227,10 +512,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('macOS deployment target is modified in Podfile', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
final File podfileFile = RepositoryPackage(
|
||||
command.packagesDir.parent.childDirectory('all_packages'))
|
||||
command.packagesDir.parent.childDirectory(allPackagesProjectName))
|
||||
.platformDirectory(FlutterPlatform.macos)
|
||||
.childFile('Podfile');
|
||||
podfileFile.createSync(recursive: true);
|
||||
|
Reference in New Issue
Block a user