Avoid null manifests in FlutterProject (#20332)
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 0ef82d0..2473479 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -203,18 +203,13 @@ /// Overwrite local.properties in the specified Flutter project's Android /// sub-project, if needed. /// -/// Throws tool exit, if `pubspec.yaml` is invalid. -/// -/// If [requireSdk] is `true` this will fail with a tool exit if no Android Sdk -/// is found. +/// If [requireAndroidSdk] is true (the default) and no Android SDK is found, +/// this will fail with a [ToolExit]. Future<void> updateLocalProperties({ @required FlutterProject project, BuildInfo buildInfo, bool requireAndroidSdk = true, }) async { - if (project.manifest == null) { - throwToolExit('Invalid `pubspec.yaml`'); - } if (requireAndroidSdk && androidSdk == null) { throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.'); }
diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart index 3cfb1e7..81a1e64 100644 --- a/packages/flutter_tools/lib/src/flutter_manifest.dart +++ b/packages/flutter_tools/lib/src/flutter_manifest.dart
@@ -28,14 +28,6 @@ return manifest; } - /// Returns a mock manifest with the given contents. - static FlutterManifest mock(Map<String, dynamic> contents) { - final FlutterManifest manifest = new FlutterManifest._(); - manifest._descriptor = contents ?? const <String, dynamic>{}; - manifest._flutterDescriptor = manifest._descriptor['flutter'] ?? const <String, dynamic>{}; - return manifest; - } - /// Returns null on invalid manifest. Returns empty manifest on missing file. static Future<FlutterManifest> createFromPath(String path) async { if (path == null || !fs.isFileSync(path)) @@ -78,8 +70,10 @@ /// A map representation of the `flutter` section in the `pubspec.yaml` file. Map<String, dynamic> _flutterDescriptor; + /// True if the `pubspec.yaml` file does not exist. bool get isEmpty => _descriptor.isEmpty; + /// The string value of the top-level `name` property in the `pubspec.yaml` file. String get appName => _descriptor['name'] ?? ''; /// The version String from the `pubspec.yaml` file.
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 18b629b..c2aeaca 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -16,7 +16,6 @@ import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; -import '../flutter_manifest.dart'; import '../globals.dart'; import '../project.dart'; @@ -59,20 +58,19 @@ localsBuffer.writeln('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}'); - final FlutterManifest manifest = project.manifest; - if (!manifest.isModule) { + if (!project.isModule) { // For module projects we do not want to write the FLUTTER_FRAMEWORK_DIR // explicitly. Rather we rely on the xcode backend script and the Podfile // logic to derive it from FLUTTER_ROOT and FLUTTER_BUILD_MODE. localsBuffer.writeln('FLUTTER_FRAMEWORK_DIR=${flutterFrameworkDir(buildInfo.mode)}'); } - final String buildName = buildInfo?.buildName ?? manifest.buildName; + final String buildName = buildInfo?.buildName ?? project.manifest.buildName; if (buildName != null) { localsBuffer.writeln('FLUTTER_BUILD_NAME=$buildName'); } - final int buildNumber = buildInfo?.buildNumber ?? manifest.buildNumber; + final int buildNumber = buildInfo?.buildNumber ?? project.manifest.buildNumber; if (buildNumber != null) { localsBuffer.writeln('FLUTTER_BUILD_NUMBER=$buildNumber'); }
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index b10192c..3c8743e 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart
@@ -8,6 +8,7 @@ import 'package:meta/meta.dart'; import 'android/gradle.dart' as gradle; +import 'base/common.dart'; import 'base/file_system.dart'; import 'build_info.dart'; import 'bundle.dart' as bundle; @@ -28,34 +29,39 @@ /// cached. class FlutterProject { @visibleForTesting - FlutterProject(this.directory, this.manifest, this._exampleManifest); + FlutterProject(this.directory, this.manifest, this._exampleManifest) + : assert(directory != null), + assert(manifest != null), + assert(_exampleManifest != null); - /// Returns a future that completes with a FlutterProject view of the given directory. + /// Returns a future that completes with a [FlutterProject] view of the given directory + /// or a ToolExit error, if `pubspec.yaml` or `example/pubspec.yaml` is invalid. static Future<FlutterProject> fromDirectory(Directory directory) async { - final FlutterManifest manifest = await FlutterManifest.createFromPath( + assert(directory != null); + final FlutterManifest manifest = await _readManifest( directory.childFile(bundle.defaultManifestPath).path, ); - final Directory exampleDirectory = _exampleDirectory(directory); - final FlutterManifest exampleManifest = await FlutterManifest.createFromPath( - exampleDirectory.childFile(bundle.defaultManifestPath).path, + final FlutterManifest exampleManifest = await _readManifest( + _exampleDirectory(directory).childFile(bundle.defaultManifestPath).path, ); return new FlutterProject(directory, manifest, exampleManifest); } - /// Returns a future that completes with a FlutterProject view of the current directory. + /// Returns a future that completes with a [FlutterProject] view of the current directory. + /// or a ToolExit error, if `pubspec.yaml` or `example/pubspec.yaml` is invalid. static Future<FlutterProject> current() => fromDirectory(fs.currentDirectory); - /// Returns a future that completes with a FlutterProject view of the given directory. + /// Returns a future that completes with a [FlutterProject] view of the given directory. + /// or a ToolExit error, if `pubspec.yaml` or `example/pubspec.yaml` is invalid. static Future<FlutterProject> fromPath(String path) => fromDirectory(fs.directory(path)); /// The location of this project. final Directory directory; - /// The manifest of this project, or null, if `pubspec.yaml` is invalid. + /// The manifest of this project. final FlutterManifest manifest; - /// The manifest of the example sub-project of this project, or null, if - /// `example/pubspec.yaml` is invalid. + /// The manifest of the example sub-project of this project. final FlutterManifest _exampleManifest; /// Asynchronously returns the organization names found in this project as @@ -96,7 +102,8 @@ FlutterManifest.empty(), ); - bool get isModule => manifest != null && manifest.isModule; + /// True, if this project is a Flutter module. + bool get isModule => manifest.isModule; /// True, if this project has an example application. bool get hasExampleApp => _exampleDirectory(directory).existsSync(); @@ -104,6 +111,18 @@ /// The directory that will contain the example if an example exists. static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example'); + /// Reads and validates the `pubspec.yaml` file at [path], asynchronously + /// returning a [FlutterManifest] representation of the contents. + /// + /// Completes with an empty [FlutterManifest], if the file does not exist. + /// Completes with a ToolExit on validation error. + static Future<FlutterManifest> _readManifest(String path) async { + final FlutterManifest manifest = await FlutterManifest.createFromPath(path); + if (manifest == null) + throwToolExit('Please correct the pubspec.yaml file at $path'); + return manifest; + } + /// Generates project files necessary to make Gradle builds work on Android /// and CocoaPods+Xcode work on iOS, for app and module projects only. Future<void> ensureReadyForPlatformSpecificTooling() async {
diff --git a/packages/flutter_tools/test/project_test.dart b/packages/flutter_tools/test/project_test.dart index eaf56ea..8c53e66 100644 --- a/packages/flutter_tools/test/project_test.dart +++ b/packages/flutter_tools/test/project_test.dart
@@ -3,7 +3,9 @@ // 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/project.dart'; @@ -11,27 +13,97 @@ import 'package:test/test.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'src/common.dart'; import 'src/context.dart'; void main() { group('Project', () { - testInMemory('knows 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('construction', () { + testInMemory('fails on null directory', () async { + await expectLater( + FlutterProject.fromDirectory(null), + throwsA(const isInstanceOf<AssertionError>()), + ); + }); + + 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'); + } + } + + 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('ensure ready for platform-specific tooling', () { + void expectExists(FileSystemEntity entity) { + expect(entity.existsSync(), isTrue); + } + void expectNotExists(FileSystemEntity entity) { + expect(entity.existsSync(), isFalse); + } testInMemory('does nothing, if project is not created', () async { final FlutterProject project = new FlutterProject( fs.directory('not_created'), @@ -39,137 +111,130 @@ FlutterManifest.empty(), ); await project.ensureReadyForPlatformSpecificTooling(); - expect(project.directory.existsSync(), isFalse); + expectNotExists(project.directory); }); testInMemory('does nothing in plugin or package root project', () async { - final FlutterProject project = aPluginProject(); + final FlutterProject project = await aPluginProject(); await project.ensureReadyForPlatformSpecificTooling(); - expect(project.ios.directory.childFile('Runner/GeneratedPluginRegistrant.h').existsSync(), isFalse); - expect(project.android.directory.childFile( - 'app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', - ).existsSync(), isFalse); - expect(project.ios.directory.childFile('Flutter/Generated.xcconfig').existsSync(), isFalse); - expect(project.android.directory.childFile('local.properties').existsSync(), isFalse); + expectNotExists(project.ios.directory.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h')); + expectNotExists(androidPluginRegistrant(project.android.directory.childDirectory('app'))); + expectNotExists(project.ios.directory.childDirectory('Flutter').childFile('Generated.xcconfig')); + expectNotExists(project.android.directory.childFile('local.properties')); }); testInMemory('injects plugins for iOS', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); await project.ensureReadyForPlatformSpecificTooling(); - expect(project.ios.directory.childFile('Runner/GeneratedPluginRegistrant.h').existsSync(), isTrue); + expectExists(project.ios.directory.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h')); }); testInMemory('generates Xcode configuration for iOS', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); await project.ensureReadyForPlatformSpecificTooling(); - expect(project.ios.directory.childFile('Flutter/Generated.xcconfig').existsSync(), isTrue); + expectExists(project.ios.directory.childDirectory('Flutter').childFile('Generated.xcconfig')); }); testInMemory('injects plugins for Android', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); await project.ensureReadyForPlatformSpecificTooling(); - expect(project.android.directory.childFile( - 'app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', - ).existsSync(), isTrue); + expectExists(androidPluginRegistrant(project.android.directory.childDirectory('app'))); }); testInMemory('updates local properties for Android', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); await project.ensureReadyForPlatformSpecificTooling(); - expect(project.android.directory.childFile('local.properties').existsSync(), isTrue); + expectExists(project.android.directory.childFile('local.properties')); }); testInMemory('creates Android library in module', () async { - final FlutterProject project = aModuleProject(); + final FlutterProject project = await aModuleProject(); await project.ensureReadyForPlatformSpecificTooling(); - expect(project.android.directory.childFile('template_content').existsSync(), isTrue); - expect(project.android.directory.childFile('local.properties').existsSync(), isTrue); - expect(project.android.directory.childFile( - 'Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java', - ).existsSync(), isTrue); + expectExists(project.android.directory.childFile('settings.gradle')); + expectExists(project.android.directory.childFile('local.properties')); + expectExists(androidPluginRegistrant(project.android.directory.childDirectory('Flutter'))); }); testInMemory('creates iOS pod in module', () async { - final FlutterProject project = aModuleProject(); + final FlutterProject project = await aModuleProject(); await project.ensureReadyForPlatformSpecificTooling(); final Directory flutter = project.ios.directory.childDirectory('Flutter'); - expect(flutter.childFile('template_content').existsSync(), isTrue); - expect(flutter.childFile('Generated.xcconfig').existsSync(), isTrue); - expect(flutter.childFile( - 'FlutterPluginRegistrant/Classes/GeneratedPluginRegistrant.h', - ).existsSync(), isTrue); - expect(flutter.childFile( - 'FlutterPluginRegistrant/Classes/GeneratedPluginRegistrant.m', - ).existsSync(), isTrue); + 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('module status', () { testInMemory('is known for module', () async { - final FlutterProject project = aModuleProject(); + final FlutterProject project = await aModuleProject(); expect(project.isModule, isTrue); expect(project.android.isModule, isTrue); expect(project.ios.isModule, isTrue); - expect(project.android.directory.path, startsWith('module_project/.android')); - expect(project.ios.directory.path, startsWith('module_project/.ios')); + expect(project.android.directory.basename, '.android'); + expect(project.ios.directory.basename, '.ios'); }); testInMemory('is known for non-module', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); expect(project.isModule, isFalse); expect(project.android.isModule, isFalse); expect(project.ios.isModule, isFalse); - expect(project.android.directory.path, startsWith('some_project/android')); - expect(project.ios.directory.path, startsWith('some_project/ios')); + expect(project.android.directory.basename, 'android'); + expect(project.ios.directory.basename, 'ios'); }); }); group('example', () { testInMemory('exists for plugin', () async { - final FlutterProject project = aPluginProject(); + final FlutterProject project = await aPluginProject(); expect(project.hasExampleApp, isTrue); }); testInMemory('does not exist for non-plugin', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); expect(project.hasExampleApp, isFalse); }); }); group('organization names set', () { testInMemory('is empty, if project not created', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); expect(await project.organizationNames(), isEmpty); }); testInMemory('is empty, if no platform folders exist', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); project.directory.createSync(); expect(await project.organizationNames(), isEmpty); }); testInMemory('is populated from iOS bundle identifier', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); addIosWithBundleId(project.directory, 'io.flutter.someProject'); expect(await project.organizationNames(), <String>['io.flutter']); }); testInMemory('is populated from Android application ID', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); addAndroidWithApplicationId(project.directory, 'io.flutter.someproject'); expect(await project.organizationNames(), <String>['io.flutter']); }); testInMemory('is populated from iOS bundle identifier in plugin example', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); addIosWithBundleId(project.example.directory, 'io.flutter.someProject'); expect(await project.organizationNames(), <String>['io.flutter']); }); testInMemory('is populated from Android application ID in plugin example', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); addAndroidWithApplicationId(project.example.directory, 'io.flutter.someproject'); expect(await project.organizationNames(), <String>['io.flutter']); }); testInMemory('is populated from Android group in plugin', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); addAndroidWithGroup(project.directory, 'io.flutter.someproject'); expect(await project.organizationNames(), <String>['io.flutter']); }); testInMemory('is singleton, if sources agree', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); addIosWithBundleId(project.directory, 'io.flutter.someProject'); addAndroidWithApplicationId(project.directory, 'io.flutter.someproject'); expect(await project.organizationNames(), <String>['io.flutter']); }); testInMemory('is non-singleton, if sources disagree', () async { - final FlutterProject project = someProject(); + final FlutterProject project = await someProject(); addIosWithBundleId(project.directory, 'io.flutter.someProject'); addAndroidWithApplicationId(project.directory, 'io.clutter.someproject'); expect( @@ -181,92 +246,85 @@ }); } -FlutterProject someProject() { +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 new FlutterProject( - directory, - FlutterManifest.empty(), - FlutterManifest.empty(), - ); + return FlutterProject.fromDirectory(directory); } -FlutterProject aPluginProject() { +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); - return new FlutterProject( - directory, - FlutterManifest.mock(const <String, dynamic>{ - 'flutter': <String, dynamic>{ - 'plugin': <String, dynamic>{} - } - }), - FlutterManifest.empty(), - ); + directory.childFile('pubspec.yaml').writeAsStringSync(''' +name: my_plugin +flutter: + plugin: + androidPackage: com.example + pluginClass: MyPlugin + iosPrefix: FLT +'''); + return FlutterProject.fromDirectory(directory); } -FlutterProject aModuleProject() { +Future<FlutterProject> aModuleProject() async { final Directory directory = fs.directory('module_project'); directory.childFile('.packages').createSync(recursive: true); - return new FlutterProject( - directory, - FlutterManifest.mock(const <String, dynamic>{ - 'flutter': <String, dynamic>{ - 'module': <String, dynamic>{ - 'androidPackage': 'com.example' - } - } - }), - FlutterManifest.empty(), - ); + directory.childFile('pubspec.yaml').writeAsStringSync(''' +name: my_module +flutter: + module: + 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 = 'flutter'; - final FileSystem fs = new MemoryFileSystem(); - // Pretend we have a Flutter module project template. - fs.directory(Cache.flutterRoot) + Cache.flutterRoot = getFlutterRoot(); + final FileSystem testFileSystem = new 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(new Cache().getArtifactDirectory('gradle_wrapper'), testFileSystem); + transfer(fs.directory(Cache.flutterRoot) .childDirectory('packages') .childDirectory('flutter_tools') - .childDirectory('templates') - .childDirectory('module') - .childDirectory('android') - .childFile('template_content.copy.tmpl') - .createSync(recursive: true); - fs.directory(Cache.flutterRoot) + .childDirectory('templates'), testFileSystem); + transfer(fs.directory(Cache.flutterRoot) .childDirectory('packages') .childDirectory('flutter_tools') - .childDirectory('templates') - .childDirectory('module') - .childDirectory('ios') - .childDirectory('Flutter.tmpl') - .childFile('template_content.copy.tmpl') - .createSync(recursive: true); - - // Sets up cache in a test execution context where `fs` is the file system. - Cache cacheCreator() { - final Cache cache = new Cache(rootOverride: fs.directory('flutter')); - cache.getArtifactDirectory('gradle_wrapper') - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.properties') - .createSync(recursive: true); - return cache; - } + .childDirectory('schema'), testFileSystem); testUsingContext( description, testMethod, overrides: <Type, Generator>{ - FileSystem: () => fs, - Cache: cacheCreator, + FileSystem: () => testFileSystem, + Cache: () => new 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}'; + } +} + void addIosWithBundleId(Directory directory, String id) { directory .childDirectory('ios') @@ -291,6 +349,17 @@ ..writeAsStringSync(gradleFileWithGroupId(id)); } +String get validPubspec => ''' +name: hello +flutter: +'''; + +String get invalidPubspec => ''' +name: hello +flutter: + invalid: +'''; + String projectFileWithBundleId(String id) { return ''' 97C147061CF9000F007C117D /* Debug */ = { @@ -330,3 +399,13 @@ } '''; } + +File androidPluginRegistrant(Directory parent) { + return parent.childDirectory('src') + .childDirectory('main') + .childDirectory('java') + .childDirectory('io') + .childDirectory('flutter') + .childDirectory('plugins') + .childFile('GeneratedPluginRegistrant.java'); +}