Unverified Commit 56cad89b authored by Andrew Kolos's avatar Andrew Kolos Committed by GitHub

Speed up first asset load by encoding asset manifest in binary rather than JSON (#113637)

parent d52e2de5
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show PlatformAssetBundle, StandardMessageCodec;
import 'package:flutter/services.dart' show PlatformAssetBundle;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../common.dart'; import '../common.dart';
...@@ -18,16 +18,14 @@ void main() async { ...@@ -18,16 +18,14 @@ void main() async {
final BenchmarkResultPrinter printer = BenchmarkResultPrinter(); final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
final Stopwatch watch = Stopwatch(); final Stopwatch watch = Stopwatch();
final PlatformAssetBundle bundle = PlatformAssetBundle();
final ByteData assetManifestBytes = await bundle.load('money_asset_manifest.json'); final ByteData assetManifest = await loadAssetManifest();
watch.start(); watch.start();
for (int i = 0; i < _kNumIterations; i++) { for (int i = 0; i < _kNumIterations; i++) {
bundle.clear(); // This is effectively a test.
final String json = utf8.decode(assetManifestBytes.buffer.asUint8List());
// This is a test, so we don't need to worry about this rule.
// ignore: invalid_use_of_visible_for_testing_member // ignore: invalid_use_of_visible_for_testing_member
await AssetImage.manifestParser(json); AssetImage.parseAssetManifest(assetManifest);
} }
watch.stop(); watch.stop();
...@@ -40,3 +38,49 @@ void main() async { ...@@ -40,3 +38,49 @@ void main() async {
printer.printToStdout(); printer.printToStdout();
} }
final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
Future<ByteData> loadAssetManifest() async {
double parseScale(String key) {
final Uri assetUri = Uri.parse(key);
String directoryPath = '';
if (assetUri.pathSegments.length > 1) {
directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
}
final Match? match = _extractRatioRegExp.firstMatch(directoryPath);
if (match != null && match.groupCount > 0) {
return double.parse(match.group(1)!);
}
return 1.0;
}
final Map<String, dynamic> result = <String, dynamic>{};
final PlatformAssetBundle bundle = PlatformAssetBundle();
// For the benchmark, we use the older JSON format and then convert it to the modern binary format.
final ByteData jsonAssetManifestBytes = await bundle.load('money_asset_manifest.json');
final String jsonAssetManifest = utf8.decode(jsonAssetManifestBytes.buffer.asUint8List());
final Map<String, dynamic> assetManifest = json.decode(jsonAssetManifest) as Map<String, dynamic>;
for (final MapEntry<String, dynamic> manifestEntry in assetManifest.entries) {
final List<dynamic> resultVariants = <dynamic>[];
final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>();
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 double variantDevicePixelRatio = parseScale(variant);
resultVariant['asset'] = variant;
resultVariant['dpr'] = variantDevicePixelRatio;
resultVariants.add(resultVariant);
}
result[manifestEntry.key] = resultVariants;
}
return const StandardMessageCodec().encodeMessage(result)!;
}
...@@ -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:async';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gallery/gallery/example_code_parser.dart'; import 'package:flutter_gallery/gallery/example_code_parser.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -58,4 +60,9 @@ class TestAssetBundle extends AssetBundle { ...@@ -58,4 +60,9 @@ class TestAssetBundle extends AssetBundle {
@override @override
String toString() => '$runtimeType@$hashCode()'; String toString() => '$runtimeType@$hashCode()';
@override
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
return parser(await load(key));
}
} }
...@@ -11,7 +11,8 @@ import 'package:flutter/services.dart'; ...@@ -11,7 +11,8 @@ import 'package:flutter/services.dart';
import 'image_provider.dart'; import 'image_provider.dart';
const String _kAssetManifestFileName = 'AssetManifest.json'; const String _kLegacyAssetManifestFilename = 'AssetManifest.json';
const String _kAssetManifestFilename = 'AssetManifest.bin';
/// A screen with a device-pixel ratio strictly less than this value is /// A screen with a device-pixel ratio strictly less than this value is
/// considered a low-resolution screen (typically entry-level to mid-range /// considered a low-resolution screen (typically entry-level to mid-range
...@@ -284,18 +285,45 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -284,18 +285,45 @@ class AssetImage extends AssetBundleImageProvider {
Completer<AssetBundleImageKey>? completer; Completer<AssetBundleImageKey>? completer;
Future<AssetBundleImageKey>? result; Future<AssetBundleImageKey>? result;
chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>( Future<_AssetManifest> loadJsonAssetManifest() {
(Map<String, List<String>>? manifest) { Future<_AssetManifest> parseJson(String data) {
final String chosenName = _chooseVariant( final _AssetManifest parsed = _LegacyAssetManifest.fromJsonString(data);
return SynchronousFuture<_AssetManifest>(parsed);
}
return chosenBundle.loadStructuredData(_kLegacyAssetManifestFilename, parseJson);
}
// TODO(andrewkolos): Once google3 and google-fonts-flutter are migrated
// away from using AssetManifest.json, remove all references to it.
// See https://github.com/flutter/flutter/issues/114913.
Future<_AssetManifest>? manifest;
// Since AssetBundle load calls can be synchronous (e.g. in the case of tests),
// it is not sufficient to only use catchError/onError or the onError parameter
// of Future.then--we also have to use a synchronous try/catch. Once google3
// tooling starts producing AssetManifest.bin, this block can be removed.
try {
manifest = chosenBundle.loadStructuredBinaryData(_kAssetManifestFilename, _AssetManifestBin.fromStandardMessageCodecMessage);
} catch (error) {
manifest = loadJsonAssetManifest();
}
manifest
// To understand why we use this no-op `then` instead of `catchError`/`onError`,
// see https://github.com/flutter/flutter/issues/115601
.then((_AssetManifest manifest) => manifest,
onError: (Object? error, StackTrace? stack) => loadJsonAssetManifest())
.then((_AssetManifest manifest) {
final List<_AssetVariant> candidateVariants = manifest.getVariants(keyName);
final _AssetVariant chosenVariant = _chooseVariant(
keyName, keyName,
configuration, configuration,
manifest == null ? null : manifest[keyName], candidateVariants,
)!; );
final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = AssetBundleImageKey( final AssetBundleImageKey key = AssetBundleImageKey(
bundle: chosenBundle, bundle: chosenBundle,
name: chosenName, name: chosenVariant.asset,
scale: chosenScale, scale: chosenVariant.devicePixelRatio,
); );
if (completer != null) { if (completer != null) {
// We already returned from this function, which means we are in the // We already returned from this function, which means we are in the
...@@ -309,14 +337,15 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -309,14 +337,15 @@ class AssetImage extends AssetBundleImageProvider {
// ourselves. // ourselves.
result = SynchronousFuture<AssetBundleImageKey>(key); result = SynchronousFuture<AssetBundleImageKey>(key);
} }
}, })
).catchError((Object error, StackTrace stack) { .onError((Object error, StackTrace stack) {
// We had an error. (This guarantees we weren't called synchronously.) // We had an error. (This guarantees we weren't called synchronously.)
// Forward the error to the caller. // Forward the error to the caller.
assert(completer != null); assert(completer != null);
assert(result == null); assert(result == null);
completer!.completeError(error, stack); completer!.completeError(error, stack);
}); });
if (result != null) { if (result != null) {
// The code above ran synchronously, and came up with an answer. // The code above ran synchronously, and came up with an answer.
// Return the SynchronousFuture that we created above. // Return the SynchronousFuture that we created above.
...@@ -328,35 +357,29 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -328,35 +357,29 @@ class AssetImage extends AssetBundleImageProvider {
return completer.future; return completer.future;
} }
/// Parses the asset manifest string into a strongly-typed map. /// Parses the asset manifest's file contents into it's Dart representation.
@visibleForTesting @visibleForTesting
static Future<Map<String, List<String>>?> manifestParser(String? jsonData) { // Return type is set to Object?, because the specific type is private.
if (jsonData == null) { static Object? parseAssetManifest(ByteData bytes) {
return SynchronousFuture<Map<String, List<String>>?>(null); return _AssetManifestBin.fromStandardMessageCodecMessage(bytes);
}
// TODO(ianh): JSON decoding really shouldn't be on the main thread.
final Map<String, dynamic> parsedJson = json.decode(jsonData) as Map<String, dynamic>;
final Iterable<String> keys = parsedJson.keys;
final Map<String, List<String>> parsedManifest = <String, List<String>> {
for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>),
};
// TODO(ianh): convert that data structure to the right types.
return SynchronousFuture<Map<String, List<String>>?>(parsedManifest);
} }
String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) { _AssetVariant _chooseVariant(String mainAssetKey, ImageConfiguration config, List<_AssetVariant> candidateVariants) {
if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) { final _AssetVariant mainAsset = _AssetVariant(asset: mainAssetKey,
return main; devicePixelRatio: _naturalResolution);
if (config.devicePixelRatio == null || candidateVariants.isEmpty) {
return mainAsset;
} }
// TODO(ianh): Consider moving this parsing logic into _manifestParser. final SplayTreeMap<double, _AssetVariant> candidatesByDevicePixelRatio =
final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>(); SplayTreeMap<double, _AssetVariant>();
for (final String candidate in candidates) { for (final _AssetVariant candidate in candidateVariants) {
mapping[_parseScale(candidate)] = candidate; candidatesByDevicePixelRatio[candidate.devicePixelRatio] = candidate;
} }
candidatesByDevicePixelRatio.putIfAbsent(_naturalResolution, () => mainAsset);
// TODO(ianh): implement support for config.locale, config.textDirection, // TODO(ianh): implement support for config.locale, config.textDirection,
// config.size, config.platform (then document this over in the Image.asset // config.size, config.platform (then document this over in the Image.asset
// docs) // docs)
return _findBestVariant(mapping, config.devicePixelRatio!); return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
} }
// Returns the "best" asset variant amongst the available `candidates`. // Returns the "best" asset variant amongst the available `candidates`.
...@@ -371,17 +394,17 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -371,17 +394,17 @@ class AssetImage extends AssetBundleImageProvider {
// lowest key higher than `value`. // lowest key higher than `value`.
// - If the screen has high device pixel ratio, choose the variant with the // - If the screen has high device pixel ratio, choose the variant with the
// key nearest to `value`. // key nearest to `value`.
String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) { _AssetVariant _findBestVariant(SplayTreeMap<double, _AssetVariant> candidatesByDpr, double value) {
if (candidates.containsKey(value)) { if (candidatesByDpr.containsKey(value)) {
return candidates[value]!; return candidatesByDpr[value]!;
} }
final double? lower = candidates.lastKeyBefore(value); final double? lower = candidatesByDpr.lastKeyBefore(value);
final double? upper = candidates.firstKeyAfter(value); final double? upper = candidatesByDpr.firstKeyAfter(value);
if (lower == null) { if (lower == null) {
return candidates[upper]; return candidatesByDpr[upper]!;
} }
if (upper == null) { if (upper == null) {
return candidates[lower]; return candidatesByDpr[lower]!;
} }
// On screens with low device-pixel ratios the artifacts from upscaling // On screens with low device-pixel ratios the artifacts from upscaling
...@@ -389,20 +412,116 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -389,20 +412,116 @@ class AssetImage extends AssetBundleImageProvider {
// ratios because the physical pixels are larger. Choose the higher // ratios because the physical pixels are larger. Choose the higher
// resolution image in that case instead of the nearest one. // resolution image in that case instead of the nearest one.
if (value < _kLowDprLimit || value > (lower + upper) / 2) { if (value < _kLowDprLimit || value > (lower + upper) / 2) {
return candidates[upper]; return candidatesByDpr[upper]!;
} else { } else {
return candidates[lower]; return candidatesByDpr[lower]!;
} }
} }
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is AssetImage
&& other.keyName == keyName
&& other.bundle == bundle;
}
@override
int get hashCode => Object.hash(keyName, bundle);
@override
String toString() => '${objectRuntimeType(this, 'AssetImage')}(bundle: $bundle, name: "$keyName")';
}
/// Centralizes parsing and typecasting of the contents of the asset manifest file,
/// which is generated by the flutter tool at build time.
abstract class _AssetManifest {
List<_AssetVariant> getVariants(String key);
}
/// Parses the binary asset manifest into a data structure that's easier to work with.
///
/// The asset manifest is a map of asset files to a list of objects containing
/// information about variants of that asset.
///
/// The entries with each variant object are:
/// - "asset": the location of this variant to load it from.
/// - "dpr": The device-pixel-ratio that the asset is best-suited for.
///
/// New fields could be added to this object schema to support new asset variation
/// features, such as themes, locale/region support, reading directions, and so on.
class _AssetManifestBin implements _AssetManifest {
_AssetManifestBin(Map<Object?, Object?> standardMessageData): _data = standardMessageData;
factory _AssetManifestBin.fromStandardMessageCodecMessage(ByteData message) {
final Object? data = const StandardMessageCodec().decodeMessage(message);
return _AssetManifestBin(data! as Map<Object?, Object?>);
}
final Map<Object?, Object?> _data;
final Map<String, List<_AssetVariant>> _typeCastedData = <String, List<_AssetVariant>>{};
@override
List<_AssetVariant> getVariants(String key) {
// We lazily delay typecasting to prevent a performance hiccup when parsing
// large asset manifests.
if (!_typeCastedData.containsKey(key)) {
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as List<Object?>)
.cast<Map<Object?, Object?>>()
.map(_AssetVariant.fromManifestData)
.toList();
}
return _typeCastedData[key]!;
}
}
class _LegacyAssetManifest implements _AssetManifest {
_LegacyAssetManifest({
required this.manifest,
});
factory _LegacyAssetManifest.fromJsonString(String jsonString) {
List<_AssetVariant> adaptLegacyVariantList(String mainAsset, List<String> variants) {
return variants
.map((String variant) =>
_AssetVariant(asset: variant, devicePixelRatio: _parseScale(mainAsset, variant)))
.toList();
}
if (jsonString == null) {
return _LegacyAssetManifest(manifest: <String, List<_AssetVariant>>{});
}
final Map<String, Object?> parsedJson = json.decode(jsonString) as Map<String, dynamic>;
final Iterable<String> keys = parsedJson.keys;
final Map<String, List<String>> parsedManifest = <String, List<String>> {
for (final String key in keys) key: List<String>.from(parsedJson[key]! as List<dynamic>),
};
final Map<String, List<_AssetVariant>> manifestWithParsedVariants =
parsedManifest.map((String asset, List<String> variants) =>
MapEntry<String, List<_AssetVariant>>(asset, adaptLegacyVariantList(asset, variants)));
return _LegacyAssetManifest(manifest: manifestWithParsedVariants);
}
final Map<String, List<_AssetVariant>> manifest;
static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$'); static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
static const double _naturalResolution = 1.0;
@override
List<_AssetVariant> getVariants(String key) {
return manifest[key] ?? const <_AssetVariant>[];
}
double _parseScale(String key) { static double _parseScale(String mainAsset, String variant) {
if (key == assetName) { // The legacy asset manifest includes the main asset within its variant list.
if (mainAsset == variant) {
return _naturalResolution; return _naturalResolution;
} }
final Uri assetUri = Uri.parse(key); final Uri assetUri = Uri.parse(variant);
String directoryPath = ''; String directoryPath = '';
if (assetUri.pathSegments.length > 1) { if (assetUri.pathSegments.length > 1) {
directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2]; directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
...@@ -412,22 +531,23 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -412,22 +531,23 @@ class AssetImage extends AssetBundleImageProvider {
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 _naturalResolution; // i.e. default to 1.0x return _naturalResolution; // i.e. default to 1.0x
} }
}
@override class _AssetVariant {
bool operator ==(Object other) { _AssetVariant({
if (other.runtimeType != runtimeType) { required this.asset,
return false; required this.devicePixelRatio,
} });
return other is AssetImage
&& other.keyName == keyName
&& other.bundle == bundle;
}
@override factory _AssetVariant.fromManifestData(Object data) {
int get hashCode => Object.hash(keyName, bundle); final Map<Object?, Object?> asStructuredData = data as Map<Object?, Object?>;
return _AssetVariant(asset: asStructuredData['asset']! as String,
devicePixelRatio: asStructuredData['dpr']! as double);
}
@override final double devicePixelRatio;
String toString() => '${objectRuntimeType(this, 'AssetImage')}(bundle: $bundle, name: "$keyName")'; final String asset;
} }
...@@ -96,12 +96,25 @@ abstract class AssetBundle { ...@@ -96,12 +96,25 @@ abstract class AssetBundle {
} }
/// Retrieve a string from the asset bundle, parse it with the given function, /// Retrieve a string from the asset bundle, parse it with the given function,
/// and return the function's result. /// and return that function's result.
/// ///
/// Implementations may cache the result, so a particular key should only be /// Implementations may cache the result, so a particular key should only be
/// used with one parser for the lifetime of the asset bundle. /// used with one parser for the lifetime of the asset bundle.
Future<T> loadStructuredData<T>(String key, Future<T> Function(String value) parser); Future<T> loadStructuredData<T>(String key, Future<T> Function(String value) parser);
/// Retrieve [ByteData] from the asset bundle, parse it with the given function,
/// and return that function's result.
///
/// Implementations may cache the result, so a particular key should only be
/// used with one parser for the lifetime of the asset bundle.
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
final ByteData data = await load(key);
if (data == null) {
throw FlutterError('Unable to load asset: $key');
}
return parser(data);
}
/// If this is a caching asset bundle, and the given key describes a cached /// If this is a caching asset bundle, and the given key describes a cached
/// asset, then evict the asset from the cache so that the next time it is /// asset, then evict the asset from the cache so that the next time it is
/// loaded, the cache will be reread from the asset bundle. /// loaded, the cache will be reread from the asset bundle.
...@@ -156,6 +169,18 @@ class NetworkAssetBundle extends AssetBundle { ...@@ -156,6 +169,18 @@ class NetworkAssetBundle extends AssetBundle {
return parser(await loadString(key)); return parser(await loadString(key));
} }
/// Retrieve [ByteData] from the asset bundle, parse it with the given function,
/// and return the function's result.
///
/// The result is not cached. The parser is run each time the resource is
/// fetched.
@override
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
assert(key != null);
assert(parser != null);
return parser(await load(key));
}
// TODO(ianh): Once the underlying network logic learns about caching, we // TODO(ianh): Once the underlying network logic learns about caching, we
// should implement evict(). // should implement evict().
...@@ -175,6 +200,7 @@ abstract class CachingAssetBundle extends AssetBundle { ...@@ -175,6 +200,7 @@ abstract class CachingAssetBundle extends AssetBundle {
// TODO(ianh): Replace this with an intelligent cache, see https://github.com/flutter/flutter/issues/3568 // TODO(ianh): Replace this with an intelligent cache, see https://github.com/flutter/flutter/issues/3568
final Map<String, Future<String>> _stringCache = <String, Future<String>>{}; final Map<String, Future<String>> _stringCache = <String, Future<String>>{};
final Map<String, Future<dynamic>> _structuredDataCache = <String, Future<dynamic>>{}; final Map<String, Future<dynamic>> _structuredDataCache = <String, Future<dynamic>>{};
final Map<String, Future<dynamic>> _structuredBinaryDataCache = <String, Future<dynamic>>{};
@override @override
Future<String> loadString(String key, { bool cache = true }) { Future<String> loadString(String key, { bool cache = true }) {
...@@ -225,16 +251,69 @@ abstract class CachingAssetBundle extends AssetBundle { ...@@ -225,16 +251,69 @@ abstract class CachingAssetBundle extends AssetBundle {
return completer.future; return completer.future;
} }
/// Retrieve bytedata from the asset bundle, parse it with the given function,
/// and return the function's result.
///
/// The result of parsing the bytedata is cached (the bytedata itself is not).
/// For any given `key`, the `parser` is only run the first time.
///
/// Once the value has been parsed, the future returned by this function for
/// subsequent calls will be a [SynchronousFuture], which resolves its
/// callback synchronously.
@override
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) {
assert(key != null);
assert(parser != null);
if (_structuredBinaryDataCache.containsKey(key)) {
return _structuredBinaryDataCache[key]! as Future<T>;
}
// load can return a SynchronousFuture in certain cases, like in the
// flutter_test framework. So, we need to support both async and sync flows.
Completer<T>? completer; // For async flow.
SynchronousFuture<T>? result; // For sync flow.
load(key)
.then<T>(parser)
.then<void>((T value) {
result = SynchronousFuture<T>(value);
if (completer != null) {
// The load and parse operation ran asynchronously. We already returned
// from the loadStructuredBinaryData function and therefore the caller
// was given the future of the completer.
completer.complete(value);
}
}, onError: (Object err, StackTrace? stack) {
completer!.completeError(err, stack);
});
if (result != null) {
// The above code ran synchronously. We can synchronously return the result.
_structuredBinaryDataCache[key] = result!;
return result!;
}
// Since the above code is being run asynchronously and thus hasn't run its
// `then` handler yet, we'll return a completer that will be completed
// when the handler does run.
completer = Completer<T>();
_structuredBinaryDataCache[key] = completer.future;
return completer.future;
}
@override @override
void evict(String key) { void evict(String key) {
_stringCache.remove(key); _stringCache.remove(key);
_structuredDataCache.remove(key); _structuredDataCache.remove(key);
_structuredBinaryDataCache.remove(key);
} }
@override @override
void clear() { void clear() {
_stringCache.clear(); _stringCache.clear();
_structuredDataCache.clear(); _structuredDataCache.clear();
_structuredBinaryDataCache.clear();
} }
@override @override
...@@ -276,7 +355,7 @@ class PlatformAssetBundle extends CachingAssetBundle { ...@@ -276,7 +355,7 @@ class PlatformAssetBundle extends CachingAssetBundle {
bool debugUsePlatformChannel = false; bool debugUsePlatformChannel = false;
assert(() { assert(() {
// dart:io is safe to use here since we early return for web // dart:io is safe to use here since we early return for web
// above. If that code is changed, this needs to be gaurded on // above. If that code is changed, this needs to be guarded on
// web presence. Override how assets are loaded in tests so that // web presence. Override how assets are loaded in tests so that
// the old loader behavior that allows tests to load assets from // the old loader behavior that allows tests to load assets from
// the current package using the package prefix. // the current package using the package prefix.
......
...@@ -13,18 +13,14 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -13,18 +13,14 @@ import 'package:flutter_test/flutter_test.dart';
class TestAssetBundle extends CachingAssetBundle { class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle(this._assetBundleMap); TestAssetBundle(this._assetBundleMap);
final Map<String, List<String>> _assetBundleMap; final Map<String, List<Map<dynamic, dynamic>>> _assetBundleMap;
Map<String, int> loadCallCount = <String, int>{}; Map<String, int> loadCallCount = <String, int>{};
String get _assetBundleContents {
return json.encode(_assetBundleMap);
}
@override @override
Future<ByteData> load(String key) async { Future<ByteData> load(String key) async {
if (key == 'AssetManifest.json') { if (key == 'AssetManifest.bin') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer); return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
} }
loadCallCount[key] = loadCallCount[key] ?? 0 + 1; loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
...@@ -42,12 +38,71 @@ class TestAssetBundle extends CachingAssetBundle { ...@@ -42,12 +38,71 @@ class TestAssetBundle extends CachingAssetBundle {
} }
} }
class BundleWithoutAssetManifestBin extends CachingAssetBundle {
BundleWithoutAssetManifestBin(this._legacyAssetBundleMap);
final Map<dynamic, List<String>> _legacyAssetBundleMap;
Map<String, int> loadCallCount = <String, int>{};
@override
Future<ByteData> load(String key) async {
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
if (key == 'AssetManifest.bin') {
throw FlutterError('AssetManifest.bin was not found.');
}
if (key == 'AssetManifest.json') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(json.encode(_legacyAssetBundleMap))).buffer);
}
switch (key) {
case 'assets/image.png':
return testByteData(1.0); // see "...with a main asset and a 1.0x asset"
case 'assets/2.0x/image.png':
return testByteData(1.5);
}
throw FlutterError('Unexpected key: $key');
}
@override
Future<ui.ImmutableBuffer> loadBuffer(String key) async {
final ByteData data = await load(key);
return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List());
}
}
void main() { void main() {
// TODO(andrewkolos): Once google3 is migrated away from using AssetManifest.json,
// remove all references to it. See https://github.com/flutter/flutter/issues/114913.
test('AssetBundle falls back to using AssetManifest.json if AssetManifest.bin cannot be found.', () async {
const String assetPath = 'assets/image.png';
final Map<dynamic, List<String>> assetBundleMap = <dynamic, List<String>>{};
assetBundleMap[assetPath] = <String>[];
final AssetImage assetImage = AssetImage(assetPath, bundle: BundleWithoutAssetManifestBin(assetBundleMap));
final AssetBundleImageKey key = await assetImage.obtainKey(ImageConfiguration.empty);
expect(key.name, assetPath);
expect(key.scale, 1.0);
});
test('When using AssetManifest.json, on a high DPR device, a high dpr variant is selected.', () async {
const String assetPath = 'assets/image.png';
const String asset2xPath = 'assets/2.0x/image.png';
final Map<dynamic, List<String>> assetBundleMap = <dynamic, List<String>>{};
assetBundleMap[assetPath] = <String>[asset2xPath];
final AssetImage assetImage = AssetImage(assetPath, bundle: BundleWithoutAssetManifestBin(assetBundleMap));
final AssetBundleImageKey key = await assetImage.obtainKey(const ImageConfiguration(devicePixelRatio: 2.0));
expect(key.name, asset2xPath);
expect(key.scale, 2.0);
});
group('1.0 scale device tests', () { group('1.0 scale device tests', () {
void buildAndTestWithOneAsset(String mainAssetPath) { void buildAndTestWithOneAsset(String mainAssetPath) {
final Map<String, List<String>> assetBundleMap = <String, List<String>>{}; final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{};
assetBundleMap[mainAssetPath] = <String>[]; assetBundleMap[mainAssetPath] = <Map<dynamic,dynamic>>[];
final AssetImage assetImage = AssetImage( final AssetImage assetImage = AssetImage(
mainAssetPath, mainAssetPath,
...@@ -93,10 +148,13 @@ void main() { ...@@ -93,10 +148,13 @@ void main() {
const String mainAssetPath = 'assets/normalFolder/normalFile.png'; const String mainAssetPath = 'assets/normalFolder/normalFile.png';
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png'; const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
final Map<String, List<String>> assetBundleMap = final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<String>>{}; <String, List<Map<dynamic, dynamic>>>{};
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath]; final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
mainAssetVariantManifestEntry['asset'] = variantPath;
mainAssetVariantManifestEntry['dpr'] = 3.0;
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
...@@ -123,10 +181,10 @@ void main() { ...@@ -123,10 +181,10 @@ void main() {
test('When high-res device and high-res asset not present in bundle then return main variant', () { test('When high-res device and high-res asset not present in bundle then return main variant', () {
const String mainAssetPath = 'assets/normalFolder/normalFile.png'; const String mainAssetPath = 'assets/normalFolder/normalFile.png';
final Map<String, List<String>> assetBundleMap = final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<String>>{}; <String, List<Map<dynamic, dynamic>>>{};
assetBundleMap[mainAssetPath] = <String>[mainAssetPath]; assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
...@@ -156,16 +214,18 @@ void main() { ...@@ -156,16 +214,18 @@ void main() {
const String mainAssetPath = 'assets/normalFolder/normalFile.png'; const String mainAssetPath = 'assets/normalFolder/normalFile.png';
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png'; const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
void buildBundleAndTestVariantLogic( void buildBundleAndTestVariantLogic(
double deviceRatio, double deviceRatio,
double chosenAssetRatio, double chosenAssetRatio,
String expectedAssetPath, String expectedAssetPath,
) { ) {
final Map<String, List<String>> assetBundleMap = final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<String>>{}; <String, List<Map<dynamic, dynamic>>>{};
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath]; final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
mainAssetVariantManifestEntry['asset'] = variantPath;
mainAssetVariantManifestEntry['dpr'] = 3.0;
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......
...@@ -9,14 +9,14 @@ import 'package:flutter/painting.dart'; ...@@ -9,14 +9,14 @@ import 'package:flutter/painting.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
class TestAssetBundle extends CachingAssetBundle { class _TestAssetBundle extends CachingAssetBundle {
Map<String, int> loadCallCount = <String, int>{}; Map<String, int> loadCallCount = <String, int>{};
@override @override
Future<ByteData> load(String key) async { Future<ByteData> load(String key) async {
loadCallCount[key] = loadCallCount[key] ?? 0 + 1; loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
if (key == 'AssetManifest.json') { if (key == 'AssetManifest.bin') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert('{"one": ["one"]}')).buffer); return const StandardMessageCodec().encodeMessage(json.decode('{"one":[]}'))!;
} }
if (key == 'one') { if (key == 'one') {
...@@ -30,7 +30,7 @@ void main() { ...@@ -30,7 +30,7 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
test('Caching asset bundle test', () async { test('Caching asset bundle test', () async {
final TestAssetBundle bundle = TestAssetBundle(); final _TestAssetBundle bundle = _TestAssetBundle();
final ByteData assetData = await bundle.load('one'); final ByteData assetData = await bundle.load('one');
expect(assetData.getInt8(0), equals(49)); expect(assetData.getInt8(0), equals(49));
...@@ -53,7 +53,7 @@ void main() { ...@@ -53,7 +53,7 @@ void main() {
test('AssetImage.obtainKey succeeds with ImageConfiguration.empty', () async { test('AssetImage.obtainKey succeeds with ImageConfiguration.empty', () async {
// This is a regression test for https://github.com/flutter/flutter/issues/12392 // This is a regression test for https://github.com/flutter/flutter/issues/12392
final AssetImage assetImage = AssetImage('one', bundle: TestAssetBundle()); final AssetImage assetImage = AssetImage('one', bundle: _TestAssetBundle());
final AssetBundleImageKey key = await assetImage.obtainKey(ImageConfiguration.empty); final AssetBundleImageKey key = await assetImage.obtainKey(ImageConfiguration.empty);
expect(key.name, 'one'); expect(key.name, 'one');
expect(key.scale, 1.0); expect(key.scale, 1.0);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
@TestOn('!chrome') @TestOn('!chrome')
import 'dart:convert';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Image;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -16,27 +17,32 @@ import '../image_data.dart'; ...@@ -16,27 +17,32 @@ 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);
const String testManifest = ''' final Map<dynamic, dynamic> testManifest = json.decode('''
{ {
"assets/image.png" : [ "assets/image.png" : [
"assets/image.png", {"asset": "assets/1.5x/image.png", "dpr": 1.5},
"assets/1.5x/image.png", {"asset": "assets/2.0x/image.png", "dpr": 2.0},
"assets/2.0x/image.png", {"asset": "assets/3.0x/image.png", "dpr": 3.0},
"assets/3.0x/image.png", {"asset": "assets/4.0x/image.png", "dpr": 4.0}
"assets/4.0x/image.png"
] ]
} }
'''; ''') as Map<dynamic, dynamic>;
class TestAssetBundle extends CachingAssetBundle { class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle({ this.manifest = testManifest });
final String manifest; TestAssetBundle({ required Map<dynamic, dynamic> manifest }) {
this.manifest = const StandardMessageCodec().encodeMessage(manifest)!;
}
late final ByteData manifest;
@override @override
Future<ByteData> load(String key) { Future<ByteData> load(String key) {
late ByteData data; late ByteData data;
switch (key) { switch (key) {
case 'AssetManifest.bin':
data = manifest;
break;
case 'assets/image.png': case 'assets/image.png':
data = testByteData(1.0); data = testByteData(1.0);
break; break;
...@@ -59,14 +65,6 @@ class TestAssetBundle extends CachingAssetBundle { ...@@ -59,14 +65,6 @@ class TestAssetBundle extends CachingAssetBundle {
return SynchronousFuture<ByteData>(data); return SynchronousFuture<ByteData>(data);
} }
@override
Future<String> loadString(String key, { bool cache = true }) {
if (key == 'AssetManifest.json') {
return SynchronousFuture<String>(manifest);
}
return SynchronousFuture<String>('');
}
@override @override
String toString() => '${describeIdentity(this)}()'; String toString() => '${describeIdentity(this)}()';
} }
...@@ -106,7 +104,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize ...@@ -106,7 +104,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize
devicePixelRatio: ratio, devicePixelRatio: ratio,
), ),
child: DefaultAssetBundle( child: DefaultAssetBundle(
bundle: bundle ?? TestAssetBundle(), bundle: bundle ?? TestAssetBundle(manifest: testManifest),
child: Center( child: Center(
child: inferSize ? child: inferSize ?
Image( Image(
...@@ -259,46 +257,21 @@ void main() { ...@@ -259,46 +257,21 @@ void main() {
expect(getRenderImage(tester, key).scale, 4.0); expect(getRenderImage(tester, key).scale, 4.0);
}); });
testWidgets('Image for device pixel ratio 1.0, with no main asset', (WidgetTester tester) async {
const String manifest = '''
{
"assets/image.png" : [
"assets/1.5x/image.png",
"assets/2.0x/image.png",
"assets/3.0x/image.png",
"assets/4.0x/image.png"
]
}
''';
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
const double ratio = 1.0;
Key key = GlobalKey();
await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
expect(getRenderImage(tester, key).scale, 1.5);
key = GlobalKey();
await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
expect(getRenderImage(tester, key).scale, 1.5);
});
testWidgets('Image for device pixel ratio 1.0, with a main asset and a 1.0x asset', (WidgetTester tester) async { testWidgets('Image for device pixel ratio 1.0, with a main asset and a 1.0x asset', (WidgetTester tester) async {
// If both a main asset and a 1.0x asset are specified, then prefer // If both a main asset and a 1.0x asset are specified, then prefer
// the 1.0x asset. // the 1.0x asset.
const String manifest = ''' final Map<dynamic, dynamic> manifest = json.decode('''
{ {
"assets/image.png" : [ "assets/image.png" : [
"assets/image.png", {"asset": "assets/1.0x/image.png", "dpr":1.0},
"assets/1.0x/image.png", {"asset": "assets/1.5x/image.png", "dpr":1.5},
"assets/1.5x/image.png", {"asset": "assets/2.0x/image.png", "dpr":2.0},
"assets/2.0x/image.png", {"asset": "assets/3.0x/image.png", "dpr":3.0},
"assets/3.0x/image.png", {"asset": "assets/4.0x/image.png", "dpr":4.0}
"assets/4.0x/image.png"
] ]
} }
'''; ''') as Map<dynamic, dynamic>;
final AssetBundle bundle = TestAssetBundle(manifest: manifest); final AssetBundle bundle = TestAssetBundle(manifest: manifest);
const double ratio = 1.0; const double ratio = 1.0;
...@@ -337,14 +310,13 @@ void main() { ...@@ -337,14 +310,13 @@ 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 AssetBundle bundle = TestAssetBundle(manifest: ''' final AssetBundle bundle = TestAssetBundle(manifest: json.decode('''
{ {
"assets/image.png" : [ "assets/image.png" : [
"assets/image.png", {"asset": "assets/1.5x/image.png", "dpr": 1.5}
"assets/1.5x/image.png"
] ]
} }
'''); ''') as Map<dynamic, dynamic>);
Future<void> testRatio({required double ratio, required double expectedScale}) async { Future<void> testRatio({required double ratio, required double expectedScale}) async {
Key key = GlobalKey(); Key key = GlobalKey();
......
...@@ -2,8 +2,11 @@ ...@@ -2,8 +2,11 @@
// 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:typed_data';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:standard_message_codec/standard_message_codec.dart';
import 'base/context.dart'; import 'base/context.dart';
import 'base/deferred_component.dart'; import 'base/deferred_component.dart';
...@@ -136,6 +139,9 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -136,6 +139,9 @@ class ManifestAssetBundle implements AssetBundle {
_splitDeferredAssets = splitDeferredAssets, _splitDeferredAssets = splitDeferredAssets,
_licenseCollector = LicenseCollector(fileSystem: fileSystem); _licenseCollector = LicenseCollector(fileSystem: fileSystem);
// We assume the main asset is designed for a device pixel ratio of 1.0
static const double _defaultResolution = 1.0;
final Logger _logger; final Logger _logger;
final FileSystem _fileSystem; final FileSystem _fileSystem;
final LicenseCollector _licenseCollector; final LicenseCollector _licenseCollector;
...@@ -161,7 +167,8 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -161,7 +167,8 @@ class ManifestAssetBundle implements AssetBundle {
DateTime? _lastBuildTimestamp; DateTime? _lastBuildTimestamp;
static const String _kAssetManifestJson = 'AssetManifest.json'; static const String _kAssetManifestBinFileName = 'AssetManifest.bin';
static const String _kAssetManifestJsonFileName = 'AssetManifest.json';
static const String _kNoticeFile = 'NOTICES'; static const String _kNoticeFile = 'NOTICES';
// Comically, this can't be name with the more common .gz file extension // Comically, this can't be name with the more common .gz file extension
// because when it's part of an AAR and brought into another APK via gradle, // because when it's part of an AAR and brought into another APK via gradle,
...@@ -229,8 +236,13 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -229,8 +236,13 @@ class ManifestAssetBundle implements AssetBundle {
// device. // device.
_lastBuildTimestamp = DateTime.now(); _lastBuildTimestamp = DateTime.now();
if (flutterManifest.isEmpty) { if (flutterManifest.isEmpty) {
entries[_kAssetManifestJson] = DevFSStringContent('{}'); entries[_kAssetManifestJsonFileName] = DevFSStringContent('{}');
entryKinds[_kAssetManifestJson] = AssetKind.regular; entryKinds[_kAssetManifestJsonFileName] = AssetKind.regular;
final ByteData emptyAssetManifest =
const StandardMessageCodec().encodeMessage(<dynamic, dynamic>{})!;
entries[_kAssetManifestBinFileName] =
DevFSByteContent(emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes));
entryKinds[_kAssetManifestBinFileName] = AssetKind.regular;
return 0; return 0;
} }
...@@ -426,7 +438,11 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -426,7 +438,11 @@ class ManifestAssetBundle implements AssetBundle {
_wildcardDirectories[uri] ??= _fileSystem.directory(uri); _wildcardDirectories[uri] ??= _fileSystem.directory(uri);
} }
final DevFSStringContent assetManifest = _createAssetManifest(assetVariants, deferredComponentsAssetVariants); final Map<String, List<String>> assetManifest =
_createAssetManifest(assetVariants, deferredComponentsAssetVariants);
final DevFSStringContent assetManifestJson = DevFSStringContent(json.encode(assetManifest));
final DevFSByteContent assetManifestBinary = _createAssetManifestBinary(assetManifest);
final DevFSStringContent fontManifest = DevFSStringContent(json.encode(fonts)); final DevFSStringContent fontManifest = DevFSStringContent(json.encode(fonts));
final LicenseResult licenseResult = _licenseCollector.obtainLicenses(packageConfig, additionalLicenseFiles); final LicenseResult licenseResult = _licenseCollector.obtainLicenses(packageConfig, additionalLicenseFiles);
if (licenseResult.errorMessages.isNotEmpty) { if (licenseResult.errorMessages.isNotEmpty) {
...@@ -450,7 +466,8 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -450,7 +466,8 @@ class ManifestAssetBundle implements AssetBundle {
_fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute); _fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute);
} }
_setIfChanged(_kAssetManifestJson, assetManifest, AssetKind.regular); _setIfChanged(_kAssetManifestJsonFileName, assetManifestJson, AssetKind.regular);
_setIfChanged(_kAssetManifestBinFileName, assetManifestBinary, AssetKind.regular);
_setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular); _setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular);
_setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform); _setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform);
return 0; return 0;
...@@ -459,17 +476,31 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -459,17 +476,31 @@ class ManifestAssetBundle implements AssetBundle {
@override @override
List<File> additionalDependencies = <File>[]; List<File> additionalDependencies = <File>[];
void _setIfChanged(String key, DevFSStringContent content, AssetKind assetKind) { void _setIfChanged(String key, DevFSContent content, AssetKind assetKind) {
if (!entries.containsKey(key)) { bool areEqual(List<int> o1, List<int> o2) {
entries[key] = content; if (o1.length != o2.length) {
entryKinds[key] = assetKind; return false;
return; }
for (int index = 0; index < o1.length; index++) {
if (o1[index] != o2[index]) {
return false;
}
}
return true;
} }
final DevFSStringContent? oldContent = entries[key] as DevFSStringContent?;
if (oldContent?.string != content.string) { final DevFSContent? oldContent = entries[key];
entries[key] = content; // In the case that the content is unchanged, we want to avoid an overwrite
entryKinds[key] = assetKind; // as the isModified property may be reset to true,
if (oldContent is DevFSByteContent && content is DevFSByteContent &&
areEqual(oldContent.bytes, content.bytes)) {
return;
} }
entries[key] = content;
entryKinds[key] = assetKind;
} }
void _setLicenseIfChanged( void _setLicenseIfChanged(
...@@ -621,14 +652,14 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -621,14 +652,14 @@ class ManifestAssetBundle implements AssetBundle {
return deferredComponentsAssetVariants; return deferredComponentsAssetVariants;
} }
DevFSStringContent _createAssetManifest( Map<String, List<String>> _createAssetManifest(
Map<_Asset, List<_Asset>> assetVariants, Map<_Asset, List<_Asset>> assetVariants,
Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants
) { ) {
final Map<String, List<String>> jsonObject = <String, List<String>>{}; final Map<String, List<String>> manifest = <String, List<String>>{};
final Map<_Asset, List<String>> jsonEntries = <_Asset, List<String>>{}; final Map<_Asset, List<String>> entries = <_Asset, List<String>>{};
assetVariants.forEach((_Asset main, List<_Asset> variants) { assetVariants.forEach((_Asset main, List<_Asset> variants) {
jsonEntries[main] = <String>[ entries[main] = <String>[
for (final _Asset variant in variants) for (final _Asset variant in variants)
variant.entryUri.path, variant.entryUri.path,
]; ];
...@@ -636,26 +667,69 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -636,26 +667,69 @@ class ManifestAssetBundle implements AssetBundle {
if (deferredComponentsAssetVariants != null) { if (deferredComponentsAssetVariants != null) {
for (final Map<_Asset, List<_Asset>> componentAssets in deferredComponentsAssetVariants.values) { for (final Map<_Asset, List<_Asset>> componentAssets in deferredComponentsAssetVariants.values) {
componentAssets.forEach((_Asset main, List<_Asset> variants) { componentAssets.forEach((_Asset main, List<_Asset> variants) {
jsonEntries[main] = <String>[ entries[main] = <String>[
for (final _Asset variant in variants) for (final _Asset variant in variants)
variant.entryUri.path, variant.entryUri.path,
]; ];
}); });
} }
} }
final List<_Asset> sortedKeys = jsonEntries.keys.toList() final List<_Asset> sortedKeys = entries.keys.toList()
..sort((_Asset left, _Asset right) => left.entryUri.path.compareTo(right.entryUri.path)); ..sort((_Asset left, _Asset right) => left.entryUri.path.compareTo(right.entryUri.path));
for (final _Asset main in sortedKeys) { for (final _Asset main in sortedKeys) {
final String decodedEntryPath = Uri.decodeFull(main.entryUri.path); final String decodedEntryPath = Uri.decodeFull(main.entryUri.path);
final List<String> rawEntryVariantsPaths = jsonEntries[main]!; final List<String> rawEntryVariantsPaths = entries[main]!;
final List<String> decodedEntryVariantPaths = rawEntryVariantsPaths final List<String> decodedEntryVariantPaths = rawEntryVariantsPaths
.map((String value) => Uri.decodeFull(value)) .map((String value) => Uri.decodeFull(value))
.toList(); .toList();
jsonObject[decodedEntryPath] = decodedEntryVariantPaths; manifest[decodedEntryPath] = decodedEntryVariantPaths;
}
return manifest;
}
DevFSByteContent _createAssetManifestBinary(
Map<String, List<String>> assetManifest
) {
double parseScale(String key) {
final Uri assetUri = Uri.parse(key);
String directoryPath = '';
if (assetUri.pathSegments.length > 1) {
directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
}
final Match? match = _extractRatioRegExp.firstMatch(directoryPath);
if (match != null && match.groupCount > 0) {
return double.parse(match.group(1)!);
}
return _defaultResolution;
}
final Map<String, dynamic> result = <String, dynamic>{};
for (final MapEntry<String, dynamic> manifestEntry in assetManifest.entries) {
final List<dynamic> resultVariants = <dynamic>[];
final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>();
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 double variantDevicePixelRatio = parseScale(variant);
resultVariant['asset'] = variant;
resultVariant['dpr'] = variantDevicePixelRatio;
resultVariants.add(resultVariant);
}
result[manifestEntry.key] = resultVariants;
} }
return DevFSStringContent(json.encode(jsonObject));
final ByteData message = const StandardMessageCodec().encodeMessage(result)!;
return DevFSByteContent(message.buffer.asUint8List(0, message.lengthInBytes));
} }
static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
/// Prefixes family names and asset paths of fonts included from packages with /// Prefixes family names and asset paths of fonts included from packages with
/// 'packages/<package_name>' /// 'packages/<package_name>'
List<Font> _parsePackageFonts( List<Font> _parsePackageFonts(
......
...@@ -57,6 +57,8 @@ dependencies: ...@@ -57,6 +57,8 @@ dependencies:
vm_service: 9.4.0 vm_service: 9.4.0
standard_message_codec: 0.0.1+3
_fe_analyzer_shared: 50.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" _fe_analyzer_shared: 50.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
analyzer: 5.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" analyzer: 5.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
...@@ -88,7 +90,6 @@ dependencies: ...@@ -88,7 +90,6 @@ dependencies:
watcher: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
dev_dependencies: dev_dependencies:
collection: 1.17.0
file_testing: 3.0.0 file_testing: 3.0.0
pubspec_parse: 1.2.1 pubspec_parse: 1.2.1
...@@ -97,9 +98,10 @@ dev_dependencies: ...@@ -97,9 +98,10 @@ dev_dependencies:
json_annotation: 4.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" json_annotation: 4.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
node_preamble: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test: 1.22.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" test: 1.22.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
collection: 1.17.0
dartdoc: dartdoc:
# Exclude this package from the hosted API docs. # Exclude this package from the hosted API docs.
nodoc: true nodoc: true
# PUBSPEC CHECKSUM: 65eb # PUBSPEC CHECKSUM: 408d
...@@ -220,7 +220,7 @@ loading-units-spelled-wrong: ...@@ -220,7 +220,7 @@ loading-units-spelled-wrong:
expect(logger.statusText, contains('Errors checking the following files:')); expect(logger.statusText, contains('Errors checking the following files:'));
expect(logger.statusText, contains("Invalid loading units yaml file, 'loading-units' entry did not exist.")); expect(logger.statusText, contains("Invalid loading units yaml file, 'loading-units' entry did not exist."));
expect(logger.statusText.contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), false); expect(logger.statusText, isNot(contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n')));
}); });
testWithoutContext('loadingUnitCache validator detects malformed file: not a list', () async { testWithoutContext('loadingUnitCache validator detects malformed file: not a list', () async {
...@@ -382,7 +382,7 @@ loading-units: ...@@ -382,7 +382,7 @@ loading-units:
validator.displayResults(); validator.displayResults();
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Errors checking the following files:'), false); expect(logger.statusText, isNot(contains('Errors checking the following files:')));
}); });
testWithoutContext('androidStringMapping modifies strings file', () async { testWithoutContext('androidStringMapping modifies strings file', () async {
...@@ -448,9 +448,10 @@ loading-units: ...@@ -448,9 +448,10 @@ loading-units:
.childDirectory('main') .childDirectory('main')
.childFile('AndroidManifest.xml'); .childFile('AndroidManifest.xml');
expect(manifestOutput.existsSync(), true); expect(manifestOutput.existsSync(), true);
expect(manifestOutput.readAsStringSync().contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'), true); final String manifestOutputString = manifestOutput.readAsStringSync();
expect(manifestOutput.readAsStringSync().contains('android:value="invalidmapping"'), false); expect(manifestOutputString, contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'));
expect(manifestOutput.readAsStringSync().contains("<!-- Don't delete the meta-data below."), true); expect(manifestOutputString, isNot(contains('android:value="invalidmapping"')));
expect(manifestOutputString, contains("<!-- Don't delete the meta-data below."));
}); });
testWithoutContext('androidStringMapping adds mapping when no existing mapping', () async { testWithoutContext('androidStringMapping adds mapping when no existing mapping', () async {
...@@ -695,8 +696,8 @@ loading-units: ...@@ -695,8 +696,8 @@ loading-units:
.childDirectory('main') .childDirectory('main')
.childFile('AndroidManifest.xml'); .childFile('AndroidManifest.xml');
expect(manifestOutput.existsSync(), true); expect(manifestOutput.existsSync(), true);
expect(manifestOutput.readAsStringSync().contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'), true); expect(manifestOutput.readAsStringSync(), contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'));
expect(manifestOutput.readAsStringSync().contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"')), false); expect(manifestOutput.readAsStringSync(), isNot(contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"'))));
expect(manifestOutput.readAsStringSync().contains("<!-- Don't delete the meta-data below."), true); expect(manifestOutput.readAsStringSync(), contains("<!-- Don't delete the meta-data below."));
}); });
} }
...@@ -111,8 +111,9 @@ $fontsSection ...@@ -111,8 +111,9 @@ $fontsSection
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest expect(bundle.entries.keys, containsAll(
expect(bundle.entries.containsKey('FontManifest.json'), isTrue); <String>['AssetManifest.bin', 'AssetManifest.json', 'FontManifest.json', 'NOTICES.Z']
));
}, 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';
...@@ -11,6 +12,7 @@ import 'package:flutter_tools/src/asset.dart'; ...@@ -11,6 +12,7 @@ import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:standard_message_codec/standard_message_codec.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
...@@ -24,6 +26,7 @@ void main() { ...@@ -24,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) {
...@@ -60,37 +63,6 @@ $assetsSection ...@@ -60,37 +63,6 @@ $assetsSection
..writeAsStringSync(packages); ..writeAsStringSync(packages);
} }
Future<void> buildAndVerifyAssets(
List<String> assets,
List<String> packages,
String? expectedAssetManifest, {
bool expectExists = true,
}) async {
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
for (final String packageName in packages) {
for (final String asset in assets) {
final String entryKey = Uri.encodeFull('packages/$packageName/$asset');
expect(bundle.entries.containsKey(entryKey), expectExists,
reason: 'Cannot find key on bundle: $entryKey');
if (expectExists) {
expect(
utf8.decode(await bundle.entries[entryKey]!.contentsAsBytes()),
asset,
);
}
}
}
if (expectExists) {
expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedAssetManifest,
);
}
}
void writeAssets(String path, List<String> assets) { void writeAssets(String path, List<String> assets) {
for (final String asset in assets) { for (final String asset in assets) {
final String fullPath = fixPath(globals.fs.path.join(path, asset)); final String fullPath = fixPath(globals.fs.path.join(path, asset));
...@@ -101,182 +73,391 @@ $assetsSection ...@@ -101,182 +73,391 @@ $assetsSection
} }
} }
late FileSystem testFileSystem; // TODO(andrewkolos): Delete this group once we stop producing AssetManifest.json
// as part of build.
setUp(() async { group('Legacy asset manifest (AssetManifest.json)', () {
testFileSystem = MemoryFileSystem( Future<void> buildAndVerifyAssets(
style: globals.platform.isWindows List<String> assets,
? FileSystemStyle.windows List<String> packages,
: FileSystemStyle.posix, String? expectedAssetManifest, {
); bool expectExists = true,
testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); }) async {
});
group('AssetBundle assets from packages', () {
testUsingContext('No assets are bundled when the package has no assets', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
const String expectedAssetManifest = '{}';
expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedAssetManifest,
);
expect(
utf8.decode(await bundle.entries['FontManifest.json']!.contentsAsBytes()),
'[]',
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('No assets are bundled when the package has an asset that is not listed', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final List<String> assets = <String>['a/foo'];
writeAssets('p/p/', assets);
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
const String expectedAssetManifest = '{}';
expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedAssetManifest,
);
expect(
utf8.decode(await bundle.entries['FontManifest.json']!.contentsAsBytes()),
'[]',
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the package has and lists one '
'asset its pubspec', () async {
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);
const String expectedAssetManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the package has one asset, '
"listed in the app's pubspec", () async {
final List<String> assetEntries = <String>['packages/test_package/a/foo'];
writePubspecFile(
'pubspec.yaml',
'test',
assets: assetEntries,
);
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final List<String> assets = <String>['a/foo'];
writeAssets('p/p/lib/', assets);
const String expectedAssetManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset and its variant are bundled when the package ' for (final String packageName in packages) {
'has an asset and a variant, and lists the asset in its pubspec', () async { for (final String asset in assets) {
writePubspecFile('pubspec.yaml', 'test'); final String entryKey = Uri.encodeFull('packages/$packageName/$asset');
writePackagesFile('test_package:p/p/lib/'); expect(bundle.entries.containsKey(entryKey), expectExists,
writePubspecFile( reason: 'Cannot find key on bundle: $entryKey');
'p/p/pubspec.yaml', if (expectExists) {
'test_package', expect(
assets: <String>['a/foo', 'a/bar'], utf8.decode(await bundle.entries[entryKey]!.contentsAsBytes()),
); asset,
);
}
}
}
final List<String> assets = <String>['a/foo', 'a/2x/foo', 'a/bar']; if (expectExists) {
writeAssets('p/p/', assets); expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedAssetManifest,
);
}
}
const String expectedManifest = '{' late FileSystem testFileSystem;
'"packages/test_package/a/bar":'
'["packages/test_package/a/bar"],'
'"packages/test_package/a/foo":'
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"]'
'}';
await buildAndVerifyAssets( setUp(() async {
assets, testFileSystem = MemoryFileSystem(
<String>['test_package'], style: globals.platform.isWindows
expectedManifest, ? FileSystemStyle.windows
: FileSystemStyle.posix,
); );
}, overrides: <Type, Generator>{ testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('One asset and its variant are bundled when the package ' group('AssetBundle assets from packages', () {
'has an asset and a variant, and the app lists the asset in its pubspec', () async { testUsingContext('No assets are bundled when the package has no assets', () async {
writePubspecFile( writePubspecFile('pubspec.yaml', 'test');
'pubspec.yaml', writePackagesFile('test_package:p/p/lib/');
'test', writePubspecFile('p/p/pubspec.yaml', 'test_package');
assets: <String>['packages/test_package/a/foo'],
); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
writePackagesFile('test_package:p/p/lib/'); await bundle.build(packagesPath: '.packages');
writePubspecFile( expect(bundle.entries.keys, unorderedEquals(
'p/p/pubspec.yaml', <String>['AssetManifest.bin', 'AssetManifest.json', 'FontManifest.json', 'NOTICES.Z']
'test_package', ));
); const String expectedAssetManifest = '{}';
expect(
final List<String> assets = <String>['a/foo', 'a/2x/foo']; utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
writeAssets('p/p/lib/', assets); expectedAssetManifest,
);
const String expectedManifest = '{"packages/test_package/a/foo":' expect(
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"]}'; utf8.decode(await bundle.entries['FontManifest.json']!.contentsAsBytes()),
'[]',
await buildAndVerifyAssets( );
assets, }, overrides: <Type, Generator>{
<String>['test_package'], FileSystem: () => testFileSystem,
expectedManifest, ProcessManager: () => FakeProcessManager.any(),
); });
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, testUsingContext('No assets are bundled when the package has an asset that is not listed', () async {
ProcessManager: () => FakeProcessManager.any(), writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final List<String> assets = <String>['a/foo'];
writeAssets('p/p/', assets);
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(
<String>['AssetManifest.bin', 'AssetManifest.json', 'FontManifest.json', 'NOTICES.Z']
));
const String expectedAssetManifest = '{}';
expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedAssetManifest,
);
expect(
utf8.decode(await bundle.entries['FontManifest.json']!.contentsAsBytes()),
'[]',
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the package has and lists one '
'asset its pubspec', () async {
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);
const String expectedAssetManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the package has one asset, '
"listed in the app's pubspec", () async {
final List<String> assetEntries = <String>['packages/test_package/a/foo'];
writePubspecFile(
'pubspec.yaml',
'test',
assets: assetEntries,
);
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final List<String> assets = <String>['a/foo'];
writeAssets('p/p/lib/', assets);
const String expectedAssetManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset and its variant are bundled when the package '
'has an asset and a variant, and lists the asset in its pubspec', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: <String>['a/foo', 'a/bar'],
);
final List<String> assets = <String>['a/foo', 'a/2x/foo', 'a/bar'];
writeAssets('p/p/', assets);
const String expectedManifest = '{'
'"packages/test_package/a/bar":'
'["packages/test_package/a/bar"],'
'"packages/test_package/a/foo":'
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"]'
'}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset and its variant are bundled when the package '
'has an asset and a variant, and the app lists the asset in its pubspec', () async {
writePubspecFile(
'pubspec.yaml',
'test',
assets: <String>['packages/test_package/a/foo'],
);
writePackagesFile('test_package:p/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
);
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p/p/lib/', assets);
const String expectedManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Two assets are bundled when the package has and lists '
'two assets in its pubspec', () async {
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);
const String expectedAssetManifest =
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext("Two assets are bundled when the package has two assets, listed in the app's pubspec", () async {
final List<String> assetEntries = <String>[
'packages/test_package/a/foo',
'packages/test_package/a/bar',
];
writePubspecFile(
'pubspec.yaml',
'test',
assets: assetEntries,
);
writePackagesFile('test_package:p/p/lib/');
final List<String> assets = <String>['a/foo', 'a/bar'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
);
writeAssets('p/p/lib/', assets);
const String expectedAssetManifest =
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Two assets are bundled when two packages each have and list an asset their pubspec', () async {
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/2x/foo'];
writeAssets('p/p/', assets);
writeAssets('p2/p/', assets);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":'
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"],'
'"packages/test_package2/a/foo":'
'["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package', 'test_package2'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext("Two assets are bundled when two packages each have an asset, listed in the app's pubspec", () async {
final List<String> assetEntries = <String>[
'packages/test_package/a/foo',
'packages/test_package2/a/foo',
];
writePubspecFile(
'pubspec.yaml',
'test',
assets: assetEntries,
);
writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
);
writePubspecFile(
'p2/p/pubspec.yaml',
'test_package2',
);
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p/p/lib/', assets);
writeAssets('p2/p/lib/', assets);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":'
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"],'
'"packages/test_package2/a/foo":'
'["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package', 'test_package2'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the app depends on a package, '
'listing in its pubspec an asset from another package', () async {
writePubspecFile(
'pubspec.yaml',
'test',
);
writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: <String>['packages/test_package2/a/foo'],
);
writePubspecFile(
'p2/p/pubspec.yaml',
'test_package2',
);
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p2/p/lib/', assets);
const String expectedAssetManifest =
'{"packages/test_package2/a/foo":'
'["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package2'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
}); });
testUsingContext('Two assets are bundled when the package has and lists ' testUsingContext('Asset paths can contain URL reserved characters', () async {
'two assets in its pubspec', () async {
writePubspecFile('pubspec.yaml', 'test'); writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/'); writePackagesFile('test_package:p/p/lib/');
final List<String> assets = <String>['a/foo', 'a/bar']; final List<String> assets = <String>['a/foo', 'a/foo [x]'];
writePubspecFile( writePubspecFile(
'p/p/pubspec.yaml', 'p/p/pubspec.yaml',
'test_package', 'test_package',
...@@ -285,8 +466,8 @@ $assetsSection ...@@ -285,8 +466,8 @@ $assetsSection
writeAssets('p/p/', assets); writeAssets('p/p/', assets);
const String expectedAssetManifest = const String expectedAssetManifest =
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],' '{"packages/test_package/a/foo":["packages/test_package/a/foo"],'
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}'; '"packages/test_package/a/foo [x]":["packages/test_package/a/foo [x]"]}';
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
...@@ -295,283 +476,584 @@ $assetsSection ...@@ -295,283 +476,584 @@ $assetsSection
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext("Two assets are bundled when the package has two assets, listed in the app's pubspec", () async { group('AssetBundle assets from scanned paths', () {
final List<String> assetEntries = <String>[ testUsingContext('Two assets are bundled when scanning their directory', () async {
'packages/test_package/a/foo', writePubspecFile('pubspec.yaml', 'test');
'packages/test_package/a/bar', writePackagesFile('test_package:p/p/lib/');
];
writePubspecFile( final List<String> assetsOnDisk = <String>['a/foo', 'a/bar'];
'pubspec.yaml', final List<String> assetsOnManifest = <String>['a/'];
'test',
assets: assetEntries, writePubspecFile(
); 'p/p/pubspec.yaml',
writePackagesFile('test_package:p/p/lib/'); 'test_package',
assets: assetsOnManifest,
final List<String> assets = <String>['a/foo', 'a/bar']; );
writePubspecFile(
'p/p/pubspec.yaml', writeAssets('p/p/', assetsOnDisk);
'test_package', const String expectedAssetManifest =
); '{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
writeAssets('p/p/lib/', assets);
const String expectedAssetManifest = await buildAndVerifyAssets(
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],' assetsOnDisk,
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}'; <String>['test_package'],
expectedAssetManifest,
await buildAndVerifyAssets( );
assets, }, overrides: <Type, Generator>{
<String>['test_package'], FileSystem: () => testFileSystem,
expectedAssetManifest, ProcessManager: () => FakeProcessManager.any(),
); });
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, testUsingContext('Two assets are bundled when listing one and scanning second directory', () async {
ProcessManager: () => FakeProcessManager.any(), writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo', 'abc/bar'];
final List<String> assetOnManifest = <String>['a/foo', 'abc/'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":["packages/test_package/a/foo"],'
'"packages/test_package/abc/bar":["packages/test_package/abc/bar"]}';
await buildAndVerifyAssets(
assetsOnDisk,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled with variant, scanning wrong directory', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo','a/b/foo','a/bar'];
final List<String> assetOnManifest = <String>['a','a/bar']; // can't list 'a' as asset, should be 'a/'
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries['AssetManifest.bin'], isNull,
reason: 'Invalid pubspec.yaml should not generate AssetManifest.bin' );
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
}); });
testUsingContext('Two assets are bundled when two packages each have and list an asset their pubspec', () async { group('AssetBundle assets from scanned paths with MemoryFileSystem', () {
writePubspecFile( testUsingContext('One asset is bundled with variant, scanning directory', () async {
'pubspec.yaml', writePubspecFile('pubspec.yaml', 'test');
'test', writePackagesFile('test_package:p/p/lib/');
);
writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/'); final List<String> assetsOnDisk = <String>['a/foo','a/2x/foo'];
writePubspecFile( final List<String> assetOnManifest = <String>['a/',];
'p/p/pubspec.yaml',
'test_package', writePubspecFile(
assets: <String>['a/foo'], 'p/p/pubspec.yaml',
); 'test_package',
writePubspecFile( assets: assetOnManifest,
'p2/p/pubspec.yaml', );
'test_package2',
assets: <String>['a/foo'], writeAssets('p/p/', assetsOnDisk);
); const String expectedAssetManifest =
'{"packages/test_package/a/foo":["packages/test_package/a/foo","packages/test_package/a/2x/foo"]}';
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p/p/', assets); await buildAndVerifyAssets(
writeAssets('p2/p/', assets); assetsOnDisk,
<String>['test_package'],
const String expectedAssetManifest = expectedAssetManifest,
'{"packages/test_package/a/foo":' );
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"],' }, overrides: <Type, Generator>{
'"packages/test_package2/a/foo":' FileSystem: () => testFileSystem,
'["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}'; ProcessManager: () => FakeProcessManager.any(),
});
await buildAndVerifyAssets(
assets, testUsingContext('No asset is bundled with variant, no assets or directories are listed', () async {
<String>['test_package', 'test_package2'], writePubspecFile('pubspec.yaml', 'test');
expectedAssetManifest, writePackagesFile('test_package:p/p/lib/');
);
}, overrides: <Type, Generator>{ final List<String> assetsOnDisk = <String>['a/foo', 'a/2x/foo'];
FileSystem: () => testFileSystem, final List<String> assetOnManifest = <String>[];
ProcessManager: () => FakeProcessManager.any(),
}); writePubspecFile(
'p/p/pubspec.yaml',
testUsingContext("Two assets are bundled when two packages each have an asset, listed in the app's pubspec", () async { 'test_package',
final List<String> assetEntries = <String>[ assets: assetOnManifest,
'packages/test_package/a/foo', );
'packages/test_package2/a/foo',
]; writeAssets('p/p/', assetsOnDisk);
writePubspecFile( const String expectedAssetManifest = '{}';
'pubspec.yaml',
'test', await buildAndVerifyAssets(
assets: assetEntries, assetOnManifest,
); <String>['test_package'],
writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/'); expectedAssetManifest,
writePubspecFile( );
'p/p/pubspec.yaml', }, overrides: <Type, Generator>{
'test_package', FileSystem: () => testFileSystem,
); ProcessManager: () => FakeProcessManager.any(),
writePubspecFile( });
'p2/p/pubspec.yaml',
'test_package2', testUsingContext('Expect error generating manifest, wrong non-existing directory is listed', () async {
); writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p/p/lib/', assets); final List<String> assetOnManifest = <String>['c/'];
writeAssets('p2/p/lib/', assets);
writePubspecFile(
const String expectedAssetManifest = 'p/p/pubspec.yaml',
'{"packages/test_package/a/foo":' 'test_package',
'["packages/test_package/a/foo","packages/test_package/a/2x/foo"],' assets: assetOnManifest,
'"packages/test_package2/a/foo":' );
'["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}';
await buildAndVerifyAssets(
await buildAndVerifyAssets( assetOnManifest,
assets, <String>['test_package'],
<String>['test_package', 'test_package2'], null,
expectedAssetManifest, expectExists: false,
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('One asset is bundled when the app depends on a package, '
'listing in its pubspec an asset from another package', () async {
writePubspecFile(
'pubspec.yaml',
'test',
);
writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: <String>['packages/test_package2/a/foo'],
);
writePubspecFile(
'p2/p/pubspec.yaml',
'test_package2',
);
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p2/p/lib/', assets);
const String expectedAssetManifest =
'{"packages/test_package2/a/foo":'
'["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package2'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
}); });
}); });
group('Current asset manifest (AssetManifest.bin)', () {
Future<String> extractAssetManifestFromBundleAsJson(AssetBundle bundle) async {
final List<int> manifestBytes = await bundle.entries['AssetManifest.bin']!.contentsAsBytes();
return json.encode(const StandardMessageCodec().decodeMessage(
ByteData.sublistView(Uint8List.fromList(manifestBytes))
));
}
testUsingContext('Asset paths can contain URL reserved characters', () async { Future<void> buildAndVerifyAssets(
writePubspecFile('pubspec.yaml', 'test'); List<String> assets,
writePackagesFile('test_package:p/p/lib/'); List<String> packages,
String? expectedAssetManifest, {
final List<String> assets = <String>['a/foo', 'a/foo [x]']; bool expectExists = true,
writePubspecFile( }) async {
'p/p/pubspec.yaml', final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
'test_package', await bundle.build(packagesPath: '.packages');
assets: assets,
);
writeAssets('p/p/', assets);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":["packages/test_package/a/foo"],'
'"packages/test_package/a/foo [x]":["packages/test_package/a/foo [x]"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
group('AssetBundle assets from scanned paths', () {
testUsingContext('Two assets are bundled when scanning their directory', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo', 'a/bar'];
final List<String> assetsOnManifest = <String>['a/'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetsOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
await buildAndVerifyAssets( for (final String packageName in packages) {
assetsOnDisk, for (final String asset in assets) {
<String>['test_package'], final String entryKey = Uri.encodeFull('packages/$packageName/$asset');
expectedAssetManifest, expect(bundle.entries.containsKey(entryKey), expectExists,
); reason: 'Cannot find key on bundle: $entryKey');
}, overrides: <Type, Generator>{ if (expectExists) {
FileSystem: () => testFileSystem, expect(
ProcessManager: () => FakeProcessManager.any(), utf8.decode(await bundle.entries[entryKey]!.contentsAsBytes()),
}); asset,
);
}
}
}
testUsingContext('Two assets are bundled when listing one and scanning second directory', () async { if (expectExists) {
writePubspecFile('pubspec.yaml', 'test'); final String actualAssetManifest = await extractAssetManifestFromBundleAsJson(bundle);
writePackagesFile('test_package:p/p/lib/'); expect(
actualAssetManifest,
expectedAssetManifest,
);
}
}
final List<String> assetsOnDisk = <String>['a/foo', 'abc/bar']; void writeAssets(String path, List<String> assets) {
final List<String> assetOnManifest = <String>['a/foo', 'abc/']; for (final String asset in assets) {
final String fullPath = fixPath(globals.fs.path.join(path, asset));
writePubspecFile( globals.fs.file(fullPath)
'p/p/pubspec.yaml', ..createSync(recursive: true)
'test_package', ..writeAsStringSync(asset);
assets: assetOnManifest, }
); }
writeAssets('p/p/', assetsOnDisk); late FileSystem testFileSystem;
const String expectedAssetManifest =
'{"packages/test_package/a/foo":["packages/test_package/a/foo"],'
'"packages/test_package/abc/bar":["packages/test_package/abc/bar"]}';
await buildAndVerifyAssets( setUp(() async {
assetsOnDisk, testFileSystem = MemoryFileSystem(
<String>['test_package'], style: globals.platform.isWindows
expectedAssetManifest, ? FileSystemStyle.windows
: FileSystemStyle.posix,
); );
}, overrides: <Type, Generator>{ testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('One asset is bundled with variant, scanning wrong directory', () async { group('AssetBundle assets from packages', () {
writePubspecFile('pubspec.yaml', 'test'); testUsingContext('No assets are bundled when the package has no assets', () async {
writePackagesFile('test_package:p/p/lib/'); writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo','a/b/foo','a/bar']; writePubspecFile('p/p/pubspec.yaml', 'test_package');
final List<String> assetOnManifest = <String>['a','a/bar']; // can't list 'a' as asset, should be 'a/'
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
writePubspecFile( await bundle.build(packagesPath: '.packages');
'p/p/pubspec.yaml', expect(bundle.entries.keys, unorderedEquals(
'test_package', <String>['AssetManifest.bin', 'AssetManifest.json', 'FontManifest.json', 'NOTICES.Z']
assets: assetOnManifest, ));
); final String actualAssetManifest = await extractAssetManifestFromBundleAsJson(bundle);
const String expectedAssetManifest = '{}';
writeAssets('p/p/', assetsOnDisk); expect(
actualAssetManifest,
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); expectedAssetManifest,
await bundle.build(packagesPath: '.packages'); );
expect(
expect(bundle.entries['AssetManifest.json'], isNull, utf8.decode(await bundle.entries['FontManifest.json']!.contentsAsBytes()),
reason: 'Invalid pubspec.yaml should not generate AssetManifest.json' ); '[]',
}, overrides: <Type, Generator>{ );
FileSystem: () => testFileSystem, }, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(), FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('No assets are bundled when the package has an asset that is not listed', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final List<String> assets = <String>['a/foo'];
writeAssets('p/p/', assets);
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(
<String>['AssetManifest.bin', 'AssetManifest.json', 'FontManifest.json', 'NOTICES.Z']
));
final String actualAssetManifest = await extractAssetManifestFromBundleAsJson(bundle);
const String expectedAssetManifest = '{}';
expect(
actualAssetManifest,
expectedAssetManifest,
);
expect(
utf8.decode(await bundle.entries['FontManifest.json']!.contentsAsBytes()),
'[]',
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the package has and lists one '
'asset its pubspec', () async {
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);
const String expectedAssetManifest = '{"packages/test_package/a/foo":'
'[]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the package has one asset, '
"listed in the app's pubspec", () async {
final List<String> assetEntries = <String>['packages/test_package/a/foo'];
writePubspecFile(
'pubspec.yaml',
'test',
assets: assetEntries,
);
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final List<String> assets = <String>['a/foo'];
writeAssets('p/p/lib/', assets);
const String expectedAssetManifest = '{"packages/test_package/a/foo":[]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset and its variant are bundled when the package '
'has an asset and a variant, and lists the asset in its pubspec', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: <String>['a/foo', 'a/bar'],
);
final List<String> assets = <String>['a/foo', 'a/2x/foo', 'a/bar'];
writeAssets('p/p/', assets);
const String expectedManifest = '{'
'"packages/test_package/a/bar":[],'
'"packages/test_package/a/foo":['
'{"asset":"packages/test_package/a/2x/foo","dpr":2.0}]'
'}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset and its variant are bundled when the package '
'has an asset and a variant, and the app lists the asset in its pubspec', () async {
writePubspecFile(
'pubspec.yaml',
'test',
assets: <String>['packages/test_package/a/foo'],
);
writePackagesFile('test_package:p/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
);
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p/p/lib/', assets);
const String expectedManifest = '{"packages/test_package/a/foo":'
'[{"asset":"packages/test_package/a/2x/foo","dpr":2.0}]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Two assets are bundled when the package has and lists '
'two assets in its pubspec', () async {
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);
const String expectedAssetManifest =
'{"packages/test_package/a/bar":[],'
'"packages/test_package/a/foo":[]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext("Two assets are bundled when the package has two assets, listed in the app's pubspec", () async {
final List<String> assetEntries = <String>[
'packages/test_package/a/foo',
'packages/test_package/a/bar',
];
writePubspecFile(
'pubspec.yaml',
'test',
assets: assetEntries,
);
writePackagesFile('test_package:p/p/lib/');
final List<String> assets = <String>['a/foo', 'a/bar'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
);
writeAssets('p/p/lib/', assets);
const String expectedAssetManifest =
'{"packages/test_package/a/bar":[],'
'"packages/test_package/a/foo":[]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Two assets are bundled when two packages each have and list an asset their pubspec', () async {
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/2x/foo'];
writeAssets('p/p/', assets);
writeAssets('p2/p/', assets);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":'
'[{"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(
assets,
<String>['test_package', 'test_package2'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext("Two assets are bundled when two packages each have an asset, listed in the app's pubspec", () async {
final List<String> assetEntries = <String>[
'packages/test_package/a/foo',
'packages/test_package2/a/foo',
];
writePubspecFile(
'pubspec.yaml',
'test',
assets: assetEntries,
);
writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
);
writePubspecFile(
'p2/p/pubspec.yaml',
'test_package2',
);
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p/p/lib/', assets);
writeAssets('p2/p/lib/', assets);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":'
'[{"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(
assets,
<String>['test_package', 'test_package2'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the app depends on a package, '
'listing in its pubspec an asset from another package', () async {
writePubspecFile(
'pubspec.yaml',
'test',
);
writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: <String>['packages/test_package2/a/foo'],
);
writePubspecFile(
'p2/p/pubspec.yaml',
'test_package2',
);
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
writeAssets('p2/p/lib/', assets);
const String expectedAssetManifest =
'{"packages/test_package2/a/foo":'
'[{"asset":"packages/test_package2/a/2x/foo","dpr":2.0}]}';
await buildAndVerifyAssets(
assets,
<String>['test_package2'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
}); });
});
group('AssetBundle assets from scanned paths with MemoryFileSystem', () { testUsingContext('Asset paths can contain URL reserved characters', () async {
testUsingContext('One asset is bundled with variant, scanning directory', () async {
writePubspecFile('pubspec.yaml', 'test'); writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/'); writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo','a/2x/foo']; final List<String> assets = <String>['a/foo', 'a/foo [x]'];
final List<String> assetOnManifest = <String>['a/',];
writePubspecFile( writePubspecFile(
'p/p/pubspec.yaml', 'p/p/pubspec.yaml',
'test_package', 'test_package',
assets: assetOnManifest, assets: assets,
); );
writeAssets('p/p/', assetsOnDisk); writeAssets('p/p/', assets);
const String expectedAssetManifest = const String expectedAssetManifest =
'{"packages/test_package/a/foo":["packages/test_package/a/foo","packages/test_package/a/2x/foo"]}'; '{"packages/test_package/a/foo":[],'
'"packages/test_package/a/foo [x]":[]}';
await buildAndVerifyAssets( await buildAndVerifyAssets(
assetsOnDisk, assets,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest,
); );
...@@ -580,53 +1062,165 @@ $assetsSection ...@@ -580,53 +1062,165 @@ $assetsSection
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('No asset is bundled with variant, no assets or directories are listed', () async { group('AssetBundle assets from scanned paths', () {
writePubspecFile('pubspec.yaml', 'test'); testUsingContext('Two assets are bundled when scanning their directory', () async {
writePackagesFile('test_package:p/p/lib/'); writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo', 'a/2x/foo'];
final List<String> assetOnManifest = <String>[]; final List<String> assetsOnDisk = <String>['a/foo', 'a/bar'];
final List<String> assetsOnManifest = <String>['a/'];
writePubspecFile(
'p/p/pubspec.yaml', writePubspecFile(
'test_package', 'p/p/pubspec.yaml',
assets: assetOnManifest, 'test_package',
); assets: assetsOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest = '{}'; writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
await buildAndVerifyAssets( '{"packages/test_package/a/bar":[],'
assetOnManifest, '"packages/test_package/a/foo":[]}';
<String>['test_package'],
expectedAssetManifest, await buildAndVerifyAssets(
); assetsOnDisk,
}, overrides: <Type, Generator>{ <String>['test_package'],
FileSystem: () => testFileSystem, expectedAssetManifest,
ProcessManager: () => FakeProcessManager.any(), );
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Two assets are bundled when listing one and scanning second directory', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo', 'abc/bar'];
final List<String> assetOnManifest = <String>['a/foo', 'abc/'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":[],'
'"packages/test_package/abc/bar":[]}';
await buildAndVerifyAssets(
assetsOnDisk,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled with variant, scanning wrong directory', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo','a/b/foo','a/bar'];
final List<String> assetOnManifest = <String>['a','a/bar']; // can't list 'a' as asset, should be 'a/'
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries['AssetManifest.bin'], isNull,
reason: 'Invalid pubspec.yaml should not generate AssetManifest.bin' );
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
}); });
testUsingContext('Expect error generating manifest, wrong non-existing directory is listed', () async { group('AssetBundle assets from scanned paths with MemoryFileSystem', () {
writePubspecFile('pubspec.yaml', 'test'); testUsingContext('One asset is bundled with variant, scanning directory', () async {
writePackagesFile('test_package:p/p/lib/'); writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetOnManifest = <String>['c/'];
final List<String> assetsOnDisk = <String>['a/foo','a/2x/foo'];
writePubspecFile( final List<String> assetOnManifest = <String>['a/',];
'p/p/pubspec.yaml',
'test_package', writePubspecFile(
assets: assetOnManifest, 'p/p/pubspec.yaml',
); 'test_package',
assets: assetOnManifest,
await buildAndVerifyAssets( );
assetOnManifest,
<String>['test_package'], writeAssets('p/p/', assetsOnDisk);
null, const String expectedAssetManifest =
expectExists: false, '{"packages/test_package/a/foo":[{"asset":"packages/test_package/a/2x/foo","dpr":2.0}]}';
);
}, overrides: <Type, Generator>{ await buildAndVerifyAssets(
FileSystem: () => testFileSystem, assetsOnDisk,
ProcessManager: () => FakeProcessManager.any(), <String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('No asset is bundled with variant, no assets or directories are listed', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo', 'a/2x/foo'];
final List<String> assetOnManifest = <String>[];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest = '{}';
await buildAndVerifyAssets(
assetOnManifest,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Expect error generating manifest, wrong non-existing directory is listed', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetOnManifest = <String>['c/'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
await buildAndVerifyAssets(
assetOnManifest,
<String>['test_package'],
null,
expectExists: false,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
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/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
...@@ -13,6 +14,7 @@ import 'package:flutter_tools/src/build_info.dart'; ...@@ -13,6 +14,7 @@ import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/bundle_builder.dart'; import 'package:flutter_tools/src/bundle_builder.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:standard_message_codec/standard_message_codec.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
...@@ -48,11 +50,16 @@ void main() { ...@@ -48,11 +50,16 @@ void main() {
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
expect(bundle.entries.length, 1); expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json', 'AssetManifest.bin']));
const String expectedAssetManifest = '{}'; const String expectedJsonAssetManifest = '{}';
expect( expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()), utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedAssetManifest, expectedJsonAssetManifest,
);
const String expectedBinAssetManifest = '{}';
expect(
await _extractBinAssetManifestFromBundleAsJson(bundle),
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -72,12 +79,8 @@ flutter: ...@@ -72,12 +79,8 @@ flutter:
'''); ''');
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
// Expected assets: expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
// - asset manifest 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
expect(bundle.needsBuild(), false); expect(bundle.needsBuild(), false);
// Simulate modifying the files by updating the filestat time manually. // Simulate modifying the files by updating the filestat time manually.
...@@ -87,13 +90,9 @@ flutter: ...@@ -87,13 +90,9 @@ flutter:
expect(bundle.needsBuild(), true); expect(bundle.needsBuild(), true);
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
// Expected assets: expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
// - asset manifest 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt',
// - font manifest 'assets/foo/fizz.txt']));
// - license file
// - assets/foo/bar.txt
// - assets/foo/fizz.txt
expect(bundle.entries.length, 5);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
...@@ -112,12 +111,8 @@ flutter: ...@@ -112,12 +111,8 @@ flutter:
globals.fs.file('.packages').createSync(); globals.fs.file('.packages').createSync();
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
// Expected assets: expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
// - asset manifest 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
expect(bundle.needsBuild(), false); expect(bundle.needsBuild(), false);
// Delete the wildcard directory and update pubspec file. // Delete the wildcard directory and update pubspec file.
...@@ -138,12 +133,8 @@ name: example''') ...@@ -138,12 +133,8 @@ name: example''')
// supporting file deletion. // supporting file deletion.
expect(bundle.needsBuild(), true); expect(bundle.needsBuild(), true);
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
// Expected assets: expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
// - asset manifest 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
...@@ -166,12 +157,8 @@ flutter: ...@@ -166,12 +157,8 @@ flutter:
globals.fs.file('.packages').createSync(); globals.fs.file('.packages').createSync();
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
// Expected assets: expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
// - asset manifest 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
expect(bundle.needsBuild(), false); expect(bundle.needsBuild(), false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -203,12 +190,8 @@ flutter: ...@@ -203,12 +190,8 @@ flutter:
splitDeferredAssets: true, splitDeferredAssets: true,
).createBundle(); ).createBundle();
await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true); await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true);
// Expected assets: expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
// - asset manifest 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
expect(bundle.deferredComponentsEntries.length, 1); expect(bundle.deferredComponentsEntries.length, 1);
expect(bundle.deferredComponentsEntries['component1']!.length, 2); expect(bundle.deferredComponentsEntries['component1']!.length, 2);
expect(bundle.needsBuild(), false); expect(bundle.needsBuild(), false);
...@@ -237,12 +220,9 @@ flutter: ...@@ -237,12 +220,9 @@ flutter:
'''); ''');
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
// Expected assets: expect(bundle.entries.keys, unorderedEquals(<String>['assets/foo/bar.txt',
// - asset manifest 'assets/bar/barbie.txt', 'assets/wild/dash.txt', 'AssetManifest.json',
// - font manifest 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 6);
expect(bundle.deferredComponentsEntries.isEmpty, true); expect(bundle.deferredComponentsEntries.isEmpty, true);
expect(bundle.needsBuild(), false); expect(bundle.needsBuild(), false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -275,14 +255,11 @@ flutter: ...@@ -275,14 +255,11 @@ flutter:
splitDeferredAssets: true, splitDeferredAssets: true,
).createBundle(); ).createBundle();
await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true); await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true);
// Expected assets: expect(bundle.entries.keys, unorderedEquals(<String>['assets/foo/bar.txt',
// - asset manifest 'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
// - font manifest expect(bundle.deferredComponentsEntries.keys, unorderedEquals(<String>['component1']));
// - license file expect(bundle.deferredComponentsEntries['component1']!.keys,
// - assets/foo/bar.txt unorderedEquals(<String>['assets/bar/barbie.txt', 'assets/wild/dash.txt']));
expect(bundle.entries.length, 4);
expect(bundle.deferredComponentsEntries.length, 1);
expect(bundle.deferredComponentsEntries['component1']!.length, 2);
expect(bundle.needsBuild(), false); expect(bundle.needsBuild(), false);
// Simulate modifying the files by updating the filestat time manually. // Simulate modifying the files by updating the filestat time manually.
...@@ -293,9 +270,13 @@ flutter: ...@@ -293,9 +270,13 @@ flutter:
expect(bundle.needsBuild(), true); expect(bundle.needsBuild(), true);
await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true); await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true);
expect(bundle.entries.length, 4); expect(bundle.entries.keys, unorderedEquals(<String>['assets/foo/bar.txt',
'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
expect(bundle.deferredComponentsEntries.length, 1); expect(bundle.deferredComponentsEntries.length, 1);
expect(bundle.deferredComponentsEntries['component1']!.length, 3); expect(bundle.deferredComponentsEntries['component1']!.keys,
unorderedEquals(<String>['assets/bar/barbie.txt', 'assets/wild/dash.txt',
'assets/wild/fizz.txt']));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
...@@ -335,7 +316,8 @@ assets: ...@@ -335,7 +316,8 @@ assets:
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
final DevFSStringContent? assetManifest = bundle.entries['AssetManifest.json'] final DevFSContent? assetManifestBin = bundle.entries['AssetManifest.bin'];
final DevFSStringContent? assetManifestJson = bundle.entries['AssetManifest.json']
as DevFSStringContent?; as DevFSStringContent?;
final DevFSStringContent? fontManifest = bundle.entries['FontManifest.json'] final DevFSStringContent? fontManifest = bundle.entries['FontManifest.json']
as DevFSStringContent?; as DevFSStringContent?;
...@@ -344,7 +326,8 @@ assets: ...@@ -344,7 +326,8 @@ assets:
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
expect(assetManifest, bundle.entries['AssetManifest.json']); expect(assetManifestBin, bundle.entries['AssetManifest.bin']);
expect(assetManifestJson, bundle.entries['AssetManifest.json']);
expect(fontManifest, bundle.entries['FontManifest.json']); expect(fontManifest, bundle.entries['FontManifest.json']);
expect(license, bundle.entries['NOTICES']); expect(license, bundle.entries['NOTICES']);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -639,7 +622,8 @@ flutter: ...@@ -639,7 +622,8 @@ flutter:
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
expect(bundle.entries, hasLength(4)); expect(bundle.entries.keys, unorderedEquals(<String>['packages/foo/bar/fizz.txt',
'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
expect(bundle.needsBuild(), false); expect(bundle.needsBuild(), false);
// Does not track dependency's wildcard directories. // Does not track dependency's wildcard directories.
...@@ -739,6 +723,7 @@ flutter: ...@@ -739,6 +723,7 @@ flutter:
expect(await bundle.build(packagesPath: '.packages'), 0); expect(await bundle.build(packagesPath: '.packages'), 0);
expect((bundle.entries['FontManifest.json']! as DevFSStringContent).string, '[]'); expect((bundle.entries['FontManifest.json']! as DevFSStringContent).string, '[]');
expect((bundle.entries['AssetManifest.json']! as DevFSStringContent).string, '{}'); expect((bundle.entries['AssetManifest.json']! as DevFSStringContent).string, '{}');
expect(await _extractBinAssetManifestFromBundleAsJson(bundle), '{}');
expect(testLogger.errorText, contains( expect(testLogger.errorText, contains(
'package:foo has `uses-material-design: true` set' 'package:foo has `uses-material-design: true` set'
)); ));
...@@ -774,7 +759,8 @@ flutter: ...@@ -774,7 +759,8 @@ flutter:
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
expect(await bundle.build(packagesPath: '.packages'), 0); expect(await bundle.build(packagesPath: '.packages'), 0);
expect(bundle.entries.length, 4); expect(bundle.entries.keys, unorderedEquals(<String>['assets/foo.txt',
'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(), FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
...@@ -812,9 +798,17 @@ flutter: ...@@ -812,9 +798,17 @@ flutter:
// The assets from deferred components and regular assets // The assets from deferred components and regular assets
// are both included in alphabetical order // are both included in alphabetical order
expect((bundle.entries['AssetManifest.json']! as DevFSStringContent).string, '{"assets/apple.jpg":["assets/apple.jpg"],"assets/bar.jpg":["assets/bar.jpg"],"assets/foo.jpg":["assets/foo.jpg"],"assets/zebra.jpg":["assets/zebra.jpg"]}'); expect((bundle.entries['AssetManifest.json']! as DevFSStringContent).string, '{"assets/apple.jpg":["assets/apple.jpg"],"assets/bar.jpg":["assets/bar.jpg"],"assets/foo.jpg":["assets/foo.jpg"],"assets/zebra.jpg":["assets/zebra.jpg"]}');
expect(await _extractBinAssetManifestFromBundleAsJson(bundle), '{"assets/apple.jpg":[],"assets/bar.jpg":[],"assets/foo.jpg":[],"assets/zebra.jpg":[]}');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(), FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
Platform: () => FakePlatform(), Platform: () => FakePlatform(),
}); });
} }
Future<String> _extractBinAssetManifestFromBundleAsJson(AssetBundle bundle) async {
final List<int> manifestBytes = await bundle.entries['AssetManifest.bin']!.contentsAsBytes();
return json.encode(const StandardMessageCodec().decodeMessage(
ByteData.sublistView(Uint8List.fromList(manifestBytes))
));
}
...@@ -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,209 +16,418 @@ import 'package:flutter_tools/src/base/user_messages.dart'; ...@@ -15,209 +16,418 @@ 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 { // TODO(andrewkolos): Delete this group once we stop producing AssetManifest.json
final String manifestJson = utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()); // as part of build.
final Map<String, dynamic> parsedJson = json.decode(manifestJson) as Map<String, dynamic>; group('Legacy asset manifest (AssetManifest.json)', () {
final Iterable<String> keys = parsedJson.keys; Future<Map<String, List<String>>> extractAssetManifestFromBundle(ManifestAssetBundle bundle) async {
final Map<String, List<String>> parsedManifest = <String, List<String>> { final String manifestJson = utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes());
for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>), final Map<String, dynamic> parsedJson = json.decode(manifestJson) as Map<String, dynamic>;
}; final Iterable<String> keys = parsedJson.keys;
return parsedManifest; final Map<String, List<String>> parsedManifest = <String, List<String>> {
} for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>),
};
group('AssetBundle asset variants (with Unix-style paths)', () { return parsedManifest;
late Platform platform; }
late FileSystem fs;
group('AssetBundle asset variants (with Unix-style paths)', () {
setUp(() { late Platform platform;
platform = FakePlatform(); late FileSystem fs;
fs = MemoryFileSystem.test();
Cache.flutterRoot = Cache.defaultFlutterRoot( setUp(() {
platform: platform, platform = FakePlatform();
fileSystem: fs, fs = MemoryFileSystem.test();
userMessages: UserMessages() Cache.flutterRoot = Cache.defaultFlutterRoot(
); platform: platform,
fileSystem: fs,
fs.file('.packages').createSync(); userMessages: UserMessages()
);
fs.file('pubspec.yaml').writeAsStringSync(
''' fs.file('.packages').createSync();
name: test
dependencies: fs.file('pubspec.yaml').writeAsStringSync(
'''
name: test
dependencies:
flutter:
sdk: flutter
flutter: flutter:
sdk: flutter assets:
flutter: - assets/
assets: '''
- assets/ );
''' });
);
testWithoutContext('Only images in folders named with device pixel ratios (e.g. 2x, 3.0x) should be considered as variants of other images', () async {
const String image = 'assets/image.jpg';
const String image2xVariant = 'assets/2x/image.jpg';
const String imageNonVariant = 'assets/notAVariant/image.jpg';
final List<String> assets = <String>[
image,
image2xVariant,
imageNonVariant
];
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<String>> manifest = await extractAssetManifestFromBundle(bundle);
expect(manifest, hasLength(2));
expect(manifest[image], equals(<String>[image, image2xVariant]));
expect(manifest[imageNonVariant], equals(<String>[imageNonVariant]));
});
testWithoutContext('Asset directories are recursively searched for assets', () async {
const String topLevelImage = 'assets/image.jpg';
const String secondLevelImage = 'assets/folder/secondLevel.jpg';
const String secondLevel2xVariant = 'assets/folder/2x/secondLevel.jpg';
final List<String> assets = <String>[
topLevelImage,
secondLevelImage,
secondLevel2xVariant
];
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<String>> manifest = await extractAssetManifestFromBundle(bundle);
expect(manifest, hasLength(2));
expect(manifest[topLevelImage], equals(<String>[topLevelImage]));
expect(manifest[secondLevelImage], equals(<String>[secondLevelImage, secondLevel2xVariant]));
});
testWithoutContext('Asset paths should never be URI-encoded', () async {
const String image = 'assets/normalFolder/i have URI-reserved_characters.jpg';
const String imageVariant = 'assets/normalFolder/3x/i have URI-reserved_characters.jpg';
final List<String> assets = <String>[
image,
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<String>> manifest = await extractAssetManifestFromBundle(bundle);
expect(manifest, hasLength(1));
expect(manifest[image], equals(<String>[image, imageVariant]));
});
}); });
testWithoutContext('Only images in folders named with device pixel ratios (e.g. 2x, 3.0x) should be considered as variants of other images', () async {
const String image = 'assets/image.jpg';
const String image2xVariant = 'assets/2x/image.jpg';
const String imageNonVariant = 'assets/notAVariant/image.jpg';
final List<String> assets = <String>[
image,
image2xVariant,
imageNonVariant
];
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<String>> manifest = await extractAssetManifestFromBundle(bundle);
expect(manifest, hasLength(2));
expect(manifest[image], equals(<String>[image, image2xVariant]));
expect(manifest[imageNonVariant], equals(<String>[imageNonVariant]));
});
testWithoutContext('Asset directories are recursively searched for assets', () async { group('AssetBundle asset variants (with Windows-style filepaths)', () {
const String topLevelImage = 'assets/image.jpg'; late final Platform platform;
const String secondLevelImage = 'assets/folder/secondLevel.jpg'; late final FileSystem fs;
const String secondLevel2xVariant = 'assets/folder/2x/secondLevel.jpg';
setUp(() {
final List<String> assets = <String>[ platform = FakePlatform(operatingSystem: 'windows');
topLevelImage, fs = MemoryFileSystem.test(style: FileSystemStyle.windows);
secondLevelImage, Cache.flutterRoot = Cache.defaultFlutterRoot(
secondLevel2xVariant platform: platform,
]; fileSystem: fs,
userMessages: UserMessages()
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<String>> manifest = await extractAssetManifestFromBundle(bundle);
expect(manifest, hasLength(2));
expect(manifest[topLevelImage], equals(<String>[topLevelImage]));
expect(manifest[secondLevelImage], equals(<String>[secondLevelImage, secondLevel2xVariant]));
});
testWithoutContext('Asset paths should never be URI-encoded', () async { fs.file('.packages').createSync();
const String image = 'assets/normalFolder/i have URI-reserved_characters.jpg';
const String imageVariant = 'assets/normalFolder/3x/i have URI-reserved_characters.jpg'; fs.file('pubspec.yaml').writeAsStringSync(
'''
final List<String> assets = <String>[ name: test
image, dependencies:
imageVariant flutter:
]; sdk: flutter
flutter:
for (final String asset in assets) { assets:
final File assetFile = fs.file(asset); - assets/
assetFile.createSync(recursive: true); '''
assetFile.writeAsStringSync(asset); );
} });
final ManifestAssetBundle bundle = ManifestAssetBundle( testWithoutContext('Variant detection works with windows-style filepaths', () async {
logger: BufferLogger.test(), const List<String> assets = <String>[
fileSystem: fs, r'assets\foo.jpg',
platform: platform, r'assets\2x\foo.jpg',
); r'assets\somewhereElse\bar.jpg',
r'assets\somewhereElse\2x\bar.jpg',
await bundle.build( ];
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), for (final String asset in assets) {
); final File assetFile = fs.file(asset);
assetFile.createSync(recursive: true);
final Map<String, List<String>> manifest = await extractAssetManifestFromBundle(bundle); assetFile.writeAsStringSync(asset);
expect(manifest, hasLength(1)); }
expect(manifest[image], equals(<String>[image, imageVariant]));
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<String>> manifest = await extractAssetManifestFromBundle(bundle);
expect(manifest, hasLength(2));
expect(manifest['assets/foo.jpg'], equals(<String>['assets/foo.jpg', 'assets/2x/foo.jpg']));
expect(manifest['assets/somewhereElse/bar.jpg'], equals(<String>['assets/somewhereElse/bar.jpg', 'assets/somewhereElse/2x/bar.jpg']));
});
}); });
}); });
group('Current asset manifest (AssetManifest.bin)', () {
Future<String> extractAssetManifestFromBundleAsJson(ManifestAssetBundle bundle) async {
final List<int> manifestBytes = await bundle.entries['AssetManifest.bin']!.contentsAsBytes();
return json.encode(const StandardMessageCodec().decodeMessage(
ByteData.sublistView(Uint8List.fromList(manifestBytes))
));
}
group('AssetBundle asset variants (with Unix-style paths)', () {
late Platform platform;
late FileSystem fs;
setUp(() {
platform = FakePlatform();
fs = MemoryFileSystem.test();
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fs,
userMessages: UserMessages()
);
fs.file('.packages').createSync();
fs.file('pubspec.yaml').writeAsStringSync(
'''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
assets:
- assets/
'''
);
});
testWithoutContext('Only images in folders named with device pixel ratios (e.g. 2x, 3.0x) should be considered as variants of other images', () async {
const String image = 'assets/image.jpg';
const String image2xVariant = 'assets/2x/image.jpg';
const String imageNonVariant = 'assets/notAVariant/image.jpg';
final List<String> assets = <String>[
image,
image2xVariant,
imageNonVariant
];
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),
);
const String expectedManifest = '{"$image":[{"asset":"$image2xVariant","dpr":2.0}],'
'"$imageNonVariant":[]}';
final String manifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(manifest, equals(expectedManifest));
});
testWithoutContext('Asset directories are recursively searched for assets', () async {
const String topLevelImage = 'assets/image.jpg';
const String secondLevelImage = 'assets/folder/secondLevel.jpg';
const String secondLevel2xVariant = 'assets/folder/2x/secondLevel.jpg';
final List<String> assets = <String>[
topLevelImage,
secondLevelImage,
secondLevel2xVariant
];
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),
);
const String expectedManifest = '{'
'"$secondLevelImage":[{"asset":"$secondLevel2xVariant","dpr":2.0}],'
'"$topLevelImage":[]'
'}';
final String manifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(manifest, equals(expectedManifest));
});
testWithoutContext('Asset paths should never be URI-encoded', () async {
const String image = 'assets/normalFolder/i have URI-reserved_characters.jpg';
const String imageVariant = 'assets/normalFolder/3x/i have URI-reserved_characters.jpg';
final List<String> assets = <String>[
image,
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),
);
const String expectedManifest = '{"$image":[{"asset":"$imageVariant","dpr":3.0}]}';
final String manifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(manifest, equals(expectedManifest));
});
});
group('AssetBundle asset variants (with Windows-style filepaths)', () {
late final Platform platform;
late final FileSystem fs;
setUp(() { group('AssetBundle asset variants (with Windows-style filepaths)', () {
platform = FakePlatform(operatingSystem: 'windows'); late final Platform platform;
fs = MemoryFileSystem.test(style: FileSystemStyle.windows); late final FileSystem fs;
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fs,
userMessages: UserMessages()
);
fs.file('.packages').createSync(); setUp(() {
platform = FakePlatform(operatingSystem: 'windows');
fs = MemoryFileSystem.test(style: FileSystemStyle.windows);
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fs,
userMessages: UserMessages()
);
fs.file('pubspec.yaml').writeAsStringSync( fs.file('.packages').createSync();
'''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
assets:
- assets/
'''
);
});
testWithoutContext('Variant detection works with windows-style filepaths', () async { fs.file('pubspec.yaml').writeAsStringSync(
const List<String> assets = <String>[ '''
r'assets\foo.jpg', name: test
r'assets\2x\foo.jpg', dependencies:
r'assets\somewhereElse\bar.jpg', flutter:
r'assets\somewhereElse\2x\bar.jpg', sdk: flutter
]; flutter:
assets:
for (final String asset in assets) { - assets/
final File assetFile = fs.file(asset); '''
assetFile.createSync(recursive: true); );
assetFile.writeAsStringSync(asset); });
}
testWithoutContext('Variant detection works with windows-style filepaths', () async {
final ManifestAssetBundle bundle = ManifestAssetBundle( const List<String> assets = <String>[
logger: BufferLogger.test(), r'assets\foo.jpg',
fileSystem: fs, r'assets\2x\foo.jpg',
platform: platform, r'assets\somewhereElse\bar.jpg',
); r'assets\somewhereElse\2x\bar.jpg',
];
await bundle.build(
packagesPath: '.packages', for (final String asset in assets) {
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), final File assetFile = fs.file(asset);
); assetFile.createSync(recursive: true);
assetFile.writeAsStringSync(asset);
final Map<String, List<String>> manifest = await extractAssetManifestFromBundle(bundle); }
expect(manifest, hasLength(2)); final ManifestAssetBundle bundle = ManifestAssetBundle(
expect(manifest['assets/foo.jpg'], equals(<String>['assets/foo.jpg', 'assets/2x/foo.jpg'])); logger: BufferLogger.test(),
expect(manifest['assets/somewhereElse/bar.jpg'], equals(<String>['assets/somewhereElse/bar.jpg', 'assets/somewhereElse/2x/bar.jpg'])); fileSystem: fs,
platform: platform,
);
await bundle.build(
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
);
const String expectedManifest = '{"assets/foo.jpg":[{"asset":"assets/2x/foo.jpg","dpr":2.0}],'
'"assets/somewhereElse/bar.jpg":[{"asset":"assets/somewhereElse/2x/bar.jpg","dpr":2.0}]}';
final String manifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(manifest, equals(expectedManifest));
});
}); });
}); });
} }
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