Unverified Commit 7673108d authored by Casey Hillers's avatar Casey Hillers Committed by GitHub

Revert "Speed up first asset load by encoding asset manifest in binary rather...

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

This reverts commit 56cad89b.
parent 352ad3a9
...@@ -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/services.dart' show PlatformAssetBundle, StandardMessageCodec; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show PlatformAssetBundle;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../common.dart'; import '../common.dart';
...@@ -18,14 +18,16 @@ void main() async { ...@@ -18,14 +18,16 @@ 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 assetManifest = await loadAssetManifest(); final ByteData assetManifestBytes = await bundle.load('money_asset_manifest.json');
watch.start(); watch.start();
for (int i = 0; i < _kNumIterations; i++) { for (int i = 0; i < _kNumIterations; i++) {
// This is effectively a test. bundle.clear();
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
AssetImage.parseAssetManifest(assetManifest); await AssetImage.manifestParser(json);
} }
watch.stop(); watch.stop();
...@@ -38,49 +40,3 @@ void main() async { ...@@ -38,49 +40,3 @@ 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,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// 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';
...@@ -60,9 +58,4 @@ class TestAssetBundle extends AssetBundle { ...@@ -60,9 +58,4 @@ 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,8 +11,7 @@ import 'package:flutter/services.dart'; ...@@ -11,8 +11,7 @@ import 'package:flutter/services.dart';
import 'image_provider.dart'; import 'image_provider.dart';
const String _kLegacyAssetManifestFilename = 'AssetManifest.json'; const String _kAssetManifestFileName = '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
...@@ -285,45 +284,18 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -285,45 +284,18 @@ class AssetImage extends AssetBundleImageProvider {
Completer<AssetBundleImageKey>? completer; Completer<AssetBundleImageKey>? completer;
Future<AssetBundleImageKey>? result; Future<AssetBundleImageKey>? result;
Future<_AssetManifest> loadJsonAssetManifest() { chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>(
Future<_AssetManifest> parseJson(String data) { (Map<String, List<String>>? manifest) {
final _AssetManifest parsed = _LegacyAssetManifest.fromJsonString(data); final String chosenName = _chooseVariant(
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,
candidateVariants, manifest == null ? null : manifest[keyName],
); )!;
final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = AssetBundleImageKey( final AssetBundleImageKey key = AssetBundleImageKey(
bundle: chosenBundle, bundle: chosenBundle,
name: chosenVariant.asset, name: chosenName,
scale: chosenVariant.devicePixelRatio, scale: chosenScale,
); );
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
...@@ -337,15 +309,14 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -337,15 +309,14 @@ class AssetImage extends AssetBundleImageProvider {
// ourselves. // ourselves.
result = SynchronousFuture<AssetBundleImageKey>(key); result = SynchronousFuture<AssetBundleImageKey>(key);
} }
}) },
.onError((Object error, StackTrace stack) { ).catchError((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.
...@@ -357,29 +328,35 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -357,29 +328,35 @@ class AssetImage extends AssetBundleImageProvider {
return completer.future; return completer.future;
} }
/// Parses the asset manifest's file contents into it's Dart representation. /// Parses the asset manifest string into a strongly-typed map.
@visibleForTesting @visibleForTesting
// Return type is set to Object?, because the specific type is private. static Future<Map<String, List<String>>?> manifestParser(String? jsonData) {
static Object? parseAssetManifest(ByteData bytes) { if (jsonData == null) {
return _AssetManifestBin.fromStandardMessageCodecMessage(bytes); return SynchronousFuture<Map<String, List<String>>?>(null);
}
// 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);
} }
_AssetVariant _chooseVariant(String mainAssetKey, ImageConfiguration config, List<_AssetVariant> candidateVariants) { String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) {
final _AssetVariant mainAsset = _AssetVariant(asset: mainAssetKey, if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
devicePixelRatio: _naturalResolution); return main;
if (config.devicePixelRatio == null || candidateVariants.isEmpty) {
return mainAsset;
} }
final SplayTreeMap<double, _AssetVariant> candidatesByDevicePixelRatio = // TODO(ianh): Consider moving this parsing logic into _manifestParser.
SplayTreeMap<double, _AssetVariant>(); final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
for (final _AssetVariant candidate in candidateVariants) { for (final String candidate in candidates) {
candidatesByDevicePixelRatio[candidate.devicePixelRatio] = candidate; mapping[_parseScale(candidate)] = 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(candidatesByDevicePixelRatio, config.devicePixelRatio!); return _findBestVariant(mapping, config.devicePixelRatio!);
} }
// Returns the "best" asset variant amongst the available `candidates`. // Returns the "best" asset variant amongst the available `candidates`.
...@@ -394,17 +371,17 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -394,17 +371,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`.
_AssetVariant _findBestVariant(SplayTreeMap<double, _AssetVariant> candidatesByDpr, double value) { String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
if (candidatesByDpr.containsKey(value)) { if (candidates.containsKey(value)) {
return candidatesByDpr[value]!; return candidates[value]!;
} }
final double? lower = candidatesByDpr.lastKeyBefore(value); final double? lower = candidates.lastKeyBefore(value);
final double? upper = candidatesByDpr.firstKeyAfter(value); final double? upper = candidates.firstKeyAfter(value);
if (lower == null) { if (lower == null) {
return candidatesByDpr[upper]!; return candidates[upper];
} }
if (upper == null) { if (upper == null) {
return candidatesByDpr[lower]!; return candidates[lower];
} }
// On screens with low device-pixel ratios the artifacts from upscaling // On screens with low device-pixel ratios the artifacts from upscaling
...@@ -412,116 +389,20 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -412,116 +389,20 @@ 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 candidatesByDpr[upper]!; return candidates[upper];
} else { } else {
return candidatesByDpr[lower]!; return candidates[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>[];
}
static double _parseScale(String mainAsset, String variant) { double _parseScale(String key) {
// The legacy asset manifest includes the main asset within its variant list. if (key == assetName) {
if (mainAsset == variant) {
return _naturalResolution; return _naturalResolution;
} }
final Uri assetUri = Uri.parse(variant); final Uri assetUri = Uri.parse(key);
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];
...@@ -531,23 +412,22 @@ class _LegacyAssetManifest implements _AssetManifest { ...@@ -531,23 +412,22 @@ class _LegacyAssetManifest implements _AssetManifest {
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
} }
}
class _AssetVariant {
_AssetVariant({
required this.asset,
required this.devicePixelRatio,
});
factory _AssetVariant.fromManifestData(Object data) { @override
final Map<Object?, Object?> asStructuredData = data as Map<Object?, Object?>; bool operator ==(Object other) {
return _AssetVariant(asset: asStructuredData['asset']! as String, if (other.runtimeType != runtimeType) {
devicePixelRatio: asStructuredData['dpr']! as double); return false;
}
return other is AssetImage
&& other.keyName == keyName
&& other.bundle == bundle;
} }
final double devicePixelRatio; @override
final String asset; int get hashCode => Object.hash(keyName, bundle);
@override
String toString() => '${objectRuntimeType(this, 'AssetImage')}(bundle: $bundle, name: "$keyName")';
} }
...@@ -96,25 +96,12 @@ abstract class AssetBundle { ...@@ -96,25 +96,12 @@ 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 that function's result. /// and return the 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.
...@@ -169,18 +156,6 @@ class NetworkAssetBundle extends AssetBundle { ...@@ -169,18 +156,6 @@ 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().
...@@ -200,7 +175,6 @@ abstract class CachingAssetBundle extends AssetBundle { ...@@ -200,7 +175,6 @@ 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 }) {
...@@ -251,69 +225,16 @@ abstract class CachingAssetBundle extends AssetBundle { ...@@ -251,69 +225,16 @@ 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
...@@ -355,7 +276,7 @@ class PlatformAssetBundle extends CachingAssetBundle { ...@@ -355,7 +276,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 guarded on // above. If that code is changed, this needs to be gaurded 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,14 +13,18 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -13,14 +13,18 @@ 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<Map<dynamic, dynamic>>> _assetBundleMap; final Map<String, List<String>> _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.bin') { if (key == 'AssetManifest.json') {
return const StandardMessageCodec().encodeMessage(_assetBundleMap)!; return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer);
} }
loadCallCount[key] = loadCallCount[key] ?? 0 + 1; loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
...@@ -38,71 +42,12 @@ class TestAssetBundle extends CachingAssetBundle { ...@@ -38,71 +42,12 @@ 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<Map<dynamic, dynamic>>> assetBundleMap = final Map<String, List<String>> assetBundleMap = <String, List<String>>{};
<String, List<Map<dynamic, dynamic>>>{};
assetBundleMap[mainAssetPath] = <Map<dynamic,dynamic>>[]; assetBundleMap[mainAssetPath] = <String>[];
final AssetImage assetImage = AssetImage( final AssetImage assetImage = AssetImage(
mainAssetPath, mainAssetPath,
...@@ -148,13 +93,10 @@ void main() { ...@@ -148,13 +93,10 @@ 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<Map<dynamic, dynamic>>> assetBundleMap = final Map<String, List<String>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{}; <String, List<String>>{};
final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{}; assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
mainAssetVariantManifestEntry['asset'] = variantPath;
mainAssetVariantManifestEntry['dpr'] = 3.0;
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
...@@ -181,10 +123,10 @@ void main() { ...@@ -181,10 +123,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<Map<dynamic, dynamic>>> assetBundleMap = final Map<String, List<String>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{}; <String, List<String>>{};
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[]; assetBundleMap[mainAssetPath] = <String>[mainAssetPath];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
...@@ -214,18 +156,16 @@ void main() { ...@@ -214,18 +156,16 @@ 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<Map<dynamic, dynamic>>> assetBundleMap = final Map<String, List<String>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{}; <String, List<String>>{};
final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{}; assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
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.bin') { if (key == 'AssetManifest.json') {
return const StandardMessageCodec().encodeMessage(json.decode('{"one":[]}'))!; return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert('{"one": ["one"]}')).buffer);
} }
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,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// 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';
...@@ -17,32 +16,27 @@ import '../image_data.dart'; ...@@ -17,32 +16,27 @@ import '../image_data.dart';
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale); ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
double scaleOf(ByteData data) => data.getFloat64(0); double scaleOf(ByteData data) => data.getFloat64(0);
final Map<dynamic, dynamic> testManifest = json.decode(''' const String testManifest = '''
{ {
"assets/image.png" : [ "assets/image.png" : [
{"asset": "assets/1.5x/image.png", "dpr": 1.5}, "assets/image.png",
{"asset": "assets/2.0x/image.png", "dpr": 2.0}, "assets/1.5x/image.png",
{"asset": "assets/3.0x/image.png", "dpr": 3.0}, "assets/2.0x/image.png",
{"asset": "assets/4.0x/image.png", "dpr": 4.0} "assets/3.0x/image.png",
"assets/4.0x/image.png"
] ]
} }
''') as Map<dynamic, dynamic>; ''';
class TestAssetBundle extends CachingAssetBundle { class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle({ this.manifest = testManifest });
TestAssetBundle({ required Map<dynamic, dynamic> manifest }) { final String 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;
...@@ -65,6 +59,14 @@ class TestAssetBundle extends CachingAssetBundle { ...@@ -65,6 +59,14 @@ 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)}()';
} }
...@@ -104,7 +106,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize ...@@ -104,7 +106,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize
devicePixelRatio: ratio, devicePixelRatio: ratio,
), ),
child: DefaultAssetBundle( child: DefaultAssetBundle(
bundle: bundle ?? TestAssetBundle(manifest: testManifest), bundle: bundle ?? TestAssetBundle(),
child: Center( child: Center(
child: inferSize ? child: inferSize ?
Image( Image(
...@@ -257,21 +259,46 @@ void main() { ...@@ -257,21 +259,46 @@ 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.
final Map<dynamic, dynamic> manifest = json.decode(''' const String manifest = '''
{ {
"assets/image.png" : [ "assets/image.png" : [
{"asset": "assets/1.0x/image.png", "dpr":1.0}, "assets/image.png",
{"asset": "assets/1.5x/image.png", "dpr":1.5}, "assets/1.0x/image.png",
{"asset": "assets/2.0x/image.png", "dpr":2.0}, "assets/1.5x/image.png",
{"asset": "assets/3.0x/image.png", "dpr":3.0}, "assets/2.0x/image.png",
{"asset": "assets/4.0x/image.png", "dpr":4.0} "assets/3.0x/image.png",
"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;
...@@ -310,13 +337,14 @@ void main() { ...@@ -310,13 +337,14 @@ 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: json.decode(''' final AssetBundle bundle = TestAssetBundle(manifest: '''
{ {
"assets/image.png" : [ "assets/image.png" : [
{"asset": "assets/1.5x/image.png", "dpr": 1.5} "assets/image.png",
"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,11 +2,8 @@ ...@@ -2,11 +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: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';
...@@ -139,9 +136,6 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -139,9 +136,6 @@ 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;
...@@ -167,8 +161,7 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -167,8 +161,7 @@ class ManifestAssetBundle implements AssetBundle {
DateTime? _lastBuildTimestamp; DateTime? _lastBuildTimestamp;
static const String _kAssetManifestBinFileName = 'AssetManifest.bin'; static const String _kAssetManifestJson = 'AssetManifest.json';
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,
...@@ -236,13 +229,8 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -236,13 +229,8 @@ class ManifestAssetBundle implements AssetBundle {
// device. // device.
_lastBuildTimestamp = DateTime.now(); _lastBuildTimestamp = DateTime.now();
if (flutterManifest.isEmpty) { if (flutterManifest.isEmpty) {
entries[_kAssetManifestJsonFileName] = DevFSStringContent('{}'); entries[_kAssetManifestJson] = DevFSStringContent('{}');
entryKinds[_kAssetManifestJsonFileName] = AssetKind.regular; entryKinds[_kAssetManifestJson] = 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;
} }
...@@ -439,11 +427,7 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -439,11 +427,7 @@ class ManifestAssetBundle implements AssetBundle {
_wildcardDirectories[uri] ??= _fileSystem.directory(uri); _wildcardDirectories[uri] ??= _fileSystem.directory(uri);
} }
final Map<String, List<String>> assetManifest = final DevFSStringContent assetManifest = _createAssetManifest(assetVariants, deferredComponentsAssetVariants);
_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) {
...@@ -467,8 +451,7 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -467,8 +451,7 @@ 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(_kAssetManifestJsonFileName, assetManifestJson, AssetKind.regular); _setIfChanged(_kAssetManifestJson, assetManifest, 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;
...@@ -477,31 +460,17 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -477,31 +460,17 @@ class ManifestAssetBundle implements AssetBundle {
@override @override
List<File> additionalDependencies = <File>[]; List<File> additionalDependencies = <File>[];
void _setIfChanged(String key, DevFSContent content, AssetKind assetKind) { void _setIfChanged(String key, DevFSStringContent content, AssetKind assetKind) {
bool areEqual(List<int> o1, List<int> o2) { if (!entries.containsKey(key)) {
if (o1.length != o2.length) { entries[key] = content;
return false; entryKinds[key] = assetKind;
}
for (int index = 0; index < o1.length; index++) {
if (o1[index] != o2[index]) {
return false;
}
}
return true;
}
final DevFSContent? oldContent = entries[key];
// In the case that the content is unchanged, we want to avoid an overwrite
// as the isModified property may be reset to true,
if (oldContent is DevFSByteContent && content is DevFSByteContent &&
areEqual(oldContent.bytes, content.bytes)) {
return; return;
} }
final DevFSStringContent? oldContent = entries[key] as DevFSStringContent?;
entries[key] = content; if (oldContent?.string != content.string) {
entryKinds[key] = assetKind; entries[key] = content;
entryKinds[key] = assetKind;
}
} }
void _setLicenseIfChanged( void _setLicenseIfChanged(
...@@ -653,14 +622,14 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -653,14 +622,14 @@ class ManifestAssetBundle implements AssetBundle {
return deferredComponentsAssetVariants; return deferredComponentsAssetVariants;
} }
Map<String, List<String>> _createAssetManifest( DevFSStringContent _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>> manifest = <String, List<String>>{}; final Map<String, List<String>> jsonObject = <String, List<String>>{};
final Map<_Asset, List<String>> entries = <_Asset, List<String>>{}; final Map<_Asset, List<String>> jsonEntries = <_Asset, List<String>>{};
assetVariants.forEach((_Asset main, List<_Asset> variants) { assetVariants.forEach((_Asset main, List<_Asset> variants) {
entries[main] = <String>[ jsonEntries[main] = <String>[
for (final _Asset variant in variants) for (final _Asset variant in variants)
variant.entryUri.path, variant.entryUri.path,
]; ];
...@@ -668,69 +637,26 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -668,69 +637,26 @@ 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) {
entries[main] = <String>[ jsonEntries[main] = <String>[
for (final _Asset variant in variants) for (final _Asset variant in variants)
variant.entryUri.path, variant.entryUri.path,
]; ];
}); });
} }
} }
final List<_Asset> sortedKeys = entries.keys.toList() final List<_Asset> sortedKeys = jsonEntries.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 = entries[main]!; final List<String> rawEntryVariantsPaths = jsonEntries[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();
manifest[decodedEntryPath] = decodedEntryVariantPaths; jsonObject[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,8 +57,6 @@ dependencies: ...@@ -57,8 +57,6 @@ 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"
...@@ -90,6 +88,7 @@ dependencies: ...@@ -90,6 +88,7 @@ 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
...@@ -98,10 +97,9 @@ dev_dependencies: ...@@ -98,10 +97,9 @@ 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: 408d # PUBSPEC CHECKSUM: 65eb
...@@ -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, isNot(contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'))); expect(logger.statusText.contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), false);
}); });
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, isNot(contains('Errors checking the following files:'))); expect(logger.statusText.contains('Errors checking the following files:'), false);
}); });
testWithoutContext('androidStringMapping modifies strings file', () async { testWithoutContext('androidStringMapping modifies strings file', () async {
...@@ -448,10 +448,9 @@ loading-units: ...@@ -448,10 +448,9 @@ loading-units:
.childDirectory('main') .childDirectory('main')
.childFile('AndroidManifest.xml'); .childFile('AndroidManifest.xml');
expect(manifestOutput.existsSync(), true); expect(manifestOutput.existsSync(), true);
final String manifestOutputString = manifestOutput.readAsStringSync(); 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(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('android:value="invalidmapping"'), false);
expect(manifestOutputString, isNot(contains('android:value="invalidmapping"'))); expect(manifestOutput.readAsStringSync().contains("<!-- Don't delete the meta-data below."), true);
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 {
...@@ -696,8 +695,8 @@ loading-units: ...@@ -696,8 +695,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"/>')); 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(), isNot(contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"')))); expect(manifestOutput.readAsStringSync().contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"')), false);
expect(manifestOutput.readAsStringSync(), contains("<!-- Don't delete the meta-data below.")); expect(manifestOutput.readAsStringSync().contains("<!-- Don't delete the meta-data below."), true);
}); });
} }
...@@ -111,9 +111,8 @@ $fontsSection ...@@ -111,9 +111,8 @@ $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.keys, containsAll( expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
<String>['AssetManifest.bin', 'AssetManifest.json', 'FontManifest.json', 'NOTICES.Z'] expect(bundle.entries.containsKey('FontManifest.json'), isTrue);
));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// 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';
...@@ -12,7 +11,6 @@ import 'package:flutter_tools/src/asset.dart'; ...@@ -12,7 +11,6 @@ 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';
...@@ -26,7 +24,6 @@ void main() { ...@@ -26,7 +24,6 @@ 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) {
...@@ -63,6 +60,37 @@ $assetsSection ...@@ -63,6 +60,37 @@ $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));
...@@ -73,391 +101,182 @@ $assetsSection ...@@ -73,391 +101,182 @@ $assetsSection
} }
} }
// TODO(andrewkolos): Delete this group once we stop producing AssetManifest.json late FileSystem testFileSystem;
// as part of build.
group('Legacy asset manifest (AssetManifest.json)', () { setUp(() async {
Future<void> buildAndVerifyAssets( testFileSystem = MemoryFileSystem(
List<String> assets, style: globals.platform.isWindows
List<String> packages, ? FileSystemStyle.windows
String? expectedAssetManifest, { : FileSystemStyle.posix,
bool expectExists = true, );
}) async { testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
});
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(),
});
for (final String packageName in packages) { testUsingContext('No assets are bundled when the package has an asset that is not listed', () 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('p/p/pubspec.yaml', 'test_package');
reason: 'Cannot find key on bundle: $entryKey');
if (expectExists) {
expect(
utf8.decode(await bundle.entries[entryKey]!.contentsAsBytes()),
asset,
);
}
}
}
if (expectExists) { final List<String> assets = <String>['a/foo'];
expect( writeAssets('p/p/', assets);
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedAssetManifest, 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(),
});
late FileSystem testFileSystem; 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);
setUp(() async { const String expectedManifest = '{'
testFileSystem = MemoryFileSystem( '"packages/test_package/a/bar":'
style: globals.platform.isWindows '["packages/test_package/a/bar"],'
? FileSystemStyle.windows '"packages/test_package/a/foo":'
: FileSystemStyle.posix, '["packages/test_package/a/foo","packages/test_package/a/2x/foo"]'
'}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedManifest,
); );
testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
}); });
group('AssetBundle assets from packages', () { testUsingContext('One asset and its variant are bundled when the package '
testUsingContext('No assets are bundled when the package has no assets', () async { 'has an asset and a variant, and the app lists the asset in its pubspec', () async {
writePubspecFile('pubspec.yaml', 'test'); writePubspecFile(
writePackagesFile('test_package:p/p/lib/'); 'pubspec.yaml',
writePubspecFile('p/p/pubspec.yaml', 'test_package'); 'test',
assets: <String>['packages/test_package/a/foo'],
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); );
await bundle.build(packagesPath: '.packages'); writePackagesFile('test_package:p/p/lib/');
expect(bundle.entries.keys, unorderedEquals( writePubspecFile(
<String>['AssetManifest.bin', 'AssetManifest.json', 'FontManifest.json', 'NOTICES.Z'] 'p/p/pubspec.yaml',
)); 'test_package',
const String expectedAssetManifest = '{}'; );
expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()), final List<String> assets = <String>['a/foo', 'a/2x/foo'];
expectedAssetManifest, writeAssets('p/p/lib/', assets);
);
expect( const String expectedManifest = '{"packages/test_package/a/foo":'
utf8.decode(await bundle.entries['FontManifest.json']!.contentsAsBytes()), '["packages/test_package/a/foo","packages/test_package/a/2x/foo"]}';
'[]',
); await buildAndVerifyAssets(
}, overrides: <Type, Generator>{ assets,
FileSystem: () => testFileSystem, <String>['test_package'],
ProcessManager: () => FakeProcessManager.any(), expectedManifest,
}); );
}, overrides: <Type, Generator>{
testUsingContext('No assets are bundled when the package has an asset that is not listed', () async { FileSystem: () => testFileSystem,
writePubspecFile('pubspec.yaml', 'test'); ProcessManager: () => FakeProcessManager.any(),
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('Asset paths can contain URL reserved characters', () async { testUsingContext('Two assets are bundled when the package has and lists '
'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/foo [x]']; final List<String> assets = <String>['a/foo', 'a/bar'];
writePubspecFile( writePubspecFile(
'p/p/pubspec.yaml', 'p/p/pubspec.yaml',
'test_package', 'test_package',
...@@ -466,8 +285,8 @@ $assetsSection ...@@ -466,8 +285,8 @@ $assetsSection
writeAssets('p/p/', assets); writeAssets('p/p/', assets);
const String expectedAssetManifest = const String expectedAssetManifest =
'{"packages/test_package/a/foo":["packages/test_package/a/foo"],' '{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
'"packages/test_package/a/foo [x]":["packages/test_package/a/foo [x]"]}'; '"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assets,
...@@ -476,584 +295,283 @@ $assetsSection ...@@ -476,584 +295,283 @@ $assetsSection
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
group('AssetBundle assets from scanned paths', () { testUsingContext("Two assets are bundled when the package has two assets, listed in the app's pubspec", () async {
testUsingContext('Two assets are bundled when scanning their directory', () async { final List<String> assetEntries = <String>[
writePubspecFile('pubspec.yaml', 'test'); 'packages/test_package/a/foo',
writePackagesFile('test_package:p/p/lib/'); 'packages/test_package/a/bar',
];
final List<String> assetsOnDisk = <String>['a/foo', 'a/bar']; writePubspecFile(
final List<String> assetsOnManifest = <String>['a/']; 'pubspec.yaml',
'test',
writePubspecFile( assets: assetEntries,
'p/p/pubspec.yaml', );
'test_package', writePackagesFile('test_package:p/p/lib/');
assets: assetsOnManifest,
); final List<String> assets = <String>['a/foo', 'a/bar'];
writePubspecFile(
writeAssets('p/p/', assetsOnDisk); 'p/p/pubspec.yaml',
const String expectedAssetManifest = 'test_package',
'{"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);
await buildAndVerifyAssets( const String expectedAssetManifest =
assetsOnDisk, '{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
<String>['test_package'], '"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
expectedAssetManifest,
); await buildAndVerifyAssets(
}, overrides: <Type, Generator>{ assets,
FileSystem: () => testFileSystem, <String>['test_package'],
ProcessManager: () => FakeProcessManager.any(), expectedAssetManifest,
}); );
}, overrides: <Type, Generator>{
testUsingContext('Two assets are bundled when listing one and scanning second directory', () async { FileSystem: () => testFileSystem,
writePubspecFile('pubspec.yaml', 'test'); ProcessManager: () => FakeProcessManager.any(),
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(),
});
}); });
group('AssetBundle assets from scanned paths with MemoryFileSystem', () { testUsingContext('Two assets are bundled when two packages each have and list an asset their pubspec', () async {
testUsingContext('One asset is bundled with variant, scanning directory', () async { writePubspecFile(
writePubspecFile('pubspec.yaml', 'test'); 'pubspec.yaml',
writePackagesFile('test_package:p/p/lib/'); 'test',
);
final List<String> assetsOnDisk = <String>['a/foo','a/2x/foo']; writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
final List<String> assetOnManifest = <String>['a/',]; writePubspecFile(
'p/p/pubspec.yaml',
writePubspecFile( 'test_package',
'p/p/pubspec.yaml', assets: <String>['a/foo'],
'test_package', );
assets: assetOnManifest, writePubspecFile(
); 'p2/p/pubspec.yaml',
'test_package2',
writeAssets('p/p/', assetsOnDisk); assets: <String>['a/foo'],
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'];
await buildAndVerifyAssets( writeAssets('p/p/', assets);
assetsOnDisk, writeAssets('p2/p/', assets);
<String>['test_package'],
expectedAssetManifest, const String expectedAssetManifest =
); '{"packages/test_package/a/foo":'
}, overrides: <Type, Generator>{ '["packages/test_package/a/foo","packages/test_package/a/2x/foo"],'
FileSystem: () => testFileSystem, '"packages/test_package2/a/foo":'
ProcessManager: () => FakeProcessManager.any(), '["packages/test_package2/a/foo","packages/test_package2/a/2x/foo"]}';
});
await buildAndVerifyAssets(
testUsingContext('No asset is bundled with variant, no assets or directories are listed', () async { assets,
writePubspecFile('pubspec.yaml', 'test'); <String>['test_package', 'test_package2'],
writePackagesFile('test_package:p/p/lib/'); expectedAssetManifest,
);
final List<String> assetsOnDisk = <String>['a/foo', 'a/2x/foo']; }, overrides: <Type, Generator>{
final List<String> assetOnManifest = <String>[]; FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
writePubspecFile( });
'p/p/pubspec.yaml',
'test_package', testUsingContext("Two assets are bundled when two packages each have an asset, listed in the app's pubspec", () async {
assets: assetOnManifest, final List<String> assetEntries = <String>[
); 'packages/test_package/a/foo',
'packages/test_package2/a/foo',
writeAssets('p/p/', assetsOnDisk); ];
const String expectedAssetManifest = '{}'; writePubspecFile(
'pubspec.yaml',
await buildAndVerifyAssets( 'test',
assetOnManifest, assets: assetEntries,
<String>['test_package'], );
expectedAssetManifest, writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
); writePubspecFile(
}, overrides: <Type, Generator>{ 'p/p/pubspec.yaml',
FileSystem: () => testFileSystem, 'test_package',
ProcessManager: () => FakeProcessManager.any(), );
}); writePubspecFile(
'p2/p/pubspec.yaml',
testUsingContext('Expect error generating manifest, wrong non-existing directory is listed', () async { 'test_package2',
writePubspecFile('pubspec.yaml', 'test'); );
writePackagesFile('test_package:p/p/lib/');
final List<String> assets = <String>['a/foo', 'a/2x/foo'];
final List<String> assetOnManifest = <String>['c/']; writeAssets('p/p/lib/', assets);
writeAssets('p2/p/lib/', assets);
writePubspecFile(
'p/p/pubspec.yaml', const String expectedAssetManifest =
'test_package', '{"packages/test_package/a/foo":'
assets: assetOnManifest, '["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(
assetOnManifest, await buildAndVerifyAssets(
<String>['test_package'], assets,
null, <String>['test_package', 'test_package2'],
expectExists: false, expectedAssetManifest,
); );
}, 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))
));
}
Future<void> buildAndVerifyAssets( testUsingContext('Asset paths can contain URL reserved characters', () async {
List<String> assets, writePubspecFile('pubspec.yaml', 'test');
List<String> packages, writePackagesFile('test_package:p/p/lib/');
String? expectedAssetManifest, {
bool expectExists = true, final List<String> assets = <String>['a/foo', 'a/foo [x]'];
}) async { writePubspecFile(
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); 'p/p/pubspec.yaml',
await bundle.build(packagesPath: '.packages'); 'test_package',
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(),
});
for (final String packageName in packages) { group('AssetBundle assets from scanned paths', () {
for (final String asset in assets) { testUsingContext('Two assets are bundled when scanning their directory', () async {
final String entryKey = Uri.encodeFull('packages/$packageName/$asset'); writePubspecFile('pubspec.yaml', 'test');
expect(bundle.entries.containsKey(entryKey), expectExists, writePackagesFile('test_package:p/p/lib/');
reason: 'Cannot find key on bundle: $entryKey');
if (expectExists) {
expect(
utf8.decode(await bundle.entries[entryKey]!.contentsAsBytes()),
asset,
);
}
}
}
if (expectExists) { final List<String> assetsOnDisk = <String>['a/foo', 'a/bar'];
final String actualAssetManifest = await extractAssetManifestFromBundleAsJson(bundle); final List<String> assetsOnManifest = <String>['a/'];
expect(
actualAssetManifest,
expectedAssetManifest,
);
}
}
void writeAssets(String path, List<String> assets) { writePubspecFile(
for (final String asset in assets) { 'p/p/pubspec.yaml',
final String fullPath = fixPath(globals.fs.path.join(path, asset)); 'test_package',
assets: assetsOnManifest,
);
globals.fs.file(fullPath) writeAssets('p/p/', assetsOnDisk);
..createSync(recursive: true) const String expectedAssetManifest =
..writeAsStringSync(asset); '{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
} '"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
}
late FileSystem testFileSystem; await buildAndVerifyAssets(
assetsOnDisk,
<String>['test_package'],
expectedAssetManifest,
);
}, 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,
);
setUp(() async { writeAssets('p/p/', assetsOnDisk);
testFileSystem = MemoryFileSystem( const String expectedAssetManifest =
style: globals.platform.isWindows '{"packages/test_package/a/foo":["packages/test_package/a/foo"],'
? FileSystemStyle.windows '"packages/test_package/abc/bar":["packages/test_package/abc/bar"]}';
: FileSystemStyle.posix,
await buildAndVerifyAssets(
assetsOnDisk,
<String>['test_package'],
expectedAssetManifest,
); );
testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
}); });
group('AssetBundle assets from packages', () { testUsingContext('One asset is bundled with variant, scanning wrong directory', () async {
testUsingContext('No assets are bundled when the package has no assets', () async { writePubspecFile('pubspec.yaml', 'test');
writePubspecFile('pubspec.yaml', 'test'); writePackagesFile('test_package:p/p/lib/');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package'); 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/'
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
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',
)); assets: assetOnManifest,
final String actualAssetManifest = await extractAssetManifestFromBundleAsJson(bundle); );
const String expectedAssetManifest = '{}';
expect( writeAssets('p/p/', assetsOnDisk);
actualAssetManifest,
expectedAssetManifest, final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
); await bundle.build(packagesPath: '.packages');
expect(
utf8.decode(await bundle.entries['FontManifest.json']!.contentsAsBytes()), expect(bundle.entries['AssetManifest.json'], isNull,
'[]', reason: 'Invalid pubspec.yaml should not generate AssetManifest.json' );
); }, overrides: <Type, Generator>{
}, overrides: <Type, Generator>{ FileSystem: () => testFileSystem,
FileSystem: () => testFileSystem, ProcessManager: () => FakeProcessManager.any(),
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(),
});
}); });
});
testUsingContext('Asset paths can contain URL reserved characters', () async { group('AssetBundle assets from scanned paths with MemoryFileSystem', () {
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> assets = <String>['a/foo', 'a/foo [x]']; final List<String> assetsOnDisk = <String>['a/foo','a/2x/foo'];
final List<String> assetOnManifest = <String>['a/',];
writePubspecFile( writePubspecFile(
'p/p/pubspec.yaml', 'p/p/pubspec.yaml',
'test_package', 'test_package',
assets: assets, assets: assetOnManifest,
); );
writeAssets('p/p/', assets); writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest = const String expectedAssetManifest =
'{"packages/test_package/a/foo":[],' '{"packages/test_package/a/foo":["packages/test_package/a/foo","packages/test_package/a/2x/foo"]}';
'"packages/test_package/a/foo [x]":[]}';
await buildAndVerifyAssets( await buildAndVerifyAssets(
assets, assetsOnDisk,
<String>['test_package'], <String>['test_package'],
expectedAssetManifest, expectedAssetManifest,
); );
...@@ -1062,165 +580,53 @@ $assetsSection ...@@ -1062,165 +580,53 @@ $assetsSection
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
group('AssetBundle assets from scanned paths', () { testUsingContext('No asset is bundled with variant, no assets or directories are listed', () async {
testUsingContext('Two assets are bundled when scanning their 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> assetsOnDisk = <String>['a/foo', 'a/bar']; final List<String> assetOnManifest = <String>[];
final List<String> assetsOnManifest = <String>['a/'];
writePubspecFile(
writePubspecFile( 'p/p/pubspec.yaml',
'p/p/pubspec.yaml', 'test_package',
'test_package', assets: assetOnManifest,
assets: assetsOnManifest, );
);
writeAssets('p/p/', assetsOnDisk);
writeAssets('p/p/', assetsOnDisk); const String expectedAssetManifest = '{}';
const String expectedAssetManifest =
'{"packages/test_package/a/bar":[],' await buildAndVerifyAssets(
'"packages/test_package/a/foo":[]}'; assetOnManifest,
<String>['test_package'],
await buildAndVerifyAssets( expectedAssetManifest,
assetsOnDisk, );
<String>['test_package'], }, overrides: <Type, Generator>{
expectedAssetManifest, FileSystem: () => testFileSystem,
); 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(),
});
}); });
group('AssetBundle assets from scanned paths with MemoryFileSystem', () { testUsingContext('Expect error generating manifest, wrong non-existing directory is listed', () 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> assetOnManifest = <String>['c/'];
final List<String> assetsOnDisk = <String>['a/foo','a/2x/foo'];
final List<String> assetOnManifest = <String>['a/',]; writePubspecFile(
'p/p/pubspec.yaml',
writePubspecFile( 'test_package',
'p/p/pubspec.yaml', assets: assetOnManifest,
'test_package', );
assets: assetOnManifest,
); await buildAndVerifyAssets(
assetOnManifest,
writeAssets('p/p/', assetsOnDisk); <String>['test_package'],
const String expectedAssetManifest = null,
'{"packages/test_package/a/foo":[{"asset":"packages/test_package/a/2x/foo","dpr":2.0}]}'; expectExists: false,
);
await buildAndVerifyAssets( }, overrides: <Type, Generator>{
assetsOnDisk, FileSystem: () => testFileSystem,
<String>['test_package'], ProcessManager: () => FakeProcessManager.any(),
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,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// 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';
...@@ -14,7 +13,6 @@ import 'package:flutter_tools/src/build_info.dart'; ...@@ -14,7 +13,6 @@ 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';
...@@ -50,16 +48,11 @@ void main() { ...@@ -50,16 +48,11 @@ 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.keys, unorderedEquals(<String>['AssetManifest.json', 'AssetManifest.bin'])); expect(bundle.entries.length, 1);
const String expectedJsonAssetManifest = '{}'; const String expectedAssetManifest = '{}';
expect( expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()), utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedJsonAssetManifest, expectedAssetManifest,
);
const String expectedBinAssetManifest = '{}';
expect(
await _extractBinAssetManifestFromBundleAsJson(bundle),
expectedBinAssetManifest
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
...@@ -79,8 +72,12 @@ flutter: ...@@ -79,8 +72,12 @@ flutter:
'''); ''');
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.keys, unorderedEquals(<String>['AssetManifest.json', // Expected assets:
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt'])); // - asset manifest
// - 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.
...@@ -90,9 +87,13 @@ flutter: ...@@ -90,9 +87,13 @@ flutter:
expect(bundle.needsBuild(), true); expect(bundle.needsBuild(), true);
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json', // Expected assets:
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt', // - asset manifest
'assets/foo/fizz.txt'])); // - font manifest
// - 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(),
...@@ -111,8 +112,12 @@ flutter: ...@@ -111,8 +112,12 @@ 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');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json', // Expected assets:
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt'])); // - asset manifest
// - 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.
...@@ -133,8 +138,12 @@ name: example''') ...@@ -133,8 +138,12 @@ 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');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json', // Expected assets:
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt'])); // - asset manifest
// - 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(),
...@@ -157,8 +166,12 @@ flutter: ...@@ -157,8 +166,12 @@ 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');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json', // Expected assets:
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt'])); // - asset manifest
// - 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,
...@@ -190,8 +203,12 @@ flutter: ...@@ -190,8 +203,12 @@ flutter:
splitDeferredAssets: true, splitDeferredAssets: true,
).createBundle(); ).createBundle();
await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true); await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true);
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json', // Expected assets:
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt'])); // - asset manifest
// - 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);
...@@ -220,9 +237,12 @@ flutter: ...@@ -220,9 +237,12 @@ flutter:
'''); ''');
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.keys, unorderedEquals(<String>['assets/foo/bar.txt', // Expected assets:
'assets/bar/barbie.txt', 'assets/wild/dash.txt', 'AssetManifest.json', // - asset manifest
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z'])); // - font manifest
// - 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>{
...@@ -255,11 +275,14 @@ flutter: ...@@ -255,11 +275,14 @@ flutter:
splitDeferredAssets: true, splitDeferredAssets: true,
).createBundle(); ).createBundle();
await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true); await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true);
expect(bundle.entries.keys, unorderedEquals(<String>['assets/foo/bar.txt', // Expected assets:
'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z'])); // - asset manifest
expect(bundle.deferredComponentsEntries.keys, unorderedEquals(<String>['component1'])); // - font manifest
expect(bundle.deferredComponentsEntries['component1']!.keys, // - license file
unorderedEquals(<String>['assets/bar/barbie.txt', 'assets/wild/dash.txt'])); // - assets/foo/bar.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.
...@@ -270,13 +293,9 @@ flutter: ...@@ -270,13 +293,9 @@ 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.keys, unorderedEquals(<String>['assets/foo/bar.txt', expect(bundle.entries.length, 4);
'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
expect(bundle.deferredComponentsEntries.length, 1); expect(bundle.deferredComponentsEntries.length, 1);
expect(bundle.deferredComponentsEntries['component1']!.keys, expect(bundle.deferredComponentsEntries['component1']!.length, 3);
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(),
...@@ -316,8 +335,7 @@ assets: ...@@ -316,8 +335,7 @@ assets:
final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
final DevFSContent? assetManifestBin = bundle.entries['AssetManifest.bin']; final DevFSStringContent? assetManifest = bundle.entries['AssetManifest.json']
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?;
...@@ -326,8 +344,7 @@ assets: ...@@ -326,8 +344,7 @@ assets:
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
expect(assetManifestBin, bundle.entries['AssetManifest.bin']); expect(assetManifest, bundle.entries['AssetManifest.json']);
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>{
...@@ -622,8 +639,7 @@ flutter: ...@@ -622,8 +639,7 @@ flutter:
await bundle.build(packagesPath: '.packages'); await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['packages/foo/bar/fizz.txt', expect(bundle.entries, hasLength(4));
'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.
...@@ -723,7 +739,6 @@ flutter: ...@@ -723,7 +739,6 @@ 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'
)); ));
...@@ -759,8 +774,7 @@ flutter: ...@@ -759,8 +774,7 @@ 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.keys, unorderedEquals(<String>['assets/foo.txt', expect(bundle.entries.length, 4);
'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(),
...@@ -798,17 +812,9 @@ flutter: ...@@ -798,17 +812,9 @@ 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,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// 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';
...@@ -16,418 +15,209 @@ import 'package:flutter_tools/src/base/user_messages.dart'; ...@@ -16,418 +15,209 @@ 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() {
// TODO(andrewkolos): Delete this group once we stop producing AssetManifest.json Future<Map<String, List<String>>> extractAssetManifestFromBundle(ManifestAssetBundle bundle) async {
// as part of build. final String manifestJson = utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes());
group('Legacy asset manifest (AssetManifest.json)', () { final Map<String, dynamic> parsedJson = json.decode(manifestJson) as Map<String, dynamic>;
Future<Map<String, List<String>>> extractAssetManifestFromBundle(ManifestAssetBundle bundle) async { final Iterable<String> keys = parsedJson.keys;
final String manifestJson = utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()); final Map<String, List<String>> parsedManifest = <String, List<String>> {
final Map<String, dynamic> parsedJson = json.decode(manifestJson) as Map<String, dynamic>; for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>),
final Iterable<String> keys = parsedJson.keys; };
final Map<String, List<String>> parsedManifest = <String, List<String>> { return parsedManifest;
for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>), }
};
return parsedManifest; group('AssetBundle asset variants (with Unix-style paths)', () {
} late Platform platform;
late FileSystem fs;
group('AssetBundle asset variants (with Unix-style paths)', () {
late Platform platform; setUp(() {
late FileSystem fs; platform = FakePlatform();
fs = MemoryFileSystem.test();
setUp(() { Cache.flutterRoot = Cache.defaultFlutterRoot(
platform = FakePlatform(); platform: platform,
fs = MemoryFileSystem.test(); fileSystem: fs,
Cache.flutterRoot = Cache.defaultFlutterRoot( userMessages: UserMessages()
platform: platform, );
fileSystem: fs,
userMessages: UserMessages() fs.file('.packages').createSync();
);
fs.file('pubspec.yaml').writeAsStringSync(
fs.file('.packages').createSync(); '''
name: test
fs.file('pubspec.yaml').writeAsStringSync( dependencies:
'''
name: test
dependencies:
flutter:
sdk: flutter
flutter: flutter:
assets: sdk: flutter
- assets/ 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),
);
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]));
});
group('AssetBundle asset variants (with Windows-style filepaths)', () { testWithoutContext('Asset directories are recursively searched for assets', () async {
late final Platform platform; const String topLevelImage = 'assets/image.jpg';
late final FileSystem fs; const String secondLevelImage = 'assets/folder/secondLevel.jpg';
const String secondLevel2xVariant = 'assets/folder/2x/secondLevel.jpg';
setUp(() {
platform = FakePlatform(operatingSystem: 'windows'); final List<String> assets = <String>[
fs = MemoryFileSystem.test(style: FileSystemStyle.windows); topLevelImage,
Cache.flutterRoot = Cache.defaultFlutterRoot( secondLevelImage,
platform: platform, secondLevel2xVariant
fileSystem: fs, ];
userMessages: UserMessages()
); for (final String asset in assets) {
final File assetFile = fs.file(asset);
fs.file('.packages').createSync(); assetFile.createSync(recursive: true);
assetFile.writeAsStringSync(asset);
fs.file('pubspec.yaml').writeAsStringSync( }
'''
name: test final ManifestAssetBundle bundle = ManifestAssetBundle(
dependencies: logger: BufferLogger.test(),
flutter: fileSystem: fs,
sdk: flutter platform: platform,
flutter: );
assets:
- assets/ await bundle.build(
''' packagesPath: '.packages',
); flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
}); );
testWithoutContext('Variant detection works with windows-style filepaths', () async { final Map<String, List<String>> manifest = await extractAssetManifestFromBundle(bundle);
const List<String> assets = <String>[ expect(manifest, hasLength(2));
r'assets\foo.jpg', expect(manifest[topLevelImage], equals(<String>[topLevelImage]));
r'assets\2x\foo.jpg', expect(manifest[secondLevelImage], equals(<String>[secondLevelImage, secondLevel2xVariant]));
r'assets\somewhereElse\bar.jpg',
r'assets\somewhereElse\2x\bar.jpg',
];
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['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)', () { testWithoutContext('Asset paths should never be URI-encoded', () async {
Future<String> extractAssetManifestFromBundleAsJson(ManifestAssetBundle bundle) async { const String image = 'assets/normalFolder/i have URI-reserved_characters.jpg';
final List<int> manifestBytes = await bundle.entries['AssetManifest.bin']!.contentsAsBytes(); const String imageVariant = 'assets/normalFolder/3x/i have URI-reserved_characters.jpg';
return json.encode(const StandardMessageCodec().decodeMessage(
ByteData.sublistView(Uint8List.fromList(manifestBytes)) final List<String> assets = <String>[
)); image,
} imageVariant
];
group('AssetBundle asset variants (with Unix-style paths)', () {
late Platform platform; for (final String asset in assets) {
late FileSystem fs; final File assetFile = fs.file(asset);
assetFile.createSync(recursive: true);
setUp(() { assetFile.writeAsStringSync(asset);
platform = FakePlatform(); }
fs = MemoryFileSystem.test();
Cache.flutterRoot = Cache.defaultFlutterRoot( final ManifestAssetBundle bundle = ManifestAssetBundle(
platform: platform, logger: BufferLogger.test(),
fileSystem: fs, fileSystem: fs,
userMessages: UserMessages() platform: platform,
); );
fs.file('.packages').createSync(); await bundle.build(
packagesPath: '.packages',
fs.file('pubspec.yaml').writeAsStringSync( flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
''' );
name: test
dependencies: final Map<String, List<String>> manifest = await extractAssetManifestFromBundle(bundle);
flutter: expect(manifest, hasLength(1));
sdk: flutter expect(manifest[image], equals(<String>[image, imageVariant]));
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)', () { group('AssetBundle asset variants (with Windows-style filepaths)', () {
late final Platform platform; late final Platform platform;
late final FileSystem fs; late final FileSystem fs;
setUp(() { setUp(() {
platform = FakePlatform(operatingSystem: 'windows'); platform = FakePlatform(operatingSystem: 'windows');
fs = MemoryFileSystem.test(style: FileSystemStyle.windows); fs = MemoryFileSystem.test(style: FileSystemStyle.windows);
Cache.flutterRoot = Cache.defaultFlutterRoot( Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform, platform: platform,
fileSystem: fs, fileSystem: fs,
userMessages: UserMessages() userMessages: UserMessages()
); );
fs.file('.packages').createSync(); fs.file('.packages').createSync();
fs.file('pubspec.yaml').writeAsStringSync( fs.file('pubspec.yaml').writeAsStringSync(
''' '''
name: test name: test
dependencies: dependencies:
flutter:
sdk: flutter
flutter: flutter:
assets: sdk: flutter
- assets/ flutter:
''' assets:
); - assets/
}); '''
);
testWithoutContext('Variant detection works with windows-style filepaths', () async { });
const List<String> assets = <String>[
r'assets\foo.jpg', testWithoutContext('Variant detection works with windows-style filepaths', () async {
r'assets\2x\foo.jpg', const List<String> assets = <String>[
r'assets\somewhereElse\bar.jpg', r'assets\foo.jpg',
r'assets\somewhereElse\2x\bar.jpg', r'assets\2x\foo.jpg',
]; r'assets\somewhereElse\bar.jpg',
r'assets\somewhereElse\2x\bar.jpg',
for (final String asset in assets) { ];
final File assetFile = fs.file(asset);
assetFile.createSync(recursive: true); for (final String asset in assets) {
assetFile.writeAsStringSync(asset); final File assetFile = fs.file(asset);
} assetFile.createSync(recursive: true);
assetFile.writeAsStringSync(asset);
final ManifestAssetBundle bundle = ManifestAssetBundle( }
logger: BufferLogger.test(),
fileSystem: fs, final ManifestAssetBundle bundle = ManifestAssetBundle(
platform: platform, logger: BufferLogger.test(),
); fileSystem: fs,
platform: platform,
await bundle.build( );
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), 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 Map<String, List<String>> manifest = await extractAssetManifestFromBundle(bundle);
final String manifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(manifest, equals(expectedManifest)); 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']));
}); });
}); });
} }
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