Bundle assets used in packages (#11751)

diff --git a/packages/flutter/lib/src/services/image_provider.dart b/packages/flutter/lib/src/services/image_provider.dart
index ac48364..c29b52a 100644
--- a/packages/flutter/lib/src/services/image_provider.dart
+++ b/packages/flutter/lib/src/services/image_provider.dart
@@ -589,27 +589,94 @@
 
 /// Fetches an image from an [AssetBundle], associating it with the given scale.
 ///
-/// This implementation requires an explicit final [name] and [scale] on
+/// This implementation requires an explicit final [assetName] and [scale] on
 /// construction, and ignores the device pixel ratio and size in the
 /// configuration passed into [resolve]. For a resolution-aware variant that
 /// uses the configuration to pick an appropriate image based on the device
 /// pixel ratio and size, see [AssetImage].
+///
+/// ## Fetching assets
+///
+/// When fetching an image provided by the app itself, use the [assetName]
+/// argument to name the asset to choose. For instance, consider a directory
+/// `icons` with an image `heart.png`. First, the [pubspec.yaml] of the project
+/// should specify its assets in the `flutter` section:
+///
+/// ```yaml
+/// flutter:
+///   assets:
+///     - icons/heart.png
+/// ```
+///
+/// Then, to fetch the image and associate it with scale `1.5`, use
+///
+/// ```dart
+/// new AssetImage('icons/heart.png', scale: 1.5)
+/// ```
+///
+///## Assets in packages
+///
+/// To fetch an asset from a package, the [package] argument must be provided.
+/// For instance, suppose the structure above is inside a package called
+/// `my_icons`. Then to fetch the image, use:
+///
+/// ```dart
+/// new AssetImage('icons/heart.png', scale: 1.5, package: 'my_icons')
+/// ```
+///
+/// Assets used by the package itself should also be fetched using the [package]
+/// argument as above.
+///
+/// If the desired asset is specified in the [pubspec.yaml] of the package, it
+/// is bundled automatically with the app. In particular, assets used by the
+/// package itself must be specified in its [pubspec.yaml].
+///
+/// A package can also choose to have assets in its 'lib/' folder that are not
+/// specified in its [pubspec.yaml]. In this case for those images to be
+/// bundled, the app has to specify which ones to include. For instance a
+/// package named `fancy_backgrounds` could have:
+///
+/// ```
+/// lib/backgrounds/background1.png
+/// lib/backgrounds/background2.png
+/// lib/backgrounds/background3.png
+///```
+///
+/// To include, say the first image, the [pubspec.yaml] of the app should specify
+/// it in the `assets` section:
+///
+/// ```yaml
+///  assets:
+///    - packages/fancy_backgrounds/backgrounds/background1.png
+/// ```
+///
+/// Note that the `lib/` is implied, so it should not be included in the asset
+/// path.
+///
 class ExactAssetImage extends AssetBundleImageProvider {
   /// Creates an object that fetches the given image from an asset bundle.
   ///
-  /// The [name] and [scale] arguments must not be null. The [scale] arguments
+  /// The [assetName] and [scale] arguments must not be null. The [scale] arguments
   /// defaults to 1.0. The [bundle] argument may be null, in which case the
   /// bundle provided in the [ImageConfiguration] passed to the [resolve] call
   /// will be used instead.
-  const ExactAssetImage(this.name, {
+  ///
+  /// The [package] argument must be non-null when fetching an asset that is
+  /// included in a package. See the documentation for the [ExactAssetImage] class
+  /// itself for details.
+  const ExactAssetImage(this.assetName, {
     this.scale: 1.0,
-    this.bundle
-  }) : assert(name != null),
+    this.bundle,
+    this.package,
+  }) : assert(assetName != null),
        assert(scale != null);
 
+  /// The name of the asset.
+  final String assetName;
+
   /// The key to use to obtain the resource from the [bundle]. This is the
   /// argument passed to [AssetBundle.load].
-  final String name;
+  String get keyName => package == null ? assetName : 'packages/$package/$assetName';
 
   /// The scale to place in the [ImageInfo] object of the image.
   final double scale;
@@ -621,14 +688,18 @@
   /// that is also null, the [rootBundle] is used.
   ///
   /// The image is obtained by calling [AssetBundle.load] on the given [bundle]
-  /// using the key given by [name].
+  /// using the key given by [keyName].
   final AssetBundle bundle;
 
+  /// The name of the package from which the image is included. See the
+  /// documentation for the [ExactAssetImage] class itself for details.
+  final String package;
+
   @override
   Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
     return new SynchronousFuture<AssetBundleImageKey>(new AssetBundleImageKey(
       bundle: bundle ?? configuration.bundle ?? rootBundle,
-      name: name,
+      name: keyName,
       scale: scale
     ));
   }
@@ -638,14 +709,14 @@
     if (other.runtimeType != runtimeType)
       return false;
     final ExactAssetImage typedOther = other;
-    return name == typedOther.name
+    return keyName == typedOther.keyName
         && scale == typedOther.scale
         && bundle == typedOther.bundle;
   }
 
   @override
