Unverified Commit 759ebef6 authored by Andrew Kolos's avatar Andrew Kolos Committed by GitHub

Do not try to load main/default asset image if only higher-res variants exist (#128143)

Fixes https://github.com/flutter/flutter/issues/127090.

https://github.com/flutter/flutter/pull/122505 did a few things to speed up the first asset load that a flutter app performs. One of those things was to not include the main asset in its own list of variants in the asset manifest. The idea was that we know that the main asset always exists, so including it in its list of variants is a waste of storage space and loading time (even if the cost was tiny).

However, the assumption that the main asset always exists is wrong. From [Declaring resolution-aware image assets](https://docs.flutter.dev/ui/assets-and-images#resolution-aware), which predates https://github.com/flutter/flutter/pull/122505:

> Each entry in the asset section of the pubspec.yaml should correspond to a real file, with the exception of the main asset entry. If the main asset entry doesn’t correspond to a real file, then the asset with the lowest resolution is used as the fallback for devices with device pixel ratios below that resolution. The entry should still be included in the pubspec.yaml manifest, however.

For example, it's valid to declare `assets/image.png` as an asset even if only `assets/3x/image.png` exists on disk.

This fix restores older behavior of including a main asset as a variant of itself in the manifest if it exists.

This fix also includes a non-user-visible behavior change:
* `"dpr"` is no longer a required field in the asset manifest's underlying structure. For the main asset entry, we do not include `"dpr"`. It makes less sense for the tool to decide what the default target dpr for an image should be. This should be left to the framework.
parent 19f85935
...@@ -327,14 +327,10 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -327,14 +327,10 @@ class AssetImage extends AssetBundleImageProvider {
} }
AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata>? candidateVariants) { AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata>? candidateVariants) {
if (candidateVariants == null) { if (candidateVariants == null || candidateVariants.isEmpty || config.devicePixelRatio == null) {
return AssetMetadata(key: mainAssetKey, targetDevicePixelRatio: null, main: true); return AssetMetadata(key: mainAssetKey, targetDevicePixelRatio: null, main: true);
} }
if (config.devicePixelRatio == null) {
return candidateVariants.firstWhere((AssetMetadata variant) => variant.main);
}
final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio = final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio =
SplayTreeMap<double, AssetMetadata>(); SplayTreeMap<double, AssetMetadata>();
for (final AssetMetadata candidate in candidateVariants) { for (final AssetMetadata candidate in candidateVariants) {
......
...@@ -33,8 +33,8 @@ abstract class AssetManifest { ...@@ -33,8 +33,8 @@ abstract class AssetManifest {
/// Retrieves metadata about an asset and its variants. Returns null if the /// Retrieves metadata about an asset and its variants. Returns null if the
/// key was not found in the asset manifest. /// key was not found in the asset manifest.
/// ///
/// This method considers a main asset to be a variant of itself and /// This method considers a main asset to be a variant of itself. The returned
/// includes it in the returned list. /// list will include it if it exists.
List<AssetMetadata>? getAssetVariants(String key); List<AssetMetadata>? getAssetVariants(String key);
} }
...@@ -73,22 +73,21 @@ class _AssetManifestBin implements AssetManifest { ...@@ -73,22 +73,21 @@ class _AssetManifestBin implements AssetManifest {
} }
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>) _typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
.cast<Map<Object?, Object?>>() .cast<Map<Object?, Object?>>()
.map((Map<Object?, Object?> data) => AssetMetadata( .map((Map<Object?, Object?> data) {
final String asset = data['asset']! as String;
final Object? dpr = data['dpr'];
return AssetMetadata(
key: data['asset']! as String, key: data['asset']! as String,
targetDevicePixelRatio: data['dpr']! as double, targetDevicePixelRatio: dpr as double?,
main: false, main: key == asset,
)) );
})
.toList(); .toList();
_data.remove(key); _data.remove(key);
} }
final AssetMetadata mainAsset = AssetMetadata(key: key, return _typeCastedData[key]!;
targetDevicePixelRatio: null,
main: true
);
return <AssetMetadata>[mainAsset, ..._typeCastedData[key]!];
} }
@override @override
......
...@@ -43,8 +43,6 @@ void main() { ...@@ -43,8 +43,6 @@ void main() {
final Map<String, List<Map<Object?, Object?>>> assetBundleMap = final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
<String, List<Map<Object?, Object?>>>{}; <String, List<Map<Object?, Object?>>>{};
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
final AssetImage assetImage = AssetImage( final AssetImage assetImage = AssetImage(
mainAssetPath, mainAssetPath,
bundle: TestAssetBundle(assetBundleMap), bundle: TestAssetBundle(assetBundleMap),
...@@ -160,15 +158,17 @@ void main() { ...@@ -160,15 +158,17 @@ void main() {
double chosenAssetRatio, double chosenAssetRatio,
String expectedAssetPath, String expectedAssetPath,
) { ) {
final Map<String, List<Map<Object?, Object?>>> assetBundleMap = const Map<String, List<Map<Object?, Object?>>> assetManifest =
<String, List<Map<Object?, Object?>>>{}; <String, List<Map<Object?, Object?>>>{
'assets/normalFolder/normalFile.png': <Map<Object?, Object?>>[
final Map<Object?, Object?> mainAssetVariantManifestEntry = <Object?, Object?>{}; <Object?, Object?>{'asset': 'assets/normalFolder/normalFile.png'},
mainAssetVariantManifestEntry['asset'] = variantPath; <Object?, Object?>{
mainAssetVariantManifestEntry['dpr'] = 3.0; 'asset': 'assets/normalFolder/3.0x/normalFile.png',
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[mainAssetVariantManifestEntry]; 'dpr': 3.0
},
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); ]
};
final TestAssetBundle testAssetBundle = TestAssetBundle(assetManifest);
final AssetImage assetImage = AssetImage( final AssetImage assetImage = AssetImage(
mainAssetPath, mainAssetPath,
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -11,12 +13,19 @@ class TestAssetBundle extends AssetBundle { ...@@ -11,12 +13,19 @@ class TestAssetBundle extends AssetBundle {
if (key == 'AssetManifest.smcbin') { if (key == 'AssetManifest.smcbin') {
final Map<String, List<Object>> binManifestData = <String, List<Object>>{ final Map<String, List<Object>> binManifestData = <String, List<Object>>{
'assets/foo.png': <Object>[ 'assets/foo.png': <Object>[
<String, Object>{
'asset': 'assets/foo.png',
},
<String, Object>{ <String, Object>{
'asset': 'assets/2x/foo.png', 'asset': 'assets/2x/foo.png',
'dpr': 2.0 'dpr': 2.0
} },
],
'assets/bar.png': <Object>[
<String, Object>{
'asset': 'assets/bar.png',
},
], ],
'assets/bar.png': <Object>[],
}; };
final ByteData data = const StandardMessageCodec().encodeMessage(binManifestData)!; final ByteData data = const StandardMessageCodec().encodeMessage(binManifestData)!;
...@@ -32,7 +41,6 @@ class TestAssetBundle extends AssetBundle { ...@@ -32,7 +41,6 @@ class TestAssetBundle extends AssetBundle {
} }
} }
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
...@@ -65,3 +73,29 @@ void main() { ...@@ -65,3 +73,29 @@ void main() {
expect(manifest.getAssetVariants('invalid asset key'), isNull); expect(manifest.getAssetVariants('invalid asset key'), isNull);
}); });
} }
String createAssetManifestJson(Map<String, List<AssetMetadata>> manifest) {
final Map<Object, Object> jsonObject = manifest.map(
(String key, List<AssetMetadata> value) {
final List<String> variants = value.map((AssetMetadata e) => e.key).toList();
return MapEntry<String, List<String>>(key, variants);
}
);
return json.encode(jsonObject);
}
ByteData createAssetManifestSmcBin(Map<String, List<AssetMetadata>> manifest) {
final Map<Object, Object> smcObject = manifest.map(
(String key, List<AssetMetadata> value) {
final List<Object> variants = value.map((AssetMetadata variant) => <String, Object?>{
'asset': variant.key,
'dpr': variant.targetDevicePixelRatio,
}).toList();
return MapEntry<String, List<Object>>(key, variants);
}
);
return const StandardMessageCodec().encodeMessage(smcObject)!;
}
...@@ -19,16 +19,15 @@ import '../image_data.dart'; ...@@ -19,16 +19,15 @@ import '../image_data.dart';
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale); ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
double scaleOf(ByteData data) => data.getFloat64(0); double scaleOf(ByteData data) => data.getFloat64(0);
final Map<Object?, Object?> testManifest = json.decode(''' final Map<Object?, Object?> testManifest = <Object?, Object?>{
{ 'assets/image.png' : <Map<String, Object>>[
"assets/image.png" : [ <String, String>{'asset': 'assets/image.png'},
{"asset": "assets/1.5x/image.png", "dpr": 1.5}, <String, Object>{'asset': 'assets/1.5x/image.png', 'dpr': 1.5},
{"asset": "assets/2.0x/image.png", "dpr": 2.0}, <String, Object>{'asset': 'assets/2.0x/image.png', 'dpr': 2.0},
{"asset": "assets/3.0x/image.png", "dpr": 3.0}, <String, Object>{'asset': 'assets/3.0x/image.png', 'dpr': 3.0},
{"asset": "assets/4.0x/image.png", "dpr": 4.0} <String, Object>{'asset': 'assets/4.0x/image.png', 'dpr': 4.0}
] ],
} };
''') as Map<Object?, Object?>;
class TestAssetBundle extends CachingAssetBundle { class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle({ required Map<Object?, Object?> manifest }) { TestAssetBundle({ required Map<Object?, Object?> manifest }) {
...@@ -303,13 +302,12 @@ void main() { ...@@ -303,13 +302,12 @@ void main() {
// if higher resolution assets are not available we will pick the best // if higher resolution assets are not available we will pick the best
// available. // available.
testWidgets('Low-resolution assets', (WidgetTester tester) async { testWidgets('Low-resolution assets', (WidgetTester tester) async {
final Map<Object?, Object?> manifest = json.decode(''' const Map<Object?, Object?> manifest = <Object?, Object?>{
{ 'assets/image.png': <Map<String, Object>>[
"assets/image.png" : [ <String, Object>{'asset': 'assets/image.png'},
{"asset": "assets/1.5x/image.png", "dpr": 1.5} <String, Object>{'asset': 'assets/1.5x/image.png', 'dpr': 1.5},
] ],
} };
''') as Map<Object?, Object?>;
final AssetBundle bundle = TestAssetBundle(manifest: manifest); final AssetBundle bundle = TestAssetBundle(manifest: manifest);
Future<void> testRatio({required double ratio, required double expectedScale}) async { Future<void> testRatio({required double ratio, required double expectedScale}) async {
......
...@@ -166,7 +166,6 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -166,7 +166,6 @@ class ManifestAssetBundle implements AssetBundle {
DateTime? _lastBuildTimestamp; DateTime? _lastBuildTimestamp;
// We assume the main asset is designed for a device pixel ratio of 1.0. // We assume the main asset is designed for a device pixel ratio of 1.0.
static const double _defaultResolution = 1.0;
static const String _kAssetManifestJsonFilename = 'AssetManifest.json'; static const String _kAssetManifestJsonFilename = 'AssetManifest.json';
static const String _kAssetManifestBinFilename = 'AssetManifest.smcbin'; static const String _kAssetManifestBinFilename = 'AssetManifest.smcbin';
...@@ -688,7 +687,7 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -688,7 +687,7 @@ class ManifestAssetBundle implements AssetBundle {
DevFSByteContent _createAssetManifestBinary( DevFSByteContent _createAssetManifestBinary(
Map<String, List<String>> assetManifest Map<String, List<String>> assetManifest
) { ) {
double parseScale(String key) { double? parseScale(String key) {
final Uri assetUri = Uri.parse(key); final Uri assetUri = Uri.parse(key);
String directoryPath = ''; String directoryPath = '';
if (assetUri.pathSegments.length > 1) { if (assetUri.pathSegments.length > 1) {
...@@ -699,7 +698,8 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -699,7 +698,8 @@ class ManifestAssetBundle implements AssetBundle {
if (match != null && match.groupCount > 0) { if (match != null && match.groupCount > 0) {
return double.parse(match.group(1)!); return double.parse(match.group(1)!);
} }
return _defaultResolution;
return null;
} }
final Map<String, dynamic> result = <String, dynamic>{}; final Map<String, dynamic> result = <String, dynamic>{};
...@@ -708,15 +708,12 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -708,15 +708,12 @@ class ManifestAssetBundle implements AssetBundle {
final List<dynamic> resultVariants = <dynamic>[]; final List<dynamic> resultVariants = <dynamic>[];
final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>(); final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>();
for (final String variant in entries) { for (final String variant in entries) {
if (variant == manifestEntry.key) {
// With the newer binary format, don't include the main asset in it's
// list of variants. This reduces parsing time at runtime.
continue;
}
final Map<String, dynamic> resultVariant = <String, dynamic>{}; final Map<String, dynamic> resultVariant = <String, dynamic>{};
final double variantDevicePixelRatio = parseScale(variant); final double? variantDevicePixelRatio = parseScale(variant);
resultVariant['asset'] = variant; resultVariant['asset'] = variant;
resultVariant['dpr'] = variantDevicePixelRatio; if (variantDevicePixelRatio != null) {
resultVariant['dpr'] = variantDevicePixelRatio;
}
resultVariants.add(resultVariant); resultVariants.add(resultVariant);
} }
result[manifestEntry.key] = resultVariants; result[manifestEntry.key] = resultVariants;
......
...@@ -26,6 +26,7 @@ void main() { ...@@ -26,6 +26,7 @@ void main() {
// rolls into Flutter. // rolls into Flutter.
return path.replaceAll('/', globals.fs.path.separator); return path.replaceAll('/', globals.fs.path.separator);
} }
void writePubspecFile(String path, String name, { List<String>? assets }) { void writePubspecFile(String path, String name, { List<String>? assets }) {
String assetsSection; String assetsSection;
if (assets == null) { if (assets == null) {
...@@ -62,19 +63,19 @@ $assetsSection ...@@ -62,19 +63,19 @@ $assetsSection
..writeAsStringSync(packages); ..writeAsStringSync(packages);
} }
Map<Object, Object> assetManifestBinToJson(Map<Object, Object> manifest) {
List<Object> convertList(List<Object> variants) => variants
.map((Object variant) => (variant as Map<Object?, Object?>)['asset']!)
.toList();
return manifest.map((Object key, Object value) => MapEntry<Object, Object>(key, convertList(value as List<Object>)));
}
Future<void> buildAndVerifyAssets( Future<void> buildAndVerifyAssets(
List<String> assets, List<String> assets,
List<String> packages, List<String> packages,
String? expectedJsonAssetManifest, Map<Object,Object> expectedAssetManifest
String? expectedBinAssetManifestAsJson, { ) async {
bool expectExists = true,
}) async {
Future<String> extractAssetManifestBinFromBundleAsJson(AssetBundle bundle) async {
final List<int> manifestBytes = await bundle.entries['AssetManifest.smcbin']!.contentsAsBytes();
return json.encode(const StandardMessageCodec().decodeMessage(
ByteData.sublistView(Uint8List.fromList(manifestBytes))
));
}
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
...@@ -82,27 +83,31 @@ $assetsSection ...@@ -82,27 +83,31 @@ $assetsSection
for (final String packageName in packages) { for (final String packageName in packages) {
for (final String asset in assets) { for (final String asset in assets) {
final String entryKey = Uri.encodeFull('packages/$packageName/$asset'); final String entryKey = Uri.encodeFull('packages/$packageName/$asset');
expect(bundle.entries.containsKey(entryKey), expectExists, expect(bundle.entries, contains(entryKey),
reason: 'Cannot find key on bundle: $entryKey'); reason: 'Cannot find key on bundle: $entryKey');
if (expectExists) { expect(
expect( utf8.decode(await bundle.entries[entryKey]!.contentsAsBytes()),
utf8.decode(await bundle.entries[entryKey]!.contentsAsBytes()), asset,
asset, );
);
}
} }
} }
if (expectExists) { final Map<Object?, Object?> assetManifest = const StandardMessageCodec().decodeMessage(
expect( ByteData.sublistView(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()), Uint8List.fromList(
expectedJsonAssetManifest, await bundle.entries['AssetManifest.smcbin']!.contentsAsBytes()
); )
expect( )
await extractAssetManifestBinFromBundleAsJson(bundle), ) as Map<Object?, Object?>;
expectedBinAssetManifestAsJson
); expect(
} json.decode(utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes())),
assetManifestBinToJson(expectedAssetManifest),
);
expect(
assetManifest,
expectedAssetManifest
);
} }
void writeAssets(String path, List<String> assets) { void writeAssets(String path, List<String> assets) {
...@@ -190,14 +195,18 @@ $assetsSection ...@@ -190,14 +195,18 @@ $assetsSection
writeAssets('p/p/', assets); writeAssets('p/p/', assets);
const String expectedJsonAssetManifest = '{"packages/test_package/a/foo":' final Map<Object, Object> expectedAssetManifest = <Object, Object>{
'["packages/test_package/a/foo"]}'; 'packages/test_package/a/foo': <Map<Object, Object>>[
const String expectedBinAssetManifest = '{"packages/test_package/a/foo":[]}'; <Object, Object>{
'asset': 'packages/test_package/a/foo',
}
]
};
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package'], <String>['test_package'],
expectedJsonAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -218,14 +227,16 @@ $assetsSection ...@@ -218,14 +227,16 @@ $assetsSection
final List<String> assets = <String>['a/foo']; final List<String> assets = <String>['a/foo'];
writeAssets('p/p/lib/', assets); writeAssets('p/p/lib/', assets);
const String expectedAssetManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo"]}'; const Map<Object, Object> expectedAssetManifest = <Object, Object>{
const String expectedBinAssetManifest = '{"packages/test_package/a/foo":[]}'; 'packages/test_package/a/foo': <Map<String, Object>>[
<String, Object>{'asset': 'packages/test_package/a/foo'}
]
};
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -245,25 +256,20 @@ $assetsSection ...@@ -245,25 +256,20 @@ $assetsSection
final List<String> assets = <String>['a/foo', 'a/2x/foo', 'a/bar']; final List<String> assets = <String>['a/foo', 'a/2x/foo', 'a/bar'];
writeAssets('p/p/', assets); writeAssets('p/p/', assets);
const String expectedManifest = '{' const Map<Object, Object> expectedManifest = <Object, Object>{
'"packages/test_package/a/bar":' 'packages/test_package/a/bar': <Map<String, Object>>[
'["packages/test_package/a/bar"],' <String, Object>{'asset': 'packages/test_package/a/bar'}
'"packages/test_package/a/foo":' ],
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"]' 'packages/test_package/a/foo': <Map<String, Object>>[
'}'; <String, Object>{'asset': 'packages/test_package/a/foo'},
<String, Object>{'asset': 'packages/test_package/a/2x/foo', 'dpr': 2.0}
const String expectedBinManifest = '{' ]
'"packages/test_package/a/bar":[],' };
'"packages/test_package/a/foo":'
'[{"asset":"packages/test_package/a/2x/foo","dpr":2.0}]'
'}';
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package'], <String>['test_package'],
expectedManifest, expectedManifest,
expectedBinManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -286,17 +292,17 @@ $assetsSection ...@@ -286,17 +292,17 @@ $assetsSection
final List<String> assets = <String>['a/foo', 'a/2x/foo']; final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p/p/lib/', assets); writeAssets('p/p/lib/', assets);
const String expectedManifest = '{"packages/test_package/a/foo":' const Map<Object, Object> expectedManifest = <Object, Object>{
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"]}'; 'packages/test_package/a/foo': <Map<String, Object>>[
<String, Object>{'asset': 'packages/test_package/a/foo'},
const String expectedBinManifest = '{"packages/test_package/a/foo":' <String, Object>{'asset': 'packages/test_package/a/2x/foo', 'dpr': 2.0}
'[{"asset":"packages/test_package/a/2x/foo","dpr":2.0}]}'; ]
};
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package'], <String>['test_package'],
expectedManifest, expectedManifest,
expectedBinManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -316,19 +322,19 @@ $assetsSection ...@@ -316,19 +322,19 @@ $assetsSection
); );
writeAssets('p/p/', assets); writeAssets('p/p/', assets);
const String expectedAssetManifest = const Map<Object, Object> expectedAssetManifest = <Object, Object>{
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],' 'packages/test_package/a/bar': <Map<String, Object>>[
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}'; <String, Object>{'asset': 'packages/test_package/a/bar'}
const String expectedBinAssetManifest = ],
'{"packages/test_package/a/bar":[],' 'packages/test_package/a/foo': <Map<String, Object>>[
'"packages/test_package/a/foo":[]}'; <String, Object>{'asset': 'packages/test_package/a/foo'}
]
};
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -354,18 +360,19 @@ $assetsSection ...@@ -354,18 +360,19 @@ $assetsSection
); );
writeAssets('p/p/lib/', assets); writeAssets('p/p/lib/', assets);
const String expectedAssetManifest = const Map<Object, Object> expectedAssetManifest = <Object, Object>{
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],' 'packages/test_package/a/bar': <Map<String, Object>>[
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}'; <String, Object>{'asset': 'packages/test_package/a/bar'}
const String expectedBinAssetManifest = ],
'{"packages/test_package/a/bar":[],' 'packages/test_package/a/foo': <Map<String, Object>>[
'"packages/test_package/a/foo":[]}'; <String, Object>{'asset': 'packages/test_package/a/foo'}
]
};
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -393,22 +400,21 @@ $assetsSection ...@@ -393,22 +400,21 @@ $assetsSection
writeAssets('p/p/', assets); writeAssets('p/p/', assets);
writeAssets('p2/p/', assets); writeAssets('p2/p/', assets);
const String expectedAssetManifest = const Map<Object, Object> expectedAssetManifest = <Object, Object>{
'{"packages/test_package/a/foo":' 'packages/test_package/a/foo': <Map<String, Object>>[
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"],' <String, Object>{'asset': 'packages/test_package/a/foo'},
'"packages/test_package2/a/foo":' <String, Object>{'asset': 'packages/test_package/a/2x/foo', 'dpr': 2.0}
'["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}'; ],
const String expectedBinAssetManifest = 'packages/test_package2/a/foo': <Map<String, Object>>[
'{"packages/test_package/a/foo":' <String, Object>{'asset': 'packages/test_package2/a/foo'},
'[{"asset":"packages/test_package/a/2x/foo","dpr":2.0}],' <String, Object>{'asset': 'packages/test_package2/a/2x/foo', 'dpr': 2.0}
'"packages/test_package2/a/foo":' ]
'[{"asset":"packages/test_package2/a/2x/foo","dpr":2.0}]}'; };
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package', 'test_package2'], <String>['test_package', 'test_package2'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -439,23 +445,21 @@ $assetsSection ...@@ -439,23 +445,21 @@ $assetsSection
writeAssets('p/p/lib/', assets); writeAssets('p/p/lib/', assets);
writeAssets('p2/p/lib/', assets); writeAssets('p2/p/lib/', assets);
const String expectedAssetManifest = const Map<Object, Object> expectedAssetManifest = <Object, Object>{
'{"packages/test_package/a/foo":' 'packages/test_package/a/foo': <Map<String, Object>>[
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"],' <String, Object>{'asset': 'packages/test_package/a/foo'},
'"packages/test_package2/a/foo":' <String, Object>{'asset': 'packages/test_package/a/2x/foo', 'dpr': 2.0}
'["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}'; ],
'packages/test_package2/a/foo': <Map<String, Object>>[
const String expectedBinAssetManifest = <String, Object>{'asset': 'packages/test_package2/a/foo'},
'{"packages/test_package/a/foo":' <String, Object>{'asset': 'packages/test_package2/a/2x/foo', 'dpr': 2.0}
'[{"asset":"packages/test_package/a/2x/foo","dpr":2.0}],' ]
'"packages/test_package2/a/foo":' };
'[{"asset":"packages/test_package2/a/2x/foo","dpr":2.0}]}';
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package', 'test_package2'], <String>['test_package', 'test_package2'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -482,18 +486,17 @@ $assetsSection ...@@ -482,18 +486,17 @@ $assetsSection
final List<String> assets = <String>['a/foo', 'a/2x/foo']; final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p2/p/lib/', assets); writeAssets('p2/p/lib/', assets);
const String expectedAssetManifest = const Map<Object, Object> expectedAssetManifest = <Object, Object>{
'{"packages/test_package2/a/foo":' 'packages/test_package2/a/foo': <Map<String, Object>>[
'["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}'; <String, Object>{'asset': 'packages/test_package2/a/foo'},
const String expectedBinAssetManifest = <String, Object>{'asset': 'packages/test_package2/a/2x/foo', 'dpr': 2.0}
'{"packages/test_package2/a/foo":' ]
'[{"asset":"packages/test_package2/a/2x/foo","dpr":2.0}]}'; };
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package2'], <String>['test_package2'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -513,18 +516,19 @@ $assetsSection ...@@ -513,18 +516,19 @@ $assetsSection
); );
writeAssets('p/p/', assets); writeAssets('p/p/', assets);
const String expectedAssetManifest = const Map<Object, Object> expectedAssetManifest = <Object, Object>{
'{"packages/test_package/a/foo":["packages/test_package/a/foo"],' 'packages/test_package/a/foo': <Map<String, Object>>[
'"packages/test_package/a/foo [x]":["packages/test_package/a/foo [x]"]}'; <String, Object>{'asset': 'packages/test_package/a/foo'}
const String expectedBinAssetManifest = ],
'{"packages/test_package/a/foo":[],' 'packages/test_package/a/foo [x]': <Map<String, Object>>[
'"packages/test_package/a/foo [x]":[]}'; <String, Object>{'asset': 'packages/test_package/a/foo [x]'}
]
};
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -546,18 +550,19 @@ $assetsSection ...@@ -546,18 +550,19 @@ $assetsSection
); );
writeAssets('p/p/', assetsOnDisk); writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest = const Map<Object, Object> expectedAssetManifest = <Object, Object>{
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],' 'packages/test_package/a/bar': <Map<String, Object>>[
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}'; <String, Object>{'asset': 'packages/test_package/a/bar'}
const String expectedBinAssetManifest = ],
'{"packages/test_package/a/bar":[],' 'packages/test_package/a/foo': <Map<String, Object>>[
'"packages/test_package/a/foo":[]}'; <String, Object>{'asset': 'packages/test_package/a/foo'}
]
};
await buildAndVerifyAssets( await buildAndVerifyAssets(
assetsOnDisk, assetsOnDisk,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -578,18 +583,19 @@ $assetsSection ...@@ -578,18 +583,19 @@ $assetsSection
); );
writeAssets('p/p/', assetsOnDisk); writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest = const Map<Object, Object> expectedAssetManifest = <Object, Object>{
'{"packages/test_package/a/foo":["packages/test_package/a/foo"],' 'packages/test_package/a/foo': <Map<String, Object>>[
'"packages/test_package/abc/bar":["packages/test_package/abc/bar"]}'; <String, Object>{'asset': 'packages/test_package/a/foo'}
const String expectedBinAssetManifest = ],
'{"packages/test_package/a/foo":[],' 'packages/test_package/abc/bar': <Map<String, Object>>[
'"packages/test_package/abc/bar":[]}'; <String, Object>{'asset': 'packages/test_package/abc/bar'}
]
};
await buildAndVerifyAssets( await buildAndVerifyAssets(
assetsOnDisk, assetsOnDisk,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -637,16 +643,16 @@ $assetsSection ...@@ -637,16 +643,16 @@ $assetsSection
); );
writeAssets('p/p/', assetsOnDisk); writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest = const Map<Object, Object> expectedAssetManifest = <Object, Object>{
'{"packages/test_package/a/foo":["packages/test_package/a/foo","packages/test_package/a/2x/foo"]}'; 'packages/test_package/a/foo': <Map<String, Object>>[
const String expectedBinAssetManifest = <String, Object>{'asset': 'packages/test_package/a/foo'},
'{"packages/test_package/a/foo":[{"asset":"packages/test_package/a/2x/foo","dpr":2.0}]}'; <String, Object>{'asset': 'packages/test_package/a/2x/foo', 'dpr': 2.0}
]
};
await buildAndVerifyAssets( await buildAndVerifyAssets(
assetsOnDisk, assetsOnDisk,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -667,15 +673,12 @@ $assetsSection ...@@ -667,15 +673,12 @@ $assetsSection
); );
writeAssets('p/p/', assetsOnDisk); writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest = '{}'; const Map<Object, Object> expectedAssetManifest = <Object, Object>{};
const String expectedBinAssetManifest = '{}';
await buildAndVerifyAssets( await buildAndVerifyAssets(
assetOnManifest, assetOnManifest,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest,
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -694,13 +697,8 @@ $assetsSection ...@@ -694,13 +697,8 @@ $assetsSection
assets: assetOnManifest, assets: assetOnManifest,
); );
await buildAndVerifyAssets( final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
assetOnManifest, await bundle.build(packagesPath: '.packages');
<String>['test_package'],
null,
null,
expectExists: false,
);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
...@@ -15,12 +16,13 @@ import 'package:flutter_tools/src/base/user_messages.dart'; ...@@ -15,12 +16,13 @@ import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:standard_message_codec/standard_message_codec.dart';
import '../src/common.dart'; import '../src/common.dart';
void main() { void main() {
Future<Map<String, List<String>>> extractAssetManifestFromBundle(ManifestAssetBundle bundle) async { Future<Map<String, List<String>>> extractAssetManifestJsonFromBundle(ManifestAssetBundle bundle) async {
final String manifestJson = utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()); final String manifestJson = utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes());
final Map<String, dynamic> parsedJson = json.decode(manifestJson) as Map<String, dynamic>; final Map<String, dynamic> parsedJson = json.decode(manifestJson) as Map<String, dynamic>;
final Iterable<String> keys = parsedJson.keys; final Iterable<String> keys = parsedJson.keys;
...@@ -30,6 +32,13 @@ void main() { ...@@ -30,6 +32,13 @@ void main() {
return parsedManifest; return parsedManifest;
} }
Future<Map<Object?, Object?>> extractAssetManifestSmcBinFromBundle(ManifestAssetBundle bundle) async {
final List<int> manifest = await bundle.entries['AssetManifest.smcbin']!.contentsAsBytes();
final ByteData asByteData = ByteData.view(Uint8List.fromList(manifest).buffer);
final Map<Object?, Object?> decoded = const StandardMessageCodec().decodeMessage(asByteData)! as Map<Object?, Object?>;
return decoded;
}
group('AssetBundle asset variants (with Unix-style paths)', () { group('AssetBundle asset variants (with Unix-style paths)', () {
late Platform platform; late Platform platform;
late FileSystem fs; late FileSystem fs;
...@@ -44,7 +53,11 @@ void main() { ...@@ -44,7 +53,11 @@ void main() {
); );
fs.file('.packages').createSync(); fs.file('.packages').createSync();
});
void createPubspec({
required List<String> assets,
}) {
fs.file('pubspec.yaml').writeAsStringSync( fs.file('pubspec.yaml').writeAsStringSync(
''' '''
name: test name: test
...@@ -53,15 +66,14 @@ dependencies: ...@@ -53,15 +66,14 @@ dependencies:
sdk: flutter sdk: flutter
flutter: flutter:
assets: assets:
- assets/ ${assets.map((String entry) => ' - $entry').join('\n')}
- assets/notAVariant/
- assets/folder/
- assets/normalFolder/
''' '''
); );
}); }
testWithoutContext('Only images in folders named with device pixel ratios (e.g. 2x, 3.0x) should be considered as variants of other images', () async { testWithoutContext('Only images in folders named with device pixel ratios (e.g. 2x, 3.0x) should be considered as variants of other images', () async {
createPubspec(assets: <String>['assets/', 'assets/notAVariant/']);
const String image = 'assets/image.jpg'; const String image = 'assets/image.jpg';
const String image2xVariant = 'assets/2x/image.jpg'; const String image2xVariant = 'assets/2x/image.jpg';
const String imageNonVariant = 'assets/notAVariant/image.jpg'; const String imageNonVariant = 'assets/notAVariant/image.jpg';
...@@ -89,14 +101,33 @@ flutter: ...@@ -89,14 +101,33 @@ flutter:
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
); );
final Map<String, List<String>> manifest = await extractAssetManifestFromBundle(bundle); final Map<String, List<String>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
expect(manifest, hasLength(2));
expect(manifest[image], equals(<String>[image, image2xVariant])); final Map<String, List<Map<String, Object>>> expectedAssetManifest = <String, List<Map<String, Object>>>{
expect(manifest[imageNonVariant], equals(<String>[imageNonVariant])); image: <Map<String, Object>>[
<String, String>{
'asset': image,
},
<String, Object>{
'asset': image2xVariant,
'dpr': 2.0,
}
],
imageNonVariant: <Map<String, String>>[
<String, String>{
'asset': imageNonVariant,
}
],
};
expect(smcBinManifest, equals(expectedAssetManifest));
expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest)));
}); });
testWithoutContext('Asset directories are recursively searched for assets', () async { testWithoutContext('Asset directories have their subdirectories searched for asset variants', () async {
createPubspec(assets: <String>['assets/', 'assets/folder/']);
const String topLevelImage = 'assets/image.jpg'; const String topLevelImage = 'assets/image.jpg';
const String secondLevelImage = 'assets/folder/secondLevel.jpg'; const String secondLevelImage = 'assets/folder/secondLevel.jpg';
const String secondLevel2xVariant = 'assets/folder/2x/secondLevel.jpg'; const String secondLevel2xVariant = 'assets/folder/2x/secondLevel.jpg';
...@@ -124,13 +155,36 @@ flutter: ...@@ -124,13 +155,36 @@ flutter:
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
); );
final Map<String, List<String>> manifest = await extractAssetManifestFromBundle(bundle); final Map<String, List<String>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
expect(manifest, hasLength(2)); expect(jsonManifest, hasLength(2));
expect(manifest[topLevelImage], equals(<String>[topLevelImage])); expect(jsonManifest[topLevelImage], equals(<String>[topLevelImage]));
expect(manifest[secondLevelImage], equals(<String>[secondLevelImage, secondLevel2xVariant])); expect(jsonManifest[secondLevelImage], equals(<String>[secondLevelImage, secondLevel2xVariant]));
final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
final Map<String, List<Map<String, Object>>> expectedAssetManifest = <String, List<Map<String, Object>>>{
topLevelImage: <Map<String, Object>>[
<String, String>{
'asset': topLevelImage,
},
],
secondLevelImage: <Map<String, Object>>[
<String, String>{
'asset': secondLevelImage,
},
<String, Object>{
'asset': secondLevel2xVariant,
'dpr': 2.0,
},
],
};
expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest)));
expect(smcBinManifest, equals(expectedAssetManifest));
}); });
testWithoutContext('Asset paths should never be URI-encoded', () async { testWithoutContext('Asset paths should never be URI-encoded', () async {
createPubspec(assets: <String>['assets/normalFolder/']);
const String image = 'assets/normalFolder/i have URI-reserved_characters.jpg'; const String image = 'assets/normalFolder/i have URI-reserved_characters.jpg';
const String imageVariant = 'assets/normalFolder/3x/i have URI-reserved_characters.jpg'; const String imageVariant = 'assets/normalFolder/3x/i have URI-reserved_characters.jpg';
...@@ -156,12 +210,66 @@ flutter: ...@@ -156,12 +210,66 @@ flutter:
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
); );
final Map<String, List<String>> manifest = await extractAssetManifestFromBundle(bundle); final Map<String, List<String>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
expect(manifest, hasLength(1)); final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
expect(manifest[image], equals(<String>[image, imageVariant]));
final Map<String, List<Map<String, Object>>> expectedAssetManifest = <String, List<Map<String, Object>>>{
image: <Map<String, Object>>[
<String, Object>{
'asset': image,
},
<String, Object>{
'asset': imageVariant,
'dpr': 3.0
},
],
};
expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest)));
expect(smcBinManifest, equals(expectedAssetManifest));
}); });
});
testWithoutContext('Main assets are not included if the file does not exist', () async {
createPubspec(assets: <String>['assets/image.png']);
// We intentionally do not add a 'assets/image.png'.
const String imageVariant = 'assets/2x/image.png';
final List<String> assets = <String>[
imageVariant,
];
for (final String asset in assets) {
final File assetFile = fs.file(asset);
assetFile.createSync(recursive: true);
assetFile.writeAsStringSync(asset);
}
final ManifestAssetBundle bundle = ManifestAssetBundle(
logger: BufferLogger.test(),
fileSystem: fs,
platform: platform,
);
await bundle.build(
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
);
final Map<String, List<Map<String, Object>>> expectedManifest = <String, List<Map<String, Object>>>{
'assets/image.png': <Map<String, Object>>[
<String, Object>{
'asset': imageVariant,
'dpr': 2.0
},
],
};
final Map<String, List<String>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
expect(jsonManifest, equals(_assetManifestBinToJson(expectedManifest)));
expect(smcBinManifest, equals(expectedManifest));
});
});
group('AssetBundle asset variants (with Windows-style filepaths)', () { group('AssetBundle asset variants (with Windows-style filepaths)', () {
late final Platform platform; late final Platform platform;
...@@ -217,11 +325,40 @@ flutter: ...@@ -217,11 +325,40 @@ flutter:
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
); );
final Map<String, List<String>> manifest = await extractAssetManifestFromBundle(bundle); final Map<String, List<Map<String, Object>>> expectedAssetManifest = <String, List<Map<String, Object>>>{
'assets/foo.jpg': <Map<String, Object>>[
expect(manifest, hasLength(2)); <String, Object>{
expect(manifest['assets/foo.jpg'], equals(<String>['assets/foo.jpg', 'assets/2x/foo.jpg'])); 'asset': 'assets/foo.jpg',
expect(manifest['assets/somewhereElse/bar.jpg'], equals(<String>['assets/somewhereElse/bar.jpg', 'assets/somewhereElse/2x/bar.jpg'])); },
<String, Object>{
'asset': 'assets/2x/foo.jpg',
'dpr': 2.0,
},
],
'assets/somewhereElse/bar.jpg': <Map<String, Object>>[
<String, Object>{
'asset': 'assets/somewhereElse/bar.jpg',
},
<String, Object>{
'asset': 'assets/somewhereElse/2x/bar.jpg',
'dpr': 2.0,
},
],
};
final Map<String, List<String>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest)));
expect(smcBinManifest, equals(expectedAssetManifest));
}); });
}); });
} }
Map<Object, Object> _assetManifestBinToJson(Map<Object, Object> manifest) {
List<Object> convertList(List<Object> variants) => variants
.map((Object variant) => (variant as Map<Object?, Object?>)['asset']!)
.toList();
return manifest.map((Object key, Object value) => MapEntry<Object, Object>(key, convertList(value as List<Object>)));
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment