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');
+}