-  int get hashCode => hashValues(name, scale, bundle);
+  int get hashCode => hashValues(keyName, scale, bundle);
 
   @override
-  String toString() => '$runtimeType(name: "$name", scale: $scale, bundle: $bundle)';
+  String toString() => '$runtimeType(name: "$keyName", scale: $scale, bundle: $bundle)';
 }
diff --git a/packages/flutter/lib/src/services/image_resolution.dart b/packages/flutter/lib/src/services/image_resolution.dart
index 78764c3..0f6b1e8 100644
--- a/packages/flutter/lib/src/services/image_resolution.dart
+++ b/packages/flutter/lib/src/services/image_resolution.dart
@@ -55,16 +55,84 @@
 /// icons/1.5x/heart.png
 /// icons/2.0x/heart.png
 /// ```
+///
+/// ## Fetching assets
+///
+/// When fetching an image provided by the app itself, use the [assetName]
+/// argument to name the asset to choose. For instance, consider the structure
+/// above. First, the [pubspec.yaml] of the project should specify its assets in
+/// the `flutter` section:
+///
+/// ```yaml
+/// flutter:
+///   assets:
+///     - icons/heart.png
+/// ```
+///
+/// Then, to fetch the image, use
+/// ```dart
+/// new AssetImage('icons/heart.png')
+/// ```
+///
+/// ## Assets in packages
+///
+/// To fetch an asset from a package, the [package] argument must be provided.
+/// For instance, suppose the structure above is inside a package called
+/// `my_icons`. Then to fetch the image, use:
+///
+/// ```dart
+/// new AssetImage('icons/heart.png', package: 'my_icons')
+/// ```
+///
+/// Assets used by the package itself should also be fetched using the [package]
+/// argument as above.
+///
+/// If the desired asset is specified in the [pubspec.yaml] of the package, it
+/// is bundled automatically with the app. In particular, assets used by the
+/// package itself must be specified in its [pubspec.yaml].
+///
+/// A package can also choose to have assets in its 'lib/' folder that are not
+/// specified in its [pubspec.yaml]. In this case for those images to be
+/// bundled, the app has to specify which ones to include. For instance a
+/// package named `fancy_backgrounds` could have:
+///
+/// ```
+/// lib/backgrounds/background1.png
+/// lib/backgrounds/background2.png
+/// lib/backgrounds/background3.png
+///```
+///
+/// To include, say the first image, the [pubspec.yaml] of the app should specify
+/// it in the `assets` section:
+///
+/// ```yaml
+///  assets:
+///    - packages/fancy_backgrounds/backgrounds/background1.png
+/// ```
+///
+/// Note that the `lib/` is implied, so it should not be included in the asset
+/// path.
+///
 class AssetImage extends AssetBundleImageProvider {
   /// Creates an object that fetches an image from an asset bundle.
   ///
-  /// The [name] argument must not be null. It should name the main asset from
-  /// the set of images to chose from.
-  const AssetImage(this.name, { this.bundle }) : assert(name != null);
-
-  /// The name of the main asset from the set of images to chose from. See the
+  /// The [assetName] argument must not be null. It should name the main asset
+  /// from the set of images to choose from. The [package] argument must be
+  /// non-null when fetching an asset that is included in package. See the
   /// documentation for the [AssetImage] class itself for details.
-  final String name;
+  const AssetImage(this.assetName, {
+    this.bundle,
+    this.package,
+  }) : assert(assetName != null);
+
+  /// The name of the main asset from the set of images to choose from. See the
+  /// documentation for the [AssetImage] class itself for details.
+  final String assetName;
+
+  /// The name used to generate the key to obtain the asset. For local assets
+  /// this is [assetName], and for assets from packages the [assetName] is
+  /// prefixed 'packages/<package_name>/'.
+  String get keyName => package == null ? assetName : 'packages/$package/$assetName';
 
   /// The bundle from which the image will be obtained.
   ///
@@ -73,9 +141,15 @@
   /// that is also null, the [rootBundle] is used.
   ///
   /// The image is obtained by calling [AssetBundle.load] on the given [bundle]
-  /// using the key given by [name].
+  /// using the key given by [keyName].
   final AssetBundle bundle;
 
+  /// The name of the package from which the image is included. See the
+  /// documentation for the [AssetImage] class itself for details.
+  final String package;
+
+
+
   // We assume the main asset is designed for a device pixel ratio of 1.0
   static const double _naturalResolution = 1.0;
 
@@ -93,9 +167,9 @@
     chosenBundle.loadStructuredData<Map<String, List<String>>>(_kAssetManifestFileName, _manifestParser).then<Null>(
       (Map<String, List<String>> manifest) {
         final String chosenName = _chooseVariant(
-          name,
+          keyName,
           configuration,
-          manifest == null ? null : manifest[name]
+          manifest == null ? null : manifest[keyName]
         );
         final double chosenScale = _parseScale(chosenName);
         final AssetBundleImageKey key = new AssetBundleImageKey(
@@ -185,13 +259,13 @@
     if (other.runtimeType != runtimeType)
       return false;
     final AssetImage typedOther = other;
-    return name == typedOther.name
+    return keyName == typedOther.keyName
         && bundle == typedOther.bundle;
   }
 
   @override
-  int get hashCode => hashValues(name, bundle);
+  int get hashCode => hashValues(keyName, bundle);
 
   @override
-  String toString() => '$runtimeType(bundle: $bundle, name: "$name")';
+  String toString() => '$runtimeType(bundle: $bundle, name: "$keyName")';
 }
diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart
index c531e20..d8c5f97 100644
--- a/packages/flutter/lib/src/widgets/image.dart
+++ b/packages/flutter/lib/src/widgets/image.dart
@@ -113,7 +113,8 @@
     this.alignment,
     this.repeat: ImageRepeat.noRepeat,
     this.centerSlice,
-    this.gaplessPlayback: false
+    this.gaplessPlayback: false,
+    this.package,
   }) : assert(image != null),
        super(key: key);
 
