From 02c10b78d23613bab5698ac3315a605a35535ffe Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Tue, 5 Sep 2017 09:42:24 +0200 Subject: [PATCH] Bundle assets used in packages (#11751) --- .../lib/src/services/image_provider.dart | 93 +++++++- .../lib/src/services/image_resolution.dart | 98 +++++++- packages/flutter/lib/src/widgets/image.dart | 73 +++++- .../widgets/image_package_asset_test.dart | 47 ++++ packages/flutter_tools/lib/src/asset.dart | 80 ++++++- .../test/asset_bundle_package_test.dart | 225 ++++++++++++++++++ 6 files changed, 577 insertions(+), 39 deletions(-) create mode 100644 packages/flutter/test/widgets/image_package_asset_test.dart create mode 100644 packages/flutter_tools/test/asset_bundle_package_test.dart diff --git a/packages/flutter/lib/src/services/image_provider.dart b/packages/flutter/lib/src/services/image_provider.dart index ac48364c8d7..c29b52a1b74 100644 --- a/packages/flutter/lib/src/services/image_provider.dart +++ b/packages/flutter/lib/src/services/image_provider.dart @@ -589,27 +589,94 @@ class MemoryImage extends ImageProvider { /// 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 @@ class ExactAssetImage extends AssetBundleImageProvider { /// 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 obtainKey(ImageConfiguration configuration) { return new SynchronousFuture(new AssetBundleImageKey( bundle: bundle ?? configuration.bundle ?? rootBundle, - name: name, + name: keyName, scale: scale )); } @@ -638,14 +709,14 @@ class ExactAssetImage extends AssetBundleImageProvider { 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 78764c3eccd..0f6b1e8e1d5 100644 --- a/packages/flutter/lib/src/services/image_resolution.dart +++ b/packages/flutter/lib/src/services/image_resolution.dart @@ -55,16 +55,84 @@ const String _kAssetManifestFileName = 'AssetManifest.json'; /// 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//'. + String get keyName => package == null ? assetName : 'packages/$package/$assetName'; /// The bundle from which the image will be obtained. /// @@ -73,9 +141,15 @@ class AssetImage extends AssetBundleImageProvider { /// 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 @@ class AssetImage extends AssetBundleImageProvider { chosenBundle.loadStructuredData>>(_kAssetManifestFileName, _manifestParser).then( (Map> 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 @@ class AssetImage extends AssetBundleImageProvider { 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 c531e2037b6..d8c5f97d42d 100644 --- a/packages/flutter/lib/src/widgets/image.dart +++ b/packages/flutter/lib/src/widgets/image.dart @@ -113,7 +113,8 @@ class Image extends StatefulWidget { 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 @@ class Image extends StatefulWidget { 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 @@ class Image extends StatefulWidget { 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 @@ class Image extends StatefulWidget { /// 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 @@ class Image extends StatefulWidget { 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 @@ class Image extends StatefulWidget { 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 @@ class Image extends StatefulWidget { /// (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 00000000000..e27ba92cbac --- /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 aef088249b9..24f94d9eaa9 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -86,11 +86,12 @@ class AssetBundle { 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 manifestDescriptor = manifest; + final String appName = manifestDescriptor['name']; manifestDescriptor = manifestDescriptor['flutter'] ?? {}; final String assetBasePath = fs.path.dirname(fs.path.absolute(manifestPath)); @@ -116,6 +117,33 @@ class AssetBundle { 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 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 @@ class _Asset { @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 _readMaterialFontsManifest() { @@ -312,8 +361,8 @@ DevFSContent _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) { for (_Asset main in assetVariants.keys) { final List variants = []; 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 @@ Map<_Asset, List<_Asset>> _parseAssets( PackageMap packageMap, Map manifestDescriptor, String assetBase, { - List excludeDirs: const [] + List excludeDirs: const [], + String packageKey }) { final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{}; @@ -394,7 +444,9 @@ Map<_Asset, List<_Asset>> _parseAssets( 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 @@ Map<_Asset, List<_Asset>> _parseAssets( 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 00000000000..a072610e1f3 --- /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 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 buildAndVerifyAssets( + List assets, + List 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 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: { + FileSystem: () => new MemoryFileSystem(), + }); + + testUsingContext('One package with one asset', () async { + establishFlutterRoot(); + + writePubspecFile('pubspec.yaml', 'test'); + writePackagesFile('test_package:p/p/lib/'); + + final List assets = ['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, + ['test_package'], + expectedAssetManifest, + ); + }, overrides: { + 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: ['a/foo'], + ); + + final List assets = ['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, + ['test_package'], + expectedManifest, + ); + }, overrides: { + FileSystem: () => new MemoryFileSystem(), + }); + + testUsingContext('One package with two assets', () async { + establishFlutterRoot(); + + writePubspecFile('pubspec.yaml', 'test'); + writePackagesFile('test_package:p/p/lib/'); + + final List assets = ['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, + ['test_package'], + expectedAssetManifest, + ); + }, overrides: { + 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: ['a/foo'], + ); + writePubspecFile( + 'p2/p/pubspec.yaml', + 'test_package2', + assets: ['a/foo'], + ); + + final List assets = ['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, + ['test_package', 'test_package2'], + expectedAssetManifest, + ); + }, overrides: { + FileSystem: () => new MemoryFileSystem(), + }); + }); +}