mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This renames the "module" template to the "application" template, and makes "application" the default. The existing "app" template is now deprecated. flutter create also now recognizes the type of project in an existing directory, and is able to recreate it without having the template type explicitly specified (although you can still do that). It does this now by first looking in the .metadata file for the new project_type field, and if it doesn't find that, then it looks at the directory structure. Also, the .metadata file is now overwritten even on an existing directory so that 1) the project_type can be added to legacy projects, and 2) the version of Flutter that updated the project last is updated. I also cleaned up a bunch of things in create_test.dart, added many more tests, and added an example test to the test/ directory in the generated output of the application template. Fixes #22530 Fixes #22344
519 lines
20 KiB
Dart
519 lines
20 KiB
Dart
// 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:flutter_tools/src/base/common.dart';
|
|
import 'package:flutter_tools/src/base/context.dart';
|
|
import 'package:flutter_tools/src/base/platform.dart';
|
|
import 'package:flutter_tools/src/cache.dart';
|
|
import 'package:flutter_tools/src/flutter_manifest.dart';
|
|
import 'package:flutter_tools/src/ios/ios_workflow.dart';
|
|
import 'package:flutter_tools/src/ios/xcodeproj.dart';
|
|
import 'package:flutter_tools/src/project.dart';
|
|
import 'package:flutter_tools/src/base/file_system.dart';
|
|
import 'package:file/file.dart';
|
|
import 'package:file/memory.dart';
|
|
import 'package:mockito/mockito.dart';
|
|
|
|
import 'src/common.dart';
|
|
import 'src/context.dart';
|
|
|
|
void main() {
|
|
group('Project', () {
|
|
group('construction', () {
|
|
testInMemory('fails on null directory', () async {
|
|
await expectLater(
|
|
FlutterProject.fromDirectory(null),
|
|
throwsA(isInstanceOf<AssertionError>()),
|
|
);
|
|
});
|
|
|
|
testInMemory('fails on invalid pubspec.yaml', () async {
|
|
final Directory directory = fs.directory('myproject');
|
|
directory.childFile('pubspec.yaml')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(invalidPubspec);
|
|
await expectToolExitLater(
|
|
FlutterProject.fromDirectory(directory),
|
|
contains('pubspec.yaml'),
|
|
);
|
|
});
|
|
|
|
testInMemory('fails on invalid example/pubspec.yaml', () async {
|
|
final Directory directory = fs.directory('myproject');
|
|
directory.childDirectory('example').childFile('pubspec.yaml')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(invalidPubspec);
|
|
await expectToolExitLater(
|
|
FlutterProject.fromDirectory(directory),
|
|
contains('pubspec.yaml'),
|
|
);
|
|
});
|
|
|
|
testInMemory('treats missing pubspec.yaml as empty', () async {
|
|
final Directory directory = fs.directory('myproject')
|
|
..createSync(recursive: true);
|
|
expect(
|
|
(await FlutterProject.fromDirectory(directory)).manifest.isEmpty,
|
|
true,
|
|
);
|
|
});
|
|
|
|
testInMemory('reads valid pubspec.yaml', () async {
|
|
final Directory directory = fs.directory('myproject');
|
|
directory.childFile('pubspec.yaml')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(validPubspec);
|
|
expect(
|
|
(await FlutterProject.fromDirectory(directory)).manifest.appName,
|
|
'hello',
|
|
);
|
|
});
|
|
|
|
testInMemory('sets up location', () async {
|
|
final Directory directory = fs.directory('myproject');
|
|
expect(
|
|
(await FlutterProject.fromDirectory(directory)).directory.absolute.path,
|
|
directory.absolute.path,
|
|
);
|
|
expect(
|
|
(await FlutterProject.fromPath(directory.path)).directory.absolute.path,
|
|
directory.absolute.path,
|
|
);
|
|
expect(
|
|
(await FlutterProject.current()).directory.absolute.path,
|
|
fs.currentDirectory.absolute.path,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('editable Android host app', () {
|
|
testInMemory('fails on non-application', () async {
|
|
final FlutterProject project = await someProject();
|
|
await expectLater(
|
|
project.android.makeHostAppEditable(),
|
|
throwsA(isInstanceOf<AssertionError>()),
|
|
);
|
|
});
|
|
testInMemory('exits on already editable application', () async {
|
|
final FlutterProject project = await anApplicationProject();
|
|
await project.android.makeHostAppEditable();
|
|
return expectToolExitLater(project.android.makeHostAppEditable(), contains('already editable'));
|
|
});
|
|
testInMemory('creates android/app folder in place of .android/app', () async {
|
|
final FlutterProject project = await anApplicationProject();
|
|
await project.android.makeHostAppEditable();
|
|
expectNotExists(project.directory.childDirectory('.android').childDirectory('app'));
|
|
expect(
|
|
project.directory.childDirectory('.android').childFile('settings.gradle').readAsStringSync(),
|
|
isNot(contains("include ':app'")),
|
|
);
|
|
expectExists(project.directory.childDirectory('android').childDirectory('app'));
|
|
expectExists(project.directory.childDirectory('android').childFile('local.properties'));
|
|
expect(
|
|
project.directory.childDirectory('android').childFile('settings.gradle').readAsStringSync(),
|
|
contains("include ':app'"),
|
|
);
|
|
});
|
|
testInMemory('retains .android/Flutter folder and references it', () async {
|
|
final FlutterProject project = await anApplicationProject();
|
|
await project.android.makeHostAppEditable();
|
|
expectExists(project.directory.childDirectory('.android').childDirectory('Flutter'));
|
|
expect(
|
|
project.directory.childDirectory('android').childFile('settings.gradle').readAsStringSync(),
|
|
contains('../.android/include_flutter.groovy'),
|
|
);
|
|
});
|
|
testInMemory('can be redone after deletion', () async {
|
|
final FlutterProject project = await anApplicationProject();
|
|
await project.android.makeHostAppEditable();
|
|
project.directory.childDirectory('android').deleteSync(recursive: true);
|
|
await project.android.makeHostAppEditable();
|
|
expectExists(project.directory.childDirectory('android').childDirectory('app'));
|
|
});
|
|
});
|
|
|
|
group('ensure ready for platform-specific tooling', () {
|
|
testInMemory('does nothing, if project is not created', () async {
|
|
final FlutterProject project = FlutterProject(
|
|
fs.directory('not_created'),
|
|
FlutterManifest.empty(),
|
|
FlutterManifest.empty(),
|
|
);
|
|
await project.ensureReadyForPlatformSpecificTooling();
|
|
expectNotExists(project.directory);
|
|
});
|
|
testInMemory('does nothing in plugin or package root project', () async {
|
|
final FlutterProject project = await aPluginProject();
|
|
await project.ensureReadyForPlatformSpecificTooling();
|
|
expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
|
|
expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
|
|
expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
|
|
expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
|
|
});
|
|
testInMemory('injects plugins for iOS', () async {
|
|
final FlutterProject project = await someProject();
|
|
await project.ensureReadyForPlatformSpecificTooling();
|
|
expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
|
|
});
|
|
testInMemory('generates Xcode configuration for iOS', () async {
|
|
final FlutterProject project = await someProject();
|
|
await project.ensureReadyForPlatformSpecificTooling();
|
|
expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
|
|
});
|
|
testInMemory('injects plugins for Android', () async {
|
|
final FlutterProject project = await someProject();
|
|
await project.ensureReadyForPlatformSpecificTooling();
|
|
expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
|
|
});
|
|
testInMemory('updates local properties for Android', () async {
|
|
final FlutterProject project = await someProject();
|
|
await project.ensureReadyForPlatformSpecificTooling();
|
|
expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
|
|
});
|
|
testInMemory('creates Android library in application', () async {
|
|
final FlutterProject project = await anApplicationProject();
|
|
await project.ensureReadyForPlatformSpecificTooling();
|
|
expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
|
|
expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
|
|
expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
|
|
});
|
|
testInMemory('creates iOS pod in application', () async {
|
|
final FlutterProject project = await anApplicationProject();
|
|
await project.ensureReadyForPlatformSpecificTooling();
|
|
final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
|
|
expectExists(flutter.childFile('podhelper.rb'));
|
|
expectExists(flutter.childFile('Generated.xcconfig'));
|
|
final Directory pluginRegistrantClasses = flutter
|
|
.childDirectory('FlutterPluginRegistrant')
|
|
.childDirectory('Classes');
|
|
expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.h'));
|
|
expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.m'));
|
|
});
|
|
});
|
|
|
|
group('application status', () {
|
|
testInMemory('is known for application', () async {
|
|
final FlutterProject project = await anApplicationProject();
|
|
expect(project.isApplication, isTrue);
|
|
expect(project.android.isApplication, isTrue);
|
|
expect(project.ios.isApplication, isTrue);
|
|
expect(project.android.hostAppGradleRoot.basename, '.android');
|
|
expect(project.ios.hostAppRoot.basename, '.ios');
|
|
});
|
|
testInMemory('is known for non-application', () async {
|
|
final FlutterProject project = await someProject();
|
|
expect(project.isApplication, isFalse);
|
|
expect(project.android.isApplication, isFalse);
|
|
expect(project.ios.isApplication, isFalse);
|
|
expect(project.android.hostAppGradleRoot.basename, 'android');
|
|
expect(project.ios.hostAppRoot.basename, 'ios');
|
|
});
|
|
});
|
|
|
|
group('example', () {
|
|
testInMemory('exists for plugin', () async {
|
|
final FlutterProject project = await aPluginProject();
|
|
expect(project.hasExampleApp, isTrue);
|
|
});
|
|
testInMemory('does not exist for non-plugin', () async {
|
|
final FlutterProject project = await someProject();
|
|
expect(project.hasExampleApp, isFalse);
|
|
});
|
|
});
|
|
|
|
group('product bundle identifier', () {
|
|
MemoryFileSystem fs;
|
|
MockIOSWorkflow mockIOSWorkflow;
|
|
MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
|
|
setUp(() {
|
|
fs = MemoryFileSystem();
|
|
mockIOSWorkflow = MockIOSWorkflow();
|
|
mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
|
|
});
|
|
|
|
void testWithMocks(String description, Future<Null> testMethod()) {
|
|
testUsingContext(description, testMethod, overrides: <Type, Generator>{
|
|
FileSystem: () => fs,
|
|
IOSWorkflow: () => mockIOSWorkflow,
|
|
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
|
|
});
|
|
}
|
|
|
|
testWithMocks('null, if no pbxproj or plist entries', () async {
|
|
final FlutterProject project = await someProject();
|
|
expect(project.ios.productBundleIdentifier, isNull);
|
|
});
|
|
testWithMocks('from pbxproj file, if no plist', () async {
|
|
final FlutterProject project = await someProject();
|
|
addIosWithBundleId(project.directory, 'io.flutter.someProject');
|
|
expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
|
|
});
|
|
testWithMocks('from plist, if no variables', () async {
|
|
final FlutterProject project = await someProject();
|
|
when(mockIOSWorkflow.getPlistValueFromFile(any, any)).thenReturn('io.flutter.someProject');
|
|
expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
|
|
});
|
|
testWithMocks('from pbxproj and plist, if default variable', () async {
|
|
final FlutterProject project = await someProject();
|
|
addIosWithBundleId(project.directory, 'io.flutter.someProject');
|
|
when(mockIOSWorkflow.getPlistValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER)');
|
|
expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
|
|
});
|
|
testWithMocks('from pbxproj and plist, by substitution', () async {
|
|
final FlutterProject project = await someProject();
|
|
when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
|
|
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
|
'SUFFIX': 'suffix',
|
|
});
|
|
when(mockIOSWorkflow.getPlistValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER).\$(SUFFIX)');
|
|
expect(project.ios.productBundleIdentifier, 'io.flutter.someProject.suffix');
|
|
});
|
|
});
|
|
|
|
group('organization names set', () {
|
|
testInMemory('is empty, if project not created', () async {
|
|
final FlutterProject project = await someProject();
|
|
expect(project.organizationNames, isEmpty);
|
|
});
|
|
testInMemory('is empty, if no platform folders exist', () async {
|
|
final FlutterProject project = await someProject();
|
|
project.directory.createSync();
|
|
expect(project.organizationNames, isEmpty);
|
|
});
|
|
testInMemory('is populated from iOS bundle identifier', () async {
|
|
final FlutterProject project = await someProject();
|
|
addIosWithBundleId(project.directory, 'io.flutter.someProject');
|
|
expect(project.organizationNames, <String>['io.flutter']);
|
|
});
|
|
testInMemory('is populated from Android application ID', () async {
|
|
final FlutterProject project = await someProject();
|
|
addAndroidWithApplicationId(project.directory, 'io.flutter.someproject');
|
|
expect(project.organizationNames, <String>['io.flutter']);
|
|
});
|
|
testInMemory('is populated from iOS bundle identifier in plugin example', () async {
|
|
final FlutterProject project = await someProject();
|
|
addIosWithBundleId(project.example.directory, 'io.flutter.someProject');
|
|
expect(project.organizationNames, <String>['io.flutter']);
|
|
});
|
|
testInMemory('is populated from Android application ID in plugin example', () async {
|
|
final FlutterProject project = await someProject();
|
|
addAndroidWithApplicationId(project.example.directory, 'io.flutter.someproject');
|
|
expect(project.organizationNames, <String>['io.flutter']);
|
|
});
|
|
testInMemory('is populated from Android group in plugin', () async {
|
|
final FlutterProject project = await someProject();
|
|
addAndroidWithGroup(project.directory, 'io.flutter.someproject');
|
|
expect(project.organizationNames, <String>['io.flutter']);
|
|
});
|
|
testInMemory('is singleton, if sources agree', () async {
|
|
final FlutterProject project = await someProject();
|
|
addIosWithBundleId(project.directory, 'io.flutter.someProject');
|
|
addAndroidWithApplicationId(project.directory, 'io.flutter.someproject');
|
|
expect(project.organizationNames, <String>['io.flutter']);
|
|
});
|
|
testInMemory('is non-singleton, if sources disagree', () async {
|
|
final FlutterProject project = await someProject();
|
|
addIosWithBundleId(project.directory, 'io.flutter.someProject');
|
|
addAndroidWithApplicationId(project.directory, 'io.clutter.someproject');
|
|
expect(
|
|
project.organizationNames,
|
|
<String>['io.flutter', 'io.clutter'],
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
Future<FlutterProject> someProject() async {
|
|
final Directory directory = fs.directory('some_project');
|
|
directory.childFile('.packages').createSync(recursive: true);
|
|
directory.childDirectory('ios').createSync(recursive: true);
|
|
directory.childDirectory('android').createSync(recursive: true);
|
|
return FlutterProject.fromDirectory(directory);
|
|
}
|
|
|
|
Future<FlutterProject> aPluginProject() async {
|
|
final Directory directory = fs.directory('plugin_project');
|
|
directory.childDirectory('ios').createSync(recursive: true);
|
|
directory.childDirectory('android').createSync(recursive: true);
|
|
directory.childDirectory('example').createSync(recursive: true);
|
|
directory.childFile('pubspec.yaml').writeAsStringSync('''
|
|
name: my_plugin
|
|
flutter:
|
|
plugin:
|
|
androidPackage: com.example
|
|
pluginClass: MyPlugin
|
|
iosPrefix: FLT
|
|
''');
|
|
return FlutterProject.fromDirectory(directory);
|
|
}
|
|
|
|
Future<FlutterProject> anApplicationProject() async {
|
|
final Directory directory = fs.directory('application_project');
|
|
directory.childFile('.packages').createSync(recursive: true);
|
|
directory.childFile('pubspec.yaml').writeAsStringSync('''
|
|
name: my_application
|
|
flutter:
|
|
application:
|
|
androidPackage: com.example
|
|
''');
|
|
return FlutterProject.fromDirectory(directory);
|
|
}
|
|
|
|
/// Executes the [testMethod] in a context where the file system
|
|
/// is in memory.
|
|
void testInMemory(String description, Future<Null> testMethod()) {
|
|
Cache.flutterRoot = getFlutterRoot();
|
|
final FileSystem testFileSystem = MemoryFileSystem(
|
|
style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix,
|
|
);
|
|
// Transfer needed parts of the Flutter installation folder
|
|
// to the in-memory file system used during testing.
|
|
transfer(Cache().getArtifactDirectory('gradle_wrapper'), testFileSystem);
|
|
transfer(fs.directory(Cache.flutterRoot)
|
|
.childDirectory('packages')
|
|
.childDirectory('flutter_tools')
|
|
.childDirectory('templates'), testFileSystem);
|
|
transfer(fs.directory(Cache.flutterRoot)
|
|
.childDirectory('packages')
|
|
.childDirectory('flutter_tools')
|
|
.childDirectory('schema'), testFileSystem);
|
|
testUsingContext(
|
|
description,
|
|
testMethod,
|
|
overrides: <Type, Generator>{
|
|
FileSystem: () => testFileSystem,
|
|
Cache: () => Cache(),
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Transfers files and folders from the local file system's Flutter
|
|
/// installation to an (in-memory) file system used for testing.
|
|
void transfer(FileSystemEntity entity, FileSystem target) {
|
|
if (entity is Directory) {
|
|
target.directory(entity.absolute.path).createSync(recursive: true);
|
|
for (FileSystemEntity child in entity.listSync()) {
|
|
transfer(child, target);
|
|
}
|
|
} else if (entity is File) {
|
|
target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
|
|
} else {
|
|
throw 'Unsupported FileSystemEntity ${entity.runtimeType}';
|
|
}
|
|
}
|
|
|
|
Future<Null> expectToolExitLater(Future<dynamic> future, Matcher messageMatcher) async {
|
|
try {
|
|
await future;
|
|
fail('ToolExit expected, but nothing thrown');
|
|
} on ToolExit catch(e) {
|
|
expect(e.message, messageMatcher);
|
|
} catch(e, trace) {
|
|
fail('ToolExit expected, got $e\n$trace');
|
|
}
|
|
}
|
|
|
|
void expectExists(FileSystemEntity entity) {
|
|
expect(entity.existsSync(), isTrue);
|
|
}
|
|
|
|
void expectNotExists(FileSystemEntity entity) {
|
|
expect(entity.existsSync(), isFalse);
|
|
}
|
|
|
|
void addIosWithBundleId(Directory directory, String id) {
|
|
directory
|
|
.childDirectory('ios')
|
|
.childDirectory('Runner.xcodeproj')
|
|
.childFile('project.pbxproj')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(projectFileWithBundleId(id));
|
|
}
|
|
|
|
void addAndroidWithApplicationId(Directory directory, String id) {
|
|
directory
|
|
.childDirectory('android')
|
|
.childDirectory('app')
|
|
.childFile('build.gradle')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(gradleFileWithApplicationId(id));
|
|
}
|
|
|
|
void addAndroidWithGroup(Directory directory, String id) {
|
|
directory.childDirectory('android').childFile('build.gradle')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(gradleFileWithGroupId(id));
|
|
}
|
|
|
|
String get validPubspec => '''
|
|
name: hello
|
|
flutter:
|
|
''';
|
|
|
|
String get invalidPubspec => '''
|
|
name: hello
|
|
flutter:
|
|
invalid:
|
|
''';
|
|
|
|
String projectFileWithBundleId(String id) {
|
|
return '''
|
|
97C147061CF9000F007C117D /* Debug */ = {
|
|
isa = XCBuildConfiguration;
|
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
|
buildSettings = {
|
|
PRODUCT_BUNDLE_IDENTIFIER = $id;
|
|
PRODUCT_NAME = "\$(TARGET_NAME)";
|
|
};
|
|
name = Debug;
|
|
};
|
|
''';
|
|
}
|
|
|
|
String gradleFileWithApplicationId(String id) {
|
|
return '''
|
|
apply plugin: 'com.android.application'
|
|
android {
|
|
compileSdkVersion 27
|
|
|
|
defaultConfig {
|
|
applicationId '$id'
|
|
}
|
|
}
|
|
''';
|
|
}
|
|
|
|
String gradleFileWithGroupId(String id) {
|
|
return '''
|
|
group '$id'
|
|
version '1.0-SNAPSHOT'
|
|
|
|
apply plugin: 'com.android.library'
|
|
|
|
android {
|
|
compileSdkVersion 27
|
|
}
|
|
''';
|
|
}
|
|
|
|
File androidPluginRegistrant(Directory parent) {
|
|
return parent.childDirectory('src')
|
|
.childDirectory('main')
|
|
.childDirectory('java')
|
|
.childDirectory('io')
|
|
.childDirectory('flutter')
|
|
.childDirectory('plugins')
|
|
.childFile('GeneratedPluginRegistrant.java');
|
|
}
|
|
|
|
class MockIOSWorkflow extends Mock implements IOSWorkflow {}
|
|
|
|
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {
|
|
@override
|
|
bool get isInstalled => true;
|
|
}
|