@@ -131,7 +132,8 @@
     this.alignment,
     this.repeat: ImageRepeat.noRepeat,
     this.centerSlice,
-    this.gaplessPlayback: false
+    this.gaplessPlayback: false,
+    this.package,
   }) : image = new NetworkImage(src, scale: scale),
        super(key: key);
 
@@ -152,13 +154,18 @@
     this.alignment,
     this.repeat: ImageRepeat.noRepeat,
     this.centerSlice,
-    this.gaplessPlayback: false
+    this.gaplessPlayback: false,
+    this.package,
   }) : image = new FileImage(file, scale: scale),
        super(key: key);
 
   /// Creates a widget that displays an [ImageStream] obtained from an asset
   /// bundle. The key for the image is given by the `name` argument.
   ///
+  /// The `package` argument must be non-null when displaying an image from a
+  /// package and null otherwise. See the `Assets in packages` section for
+  /// details.
+  ///
   /// If the `bundle` argument is omitted or null, then the
   /// [DefaultAssetBundle] will be used.
   ///
@@ -210,6 +217,49 @@
   /// be present in the manifest). If it is omitted, then on a device with a 1.0
   /// device pixel ratio, the `images/2x/cat.png` image would be used instead.
   ///
+  ///
+  /// ## Assets in packages
+  ///
+  /// To create the widget with an asset from a package, the [package] argument
+  /// must be provided. For instance, suppose a package called `my_icons` has
+  /// `icons/heart.png` .
+  ///
+  /// Then to display the image, use:
+  ///
+  /// ```dart
+  /// new Image.asset('icons/heart.png', package: 'my_icons')
+  /// ```
+  ///
+  /// Assets used by the package itself should also be displayed using the
+  /// [package] argument as above.
+  ///
+  /// If the desired asset is specified in the [pubspec.yaml] of the package, it
+  /// is bundled automatically with the app. In particular, assets used by the
+  /// package itself must be specified in its [pubspec.yaml].
+  ///
+  /// A package can also choose to have assets in its 'lib/' folder that are not
+  /// specified in its [pubspec.yaml]. In this case for those images to be
+  /// bundled, the app has to specify which ones to include. For instance a
+  /// package named `fancy_backgrounds` could have:
+  ///
+  /// ```
+  /// lib/backgrounds/background1.png
+  /// lib/backgrounds/background2.png
+  /// lib/backgrounds/background3.png
+  ///```
+  ///
+  /// To include, say the first image, the [pubspec.yaml] of the app should
+  /// specify it in the assets section:
+  ///
+  /// ```yaml
+  ///  assets:
+  ///    - packages/fancy_backgrounds/backgrounds/background1.png
+  /// ```
+  ///
+  /// Note that the `lib/` is implied, so it should not be included in the asset
+  /// path.
+  ///
+  ///
   /// See also:
   ///
   ///  * [AssetImage], which is used to implement the behavior when the scale is
@@ -230,10 +280,12 @@
     this.alignment,
     this.repeat: ImageRepeat.noRepeat,
     this.centerSlice,
-    this.gaplessPlayback: false
-  }) : image = scale != null ? new ExactAssetImage(name, bundle: bundle, scale: scale)
-                             : new AssetImage(name, bundle: bundle),
-       super(key: key);
+    this.gaplessPlayback: false,
+    this.package,
+  }) : image = scale != null
+      ? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
+      : new AssetImage(name, bundle: bundle, package: package),
+        super(key: key);
 
   /// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
   ///
@@ -249,7 +301,8 @@
     this.alignment,
     this.repeat: ImageRepeat.noRepeat,
     this.centerSlice,
-    this.gaplessPlayback: false
+    this.gaplessPlayback: false,
+    this.package,
   }) : image = new MemoryImage(bytes, scale: scale),
        super(key: key);
 
@@ -310,6 +363,10 @@
   /// (false), when the image provider changes.
   final bool gaplessPlayback;
 
+  /// The name of the package from which the image is included. See the
+  /// documentation for the [Image.asset] constructor for details.
+  final String package;
+
   @override
   _ImageState createState() => new _ImageState();
 
diff --git a/packages/flutter/test/widgets/image_package_asset_test.dart b/packages/flutter/test/widgets/image_package_asset_test.dart
new file mode 100644
index 0000000..e27ba92
--- /dev/null
+++ b/packages/flutter/test/widgets/image_package_asset_test.dart
@@ -0,0 +1,47 @@
+// Copyright 2017 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 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  test('AssetImage from package', () {
+    final AssetImage image = const AssetImage(
+      'assets/image.png',
+      package: 'test_package',
+    );
+    expect(image.keyName, 'packages/test_package/assets/image.png');
+  });
+
+  test('ExactAssetImage from package', () {
+    final ExactAssetImage image = const ExactAssetImage(
+      'assets/image.png',
+      scale: 1.5,
+      package: 'test_package',
+    );
+    expect(image.keyName, 'packages/test_package/assets/image.png');
+  });
+
+  test('Image.asset from package', () {
+    final Image imageWidget = new Image.asset(
+      'assets/image.png',
+      package: 'test_package',
+    );
+    assert(imageWidget.image is AssetImage);
+    final AssetImage assetImage = imageWidget.image;
+    expect(assetImage.keyName, 'packages/test_package/assets/image.png');
+  });
+
+  test('Image.asset from package', () {
+    final Image imageWidget = new Image.asset(
+      'assets/image.png',
+      scale: 1.5,
+      package: 'test_package',
+    );
+    assert(imageWidget.image is ExactAssetImage);
+    final ExactAssetImage asssetImage = imageWidget.image;
+    expect(asssetImage.keyName, 'packages/test_package/assets/image.png');
+  });
+}
diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart
index aef0882..24f94d9 100644
--- a/packages/flutter_tools/lib/src/asset.dart
+++ b/packages/flutter_tools/lib/src/asset.dart
@@ -86,11 +86,12 @@
       return 0;
     }
     if (manifest != null) {
-     final int result = await _validateFlutterManifest(manifest);
-     if (result != 0)
-       return result;
+      final int result = await _validateFlutterManifest(manifest);
+      if (result != 0)
+        return result;
     }
     Map<String, dynamic> manifestDescriptor = manifest;
+    final String appName = manifestDescriptor['name'];
     manifestDescriptor = manifestDescriptor['flutter'] ?? <String, dynamic>{};
     final String assetBasePath = fs.path.dirname(fs.path.absolute(manifestPath));
 
@@ -116,6 +117,33 @@
         manifestDescriptor.containsKey('uses-material-design') &&
         manifestDescriptor['uses-material-design'];
 
+    // Add assets from packages.
+    for (String packageName in packageMap.map.keys) {
+      final Uri package = packageMap.map[packageName];
+      if (package != null && package.scheme == 'file') {
+        final String packageManifestPath = package.resolve('../pubspec.yaml').path;
+        final Object packageManifest = _loadFlutterManifest(packageManifestPath);
+        if (packageManifest == null)
+          continue;
+        final int result = await _validateFlutterManifest(packageManifest);
+        if (result == 0) {
+          final Map<String, dynamic> packageManifestDescriptor = packageManifest;
+          // Skip the app itself.
+          if (packageManifestDescriptor['name'] == appName)
+            continue;
+          if (packageManifestDescriptor.containsKey('flutter')) {
+            final String packageBasePath = fs.path.dirname(packageManifestPath);
+            assetVariants.addAll(_parseAssets(
+              packageMap,
+              packageManifestDescriptor['flutter'],
+              packageBasePath,
+              packageKey: packageName,
+            ));
+          }
+        }
+      }
+    }
+
     // Save the contents of each image, image variant, and font
     // asset in entries.
     for (_Asset asset in assetVariants.keys) {
@@ -203,6 +231,27 @@
 
   @override
   String toString() => 'asset: $assetEntry';
+
+  @override
+  bool operator ==(dynamic other) {
+    if (identical(other, this))
+      return true;
+    if (other.runtimeType != runtimeType)
+      return false;
+    final _Asset otherAsset = other;
+    return otherAsset.base == base
+        && otherAsset.assetEntry == assetEntry
+        && otherAsset.relativePath == relativePath
+        && otherAsset.source == source;
+  }
+
+  @override
+  int get hashCode {
+    return base.hashCode
+        ^assetEntry.hashCode
+        ^relativePath.hashCode
+        ^ source.hashCode;
+  }
 }
 
 Map<String, dynamic> _readMaterialFontsManifest() {
@@ -312,8 +361,8 @@
   for (_Asset main in assetVariants.keys) {
     final List<String> variants = <String>[];
     for (_Asset variant in assetVariants[main])
-      variants.add(variant.relativePath);
-    json[main.relativePath] = variants;
+      variants.add(variant.assetEntry);
+    json[main.assetEntry] = variants;
   }
   return new DevFSStringContent(JSON.encode(json));
 }
@@ -384,7 +433,8 @@
   PackageMap packageMap,
   Map<String, dynamic> manifestDescriptor,
   String assetBase, {
-  List<String> excludeDirs: const <String>[]
+  List<String> excludeDirs: const <String>[],
+  String packageKey
 }) {
   final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
 
@@ -394,7 +444,9 @@
   if (manifestDescriptor.containsKey('assets')) {
     final _AssetDirectoryCache cache = new _AssetDirectoryCache(excludeDirs);
     for (String assetName in manifestDescriptor['assets']) {
-      final _Asset asset = _resolveAsset(packageMap, assetBase, assetName);
+      final _Asset asset = packageKey != null
+          ? _resolvePackageAsset(assetBase, packageKey, assetName)
+          : _resolveAsset(packageMap, assetBase, assetName);
       final List<_Asset> variants = <_Asset>[];
 
       for (String path in cache.variantsFor(asset.assetFile.path)) {
@@ -435,10 +487,22 @@
   return result;
 }
 
+_Asset _resolvePackageAsset(
+    String assetBase,
+    String packageName,
+    String asset,
+) {
+  return new _Asset(
+    base: assetBase,
+    assetEntry: 'packages/$packageName/$asset',
+    relativePath: asset,
+  );
+}
+
 _Asset _resolveAsset(
   PackageMap packageMap,
   String assetBase,
-  String asset
+  String asset,
 ) {
   if (asset.startsWith('packages/') && !fs.isFileSync(fs.path.join(assetBase, asset))) {
     // Convert packages/flutter_gallery_assets/clouds-0.png to clouds-0.png.
diff --git a/packages/flutter_tools/test/asset_bundle_package_test.dart b/packages/flutter_tools/test/asset_bundle_package_test.dart
new file mode 100644
index 0000000..a072610
--- /dev/null
+++ b/packages/flutter_tools/test/asset_bundle_package_test.dart
@@ -0,0 +1,225 @@
+// Copyright 2017 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 'dart:convert';
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+
+import 'package:flutter_tools/src/asset.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+
+import 'package:test/test.dart';
+
+import 'src/common.dart';
+import 'src/context.dart';
+
+void main() {
+  void writePubspecFile(String path, String name, {List<String> assets}) {
+    String assetsSection;
+    if (assets == null) {
+      assetsSection = '';
+    } else {
+      final StringBuffer buffer = new StringBuffer();
+      buffer.write('''
+flutter:
+     assets:
+''');
+
+      for (String asset in assets) {
+        buffer.write('''
+       - $asset
+''');
+      }
+      assetsSection = buffer.toString();
+    }
+
+    fs.file(path)
+      ..createSync(recursive: true)
+      ..writeAsStringSync('''
+name: $name
+dependencies:
+  flutter:
+    sdk: flutter
+$assetsSection
+''');
+  }
+
+  void establishFlutterRoot() {
+    // Setting flutterRoot here so that it picks up the MemoryFileSystem's
+    // path separator.
+    Cache.flutterRoot = getFlutterRoot();
+  }
+
+  void writePackagesFile(String packages) {
+    fs.file(".packages")
+      ..createSync()
+      ..writeAsStringSync(packages);
+  }
+
+  Future<Null> buildAndVerifyAssets(
+    List<String> assets,
+    List<String> packages,
+    String expectedAssetManifest,
+  ) async {
+    final AssetBundle bundle = new AssetBundle();
+    await bundle.build(manifestPath: 'pubspec.yaml');
+
+    for (String packageName in packages) {
+      for (String asset in assets) {
+        final String entryKey = 'packages/$packageName/$asset';
+        expect(bundle.entries.containsKey(entryKey), true);
+        expect(
+          UTF8.decode(await bundle.entries[entryKey].contentsAsBytes()),
+          asset,
+        );
+      }
+    }
+
+    expect(
+      UTF8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
+      expectedAssetManifest,
+    );
+  }
+
+  void writeAssets(String path, List<String> assets) {
+    for (String asset in assets) {
+      fs.file('$path$asset')
+        ..createSync(recursive: true)
+        ..writeAsStringSync(asset);
+    }
+  }
+
+  group('AssetBundle assets from package', () {
+    testUsingContext('One package with no assets', () async {
+      establishFlutterRoot();
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile('p/p/pubspec.yaml', 'test_package');
+
+      final AssetBundle bundle = new AssetBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      expect(bundle.entries.length, 2); // LICENSE, AssetManifest
+    }, overrides: <Type, Generator>{
+      FileSystem: () => new MemoryFileSystem(),
+    });
+
+    testUsingContext('One package with one asset', () async {
+      establishFlutterRoot();
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assets = <String>['a/foo'];
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assets,
+      );
+
+      writeAssets('p/p/', assets);
+
+      final String expectedAssetManifest = '{"packages/test_package/a/foo":'
+          '["packages/test_package/a/foo"]}';
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => new MemoryFileSystem(),
+    });
+
+    testUsingContext('One package with asset variants', () async {
+      establishFlutterRoot();
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: <String>['a/foo'],
+      );
+
+      final List<String> assets = <String>['a/foo', 'a/v/foo'];
+      writeAssets('p/p/', assets);
+
+      final String expectedManifest = '{"packages/test_package/a/foo":'
+          '["packages/test_package/a/foo","packages/test_package/a/v/foo"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package'],
+        expectedManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => new MemoryFileSystem(),
+    });
+
+    testUsingContext('One package with two assets', () async {
+      establishFlutterRoot();
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assets = <String>['a/foo', 'a/bar'];
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assets,
+      );
+
+      writeAssets('p/p/', assets);
+      final String expectedAssetManifest =
+          '{"packages/test_package/a/foo":["packages/test_package/a/foo"],'
+          '"packages/test_package/a/bar":["packages/test_package/a/bar"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => new MemoryFileSystem(),
+    });
+
+    testUsingContext('Two packages with assets', () async {
+      establishFlutterRoot();
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: <String>['a/foo'],
+      );
+      writePubspecFile(
+        'p2/p/pubspec.yaml',
+        'test_package2',
+        assets: <String>['a/foo'],
+      );
+
+      final List<String> assets = <String>['a/foo', 'a/v/foo'];
+      writeAssets('p/p/', assets);
+      writeAssets('p2/p/', assets);
+
+      final String expectedAssetManifest =
+          '{"packages/test_package/a/foo":'
+          '["packages/test_package/a/foo","packages/test_package/a/v/foo"],'
+          '"packages/test_package2/a/foo":'
+          '["packages/test_package2/a/foo","packages/test_package2/a/v/foo"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package', 'test_package2'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => new MemoryFileSystem(),
+    });
+  });
+}