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 @@
// found in the LICENSE file.
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 '../common.dart';
......@@ -18,14 +18,16 @@ void main() async {
final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
WidgetsFlutterBinding.ensureInitialized();
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();
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
AssetImage.parseAssetManifest(assetManifest);
await AssetImage.manifestParser(json);
}
watch.stop();
......@@ -38,49 +40,3 @@ void main() async {
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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_gallery/gallery/example_code_parser.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -60,9 +58,4 @@ class TestAssetBundle extends AssetBundle {
@override
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';
import 'image_provider.dart';
const String _kLegacyAssetManifestFilename = 'AssetManifest.json';
const String _kAssetManifestFilename = 'AssetManifest.bin';
const String _kAssetManifestFileName = 'AssetManifest.json';
/// A screen with a device-pixel ratio strictly less than this value is
/// considered a low-resolution screen (typically entry-level to mid-range
......@@ -285,45 +284,18 @@ class AssetImage extends AssetBundleImageProvider {
Completer<AssetBundleImageKey>? completer;
Future<AssetBundleImageKey>? result;
Future<_AssetManifest> loadJsonAssetManifest() {
Future<_AssetManifest> parseJson(String data) {
final _AssetManifest parsed = _LegacyAssetManifest.fromJsonString(data);
return SynchronousFuture<_AssetManifest>(parsed);
}
return chosenBundle.loadStructuredData(_kLegacyAssetManifestFilename, parseJson);
}
// TODO(andrewkolos): Once google3 and google-fonts-flutter are migrated
// away from using AssetManifest.json, remove all references to it.
// See https://github.com/flutter/flutter/issues/114913.
Future<_AssetManifest>? manifest;
// Since AssetBundle load calls can be synchronous (e.g. in the case of tests),
// it is not sufficient to only use catchError/onError or the onError parameter
// of Future.then--we also have to use a synchronous try/catch. Once google3
// tooling starts producing AssetManifest.bin, this block can be removed.
try {
manifest = chosenBundle.loadStructuredBinaryData(_kAssetManifestFilename, _AssetManifestBin.fromStandardMessageCodecMessage);
} catch (error) {
manifest = loadJsonAssetManifest();
}
manifest
// To understand why we use this no-op `then` instead of `catchError`/`onError`,
// see https://github.com/flutter/flutter/issues/115601
.then((_AssetManifest manifest) => manifest,
onError: (Object? error, StackTrace? stack) => loadJsonAssetManifest())
.then((_AssetManifest manifest) {
final List<_AssetVariant> candidateVariants = manifest.getVariants(keyName);
final _AssetVariant chosenVariant = _chooseVariant(
chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>(
(Map<String, List<String>>? manifest) {
final String chosenName = _chooseVariant(
keyName,
configuration,
candidateVariants,
);
manifest == null ? null : manifest[keyName],
)!;
final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = AssetBundleImageKey(
bundle: chosenBundle,
name: chosenVariant.asset,
scale: chosenVariant.devicePixelRatio,
name: chosenName,
scale: chosenScale,
);
if (completer != null) {
// We already returned from this function, which means we are in the
......@@ -337,15 +309,14 @@ class AssetImage extends AssetBundleImageProvider {
// ourselves.
result = SynchronousFuture<AssetBundleImageKey>(key);
}
})
.onError((Object error, StackTrace stack) {
// We had an error. (This guarantees we weren't called synchronously.)
// Forward the error to the caller.
assert(completer != null);
assert(result == null);
completer!.completeError(error, stack);
});
},
).catchError((Object error, StackTrace stack) {
// We had an error. (This guarantees we weren't called synchronously.)
// Forward the error to the caller.
assert(completer != null);
assert(result == null);
completer!.completeError(error, stack);
});
if (result != null) {
// The code above ran synchronously, and came up with an answer.
// Return the SynchronousFuture that we created above.
......@@ -357,29 +328,35 @@ class AssetImage extends AssetBundleImageProvider {
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
// Return type is set to Object?, because the specific type is private.
static Object? parseAssetManifest(ByteData bytes) {
return _AssetManifestBin.fromStandardMessageCodecMessage(bytes);
static Future<Map<String, List<String>>?> manifestParser(String? jsonData) {
if (jsonData == null) {
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) {
final _AssetVariant mainAsset = _AssetVariant(asset: mainAssetKey,
devicePixelRatio: _naturalResolution);
if (config.devicePixelRatio == null || candidateVariants.isEmpty) {
return mainAsset;
String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) {
if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
return main;
}
final SplayTreeMap<double, _AssetVariant> candidatesByDevicePixelRatio =
SplayTreeMap<double, _AssetVariant>();
for (final _AssetVariant candidate in candidateVariants) {
candidatesByDevicePixelRatio[candidate.devicePixelRatio] = candidate;
// TODO(ianh): Consider moving this parsing logic into _manifestParser.
final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
for (final String candidate in candidates) {
mapping[_parseScale(candidate)] = candidate;
}
candidatesByDevicePixelRatio.putIfAbsent(_naturalResolution, () => mainAsset);
// TODO(ianh): implement support for config.locale, config.textDirection,
// config.size, config.platform (then document this over in the Image.asset
// docs)
return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
return _findBestVariant(mapping, config.devicePixelRatio!);
}
// Returns the "best" asset variant amongst the available `candidates`.
......@@ -394,17 +371,17 @@ class AssetImage extends AssetBundleImageProvider {
// lowest key higher than `value`.
// - If the screen has high device pixel ratio, choose the variant with the
// key nearest to `value`.
_AssetVariant _findBestVariant(SplayTreeMap<double, _AssetVariant> candidatesByDpr, double value) {
if (candidatesByDpr.containsKey(value)) {
return candidatesByDpr[value]!;
String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
if (candidates.containsKey(value)) {
return candidates[value]!;
}
final double? lower = candidatesByDpr.lastKeyBefore(value);
final double? upper = candidatesByDpr.firstKeyAfter(value);
final double? lower = candidates.lastKeyBefore(value);
final double? upper = candidates.firstKeyAfter(value);
if (lower == null) {
return candidatesByDpr[upper]!;
return candidates[upper];
}
if (upper == null) {
return candidatesByDpr[lower]!;
return candidates[lower];
}
// On screens with low device-pixel ratios the artifacts from upscaling
......@@ -412,116 +389,20 @@ class AssetImage extends AssetBundleImageProvider {
// ratios because the physical pixels are larger. Choose the higher
// resolution image in that case instead of the nearest one.
if (value < _kLowDprLimit || value > (lower + upper) / 2) {
return candidatesByDpr[upper]!;
return candidates[upper];
} 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 const double _naturalResolution = 1.0;
@override
List<_AssetVariant> getVariants(String key) {
return manifest[key] ?? const <_AssetVariant>[];
}
static double _parseScale(String mainAsset, String variant) {
// The legacy asset manifest includes the main asset within its variant list.
if (mainAsset == variant) {
double _parseScale(String key) {
if (key == assetName) {
return _naturalResolution;
}
final Uri assetUri = Uri.parse(variant);
final Uri assetUri = Uri.parse(key);
String directoryPath = '';
if (assetUri.pathSegments.length > 1) {
directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
......@@ -531,23 +412,22 @@ class _LegacyAssetManifest implements _AssetManifest {
if (match != null && match.groupCount > 0) {
return double.parse(match.group(1)!);
}
return _naturalResolution; // i.e. default to 1.0x
}
}
class _AssetVariant {
_AssetVariant({
required this.asset,
required this.devicePixelRatio,
});
factory _AssetVariant.fromManifestData(Object data) {
final Map<Object?, Object?> asStructuredData = data as Map<Object?, Object?>;
return _AssetVariant(asset: asStructuredData['asset']! as String,
devicePixelRatio: asStructuredData['dpr']! as double);
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is AssetImage
&& other.keyName == keyName
&& other.bundle == bundle;
}
final double devicePixelRatio;
final String asset;
@override
int get hashCode => Object.hash(keyName, bundle);
@override
String toString() => '${objectRuntimeType(this, 'AssetImage')}(bundle: $bundle, name: "$keyName")';
}
......@@ -96,25 +96,12 @@ abstract class AssetBundle {
}
/// 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
/// used with one parser for the lifetime of the asset bundle.
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
/// 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.
......@@ -169,18 +156,6 @@ class NetworkAssetBundle extends AssetBundle {
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
// should implement evict().
......@@ -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
final Map<String, Future<String>> _stringCache = <String, Future<String>>{};
final Map<String, Future<dynamic>> _structuredDataCache = <String, Future<dynamic>>{};
final Map<String, Future<dynamic>> _structuredBinaryDataCache = <String, Future<dynamic>>{};
@override
Future<String> loadString(String key, { bool cache = true }) {
......@@ -251,69 +225,16 @@ abstract class CachingAssetBundle extends AssetBundle {
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
void evict(String key) {
_stringCache.remove(key);
_structuredDataCache.remove(key);
_structuredBinaryDataCache.remove(key);
}
@override
void clear() {
_stringCache.clear();
_structuredDataCache.clear();
_structuredBinaryDataCache.clear();
}
@override
......@@ -355,7 +276,7 @@ class PlatformAssetBundle extends CachingAssetBundle {
bool debugUsePlatformChannel = false;
assert(() {
// 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
// the old loader behavior that allows tests to load assets from
// the current package using the package prefix.
......
......@@ -13,14 +13,18 @@ import 'package:flutter_test/flutter_test.dart';
class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle(this._assetBundleMap);
final Map<String, List<Map<dynamic, dynamic>>> _assetBundleMap;
final Map<String, List<String>> _assetBundleMap;
Map<String, int> loadCallCount = <String, int>{};
String get _assetBundleContents {
return json.encode(_assetBundleMap);
}
@override
Future<ByteData> load(String key) async {
if (key == 'AssetManifest.bin') {
return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
if (key == 'AssetManifest.json') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer);
}
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
......@@ -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() {
// 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', () {
void buildAndTestWithOneAsset(String mainAssetPath) {
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{};
final Map<String, List<String>> assetBundleMap = <String, List<String>>{};
assetBundleMap[mainAssetPath] = <Map<dynamic,dynamic>>[];
assetBundleMap[mainAssetPath] = <String>[];
final AssetImage assetImage = AssetImage(
mainAssetPath,
......@@ -148,13 +93,10 @@ void main() {
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{};
final Map<String, List<String>> assetBundleMap =
<String, List<String>>{};
final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
mainAssetVariantManifestEntry['asset'] = variantPath;
mainAssetVariantManifestEntry['dpr'] = 3.0;
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......@@ -181,10 +123,10 @@ void main() {
test('When high-res device and high-res asset not present in bundle then return main variant', () {
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{};
final Map<String, List<String>> assetBundleMap =
<String, List<String>>{};
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[];
assetBundleMap[mainAssetPath] = <String>[mainAssetPath];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......@@ -214,18 +156,16 @@ void main() {
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
void buildBundleAndTestVariantLogic(
double deviceRatio,
double chosenAssetRatio,
String expectedAssetPath,
) {
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{};
final Map<String, List<String>> assetBundleMap =
<String, List<String>>{};
final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
mainAssetVariantManifestEntry['asset'] = variantPath;
mainAssetVariantManifestEntry['dpr'] = 3.0;
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......
......@@ -9,14 +9,14 @@ import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
class _TestAssetBundle extends CachingAssetBundle {
class TestAssetBundle extends CachingAssetBundle {
Map<String, int> loadCallCount = <String, int>{};
@override
Future<ByteData> load(String key) async {
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
if (key == 'AssetManifest.bin') {
return const StandardMessageCodec().encodeMessage(json.decode('{"one":[]}'))!;
if (key == 'AssetManifest.json') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert('{"one": ["one"]}')).buffer);
}
if (key == 'one') {
......@@ -30,7 +30,7 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('Caching asset bundle test', () async {
final _TestAssetBundle bundle = _TestAssetBundle();
final TestAssetBundle bundle = TestAssetBundle();
final ByteData assetData = await bundle.load('one');
expect(assetData.getInt8(0), equals(49));
......@@ -53,7 +53,7 @@ void main() {
test('AssetImage.obtainKey succeeds with ImageConfiguration.empty', () async {
// 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);
expect(key.name, 'one');
expect(key.scale, 1.0);
......
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
@TestOn('!chrome')
import 'dart:convert';
import 'dart:ui' as ui show Image;
import 'package:flutter/foundation.dart';
......@@ -17,32 +16,27 @@ import '../image_data.dart';
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
double scaleOf(ByteData data) => data.getFloat64(0);
final Map<dynamic, dynamic> testManifest = json.decode('''
const String testManifest = '''
{
"assets/image.png" : [
{"asset": "assets/1.5x/image.png", "dpr": 1.5},
{"asset": "assets/2.0x/image.png", "dpr": 2.0},
{"asset": "assets/3.0x/image.png", "dpr": 3.0},
{"asset": "assets/4.0x/image.png", "dpr": 4.0}
"assets/image.png",
"assets/1.5x/image.png",
"assets/2.0x/image.png",
"assets/3.0x/image.png",
"assets/4.0x/image.png"
]
}
''') as Map<dynamic, dynamic>;
''';
class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle({ this.manifest = testManifest });
TestAssetBundle({ required Map<dynamic, dynamic> manifest }) {
this.manifest = const StandardMessageCodec().encodeMessage(manifest)!;
}
late final ByteData manifest;
final String manifest;
@override
Future<ByteData> load(String key) {
late ByteData data;
switch (key) {
case 'AssetManifest.bin':
data = manifest;
break;
case 'assets/image.png':
data = testByteData(1.0);
break;
......@@ -65,6 +59,14 @@ class TestAssetBundle extends CachingAssetBundle {
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
String toString() => '${describeIdentity(this)}()';
}
......@@ -104,7 +106,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize
devicePixelRatio: ratio,
),
child: DefaultAssetBundle(
bundle: bundle ?? TestAssetBundle(manifest: testManifest),
bundle: bundle ?? TestAssetBundle(),
child: Center(
child: inferSize ?
Image(
......@@ -257,21 +259,46 @@ void main() {
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 {
// If both a main asset and a 1.0x asset are specified, then prefer
// the 1.0x asset.
final Map<dynamic, dynamic> manifest = json.decode('''
const String manifest = '''
{
"assets/image.png" : [
{"asset": "assets/1.0x/image.png", "dpr":1.0},
{"asset": "assets/1.5x/image.png", "dpr":1.5},
{"asset": "assets/2.0x/image.png", "dpr":2.0},
{"asset": "assets/3.0x/image.png", "dpr":3.0},
{"asset": "assets/4.0x/image.png", "dpr":4.0}
"assets/image.png",
"assets/1.0x/image.png",
"assets/1.5x/image.png",
"assets/2.0x/image.png",
"assets/3.0x/image.png",
"assets/4.0x/image.png"
]
}
''') as Map<dynamic, dynamic>;
''';
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
const double ratio = 1.0;
......@@ -310,13 +337,14 @@ void main() {
// if higher resolution assets are not available we will pick the best
// available.
testWidgets('Low-resolution assets', (WidgetTester tester) async {
final AssetBundle bundle = TestAssetBundle(manifest: json.decode('''
final AssetBundle bundle = TestAssetBundle(manifest: '''
{
"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 {
Key key = GlobalKey();
......
......@@ -2,11 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:standard_message_codec/standard_message_codec.dart';
import 'base/context.dart';
import 'base/deferred_component.dart';
......@@ -139,9 +136,6 @@ class ManifestAssetBundle implements AssetBundle {
_splitDeferredAssets = splitDeferredAssets,
_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 FileSystem _fileSystem;
final LicenseCollector _licenseCollector;
......@@ -167,8 +161,7 @@ class ManifestAssetBundle implements AssetBundle {
DateTime? _lastBuildTimestamp;
static const String _kAssetManifestBinFileName = 'AssetManifest.bin';
static const String _kAssetManifestJsonFileName = 'AssetManifest.json';
static const String _kAssetManifestJson = 'AssetManifest.json';
static const String _kNoticeFile = 'NOTICES';
// 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,
......@@ -236,13 +229,8 @@ class ManifestAssetBundle implements AssetBundle {
// device.
_lastBuildTimestamp = DateTime.now();
if (flutterManifest.isEmpty) {
entries[_kAssetManifestJsonFileName] = DevFSStringContent('{}');
entryKinds[_kAssetManifestJsonFileName] = AssetKind.regular;
final ByteData emptyAssetManifest =
const StandardMessageCodec().encodeMessage(<dynamic, dynamic>{})!;
entries[_kAssetManifestBinFileName] =
DevFSByteContent(emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes));
entryKinds[_kAssetManifestBinFileName] = AssetKind.regular;
entries[_kAssetManifestJson] = DevFSStringContent('{}');
entryKinds[_kAssetManifestJson] = AssetKind.regular;
return 0;
}
......@@ -439,11 +427,7 @@ class ManifestAssetBundle implements AssetBundle {
_wildcardDirectories[uri] ??= _fileSystem.directory(uri);
}
final Map<String, List<String>> assetManifest =
_createAssetManifest(assetVariants, deferredComponentsAssetVariants);
final DevFSStringContent assetManifestJson = DevFSStringContent(json.encode(assetManifest));
final DevFSByteContent assetManifestBinary = _createAssetManifestBinary(assetManifest);
final DevFSStringContent assetManifest = _createAssetManifest(assetVariants, deferredComponentsAssetVariants);
final DevFSStringContent fontManifest = DevFSStringContent(json.encode(fonts));
final LicenseResult licenseResult = _licenseCollector.obtainLicenses(packageConfig, additionalLicenseFiles);
if (licenseResult.errorMessages.isNotEmpty) {
......@@ -467,8 +451,7 @@ class ManifestAssetBundle implements AssetBundle {
_fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute);
}
_setIfChanged(_kAssetManifestJsonFileName, assetManifestJson, AssetKind.regular);
_setIfChanged(_kAssetManifestBinFileName, assetManifestBinary, AssetKind.regular);
_setIfChanged(_kAssetManifestJson, assetManifest, AssetKind.regular);
_setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular);
_setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform);
return 0;
......@@ -477,31 +460,17 @@ class ManifestAssetBundle implements AssetBundle {
@override
List<File> additionalDependencies = <File>[];
void _setIfChanged(String key, DevFSContent content, AssetKind assetKind) {
bool areEqual(List<int> o1, List<int> o2) {
if (o1.length != o2.length) {
return false;
}
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)) {
void _setIfChanged(String key, DevFSStringContent content, AssetKind assetKind) {
if (!entries.containsKey(key)) {
entries[key] = content;
entryKinds[key] = assetKind;
return;
}
entries[key] = content;
entryKinds[key] = assetKind;
final DevFSStringContent? oldContent = entries[key] as DevFSStringContent?;
if (oldContent?.string != content.string) {
entries[key] = content;
entryKinds[key] = assetKind;
}
}
void _setLicenseIfChanged(
......@@ -653,14 +622,14 @@ class ManifestAssetBundle implements AssetBundle {
return deferredComponentsAssetVariants;
}
Map<String, List<String>> _createAssetManifest(
DevFSStringContent _createAssetManifest(
Map<_Asset, List<_Asset>> assetVariants,
Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants
) {
final Map<String, List<String>> manifest = <String, List<String>>{};
final Map<_Asset, List<String>> entries = <_Asset, List<String>>{};
final Map<String, List<String>> jsonObject = <String, List<String>>{};
final Map<_Asset, List<String>> jsonEntries = <_Asset, List<String>>{};
assetVariants.forEach((_Asset main, List<_Asset> variants) {
entries[main] = <String>[
jsonEntries[main] = <String>[
for (final _Asset variant in variants)
variant.entryUri.path,
];
......@@ -668,69 +637,26 @@ class ManifestAssetBundle implements AssetBundle {
if (deferredComponentsAssetVariants != null) {
for (final Map<_Asset, List<_Asset>> componentAssets in deferredComponentsAssetVariants.values) {
componentAssets.forEach((_Asset main, List<_Asset> variants) {
entries[main] = <String>[
jsonEntries[main] = <String>[
for (final _Asset variant in variants)
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));
for (final _Asset main in sortedKeys) {
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
.map((String value) => Uri.decodeFull(value))
.toList();
manifest[decodedEntryPath] = decodedEntryVariantPaths;
}
return manifest;
}
DevFSByteContent _createAssetManifestBinary(
Map<String, List<String>> assetManifest
) {
double parseScale(String key) {
final Uri assetUri = Uri.parse(key);
String directoryPath = '';
if (assetUri.pathSegments.length > 1) {
directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
}
final Match? match = _extractRatioRegExp.firstMatch(directoryPath);
if (match != null && match.groupCount > 0) {
return double.parse(match.group(1)!);
}
return _defaultResolution;
}
final Map<String, dynamic> result = <String, dynamic>{};
for (final MapEntry<String, dynamic> manifestEntry in assetManifest.entries) {
final List<dynamic> resultVariants = <dynamic>[];
final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>();
for (final String variant in entries) {
if (variant == manifestEntry.key) {
// With the newer binary format, don't include the main asset in it's
// list of variants. This reduces parsing time at runtime.
continue;
}
final Map<String, dynamic> resultVariant = <String, dynamic>{};
final double variantDevicePixelRatio = parseScale(variant);
resultVariant['asset'] = variant;
resultVariant['dpr'] = variantDevicePixelRatio;
resultVariants.add(resultVariant);
}
result[manifestEntry.key] = resultVariants;
jsonObject[decodedEntryPath] = decodedEntryVariantPaths;
}
final ByteData message = const StandardMessageCodec().encodeMessage(result)!;
return DevFSByteContent(message.buffer.asUint8List(0, message.lengthInBytes));
return DevFSStringContent(json.encode(jsonObject));
}
static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
/// Prefixes family names and asset paths of fonts included from packages with
/// 'packages/<package_name>'
List<Font> _parsePackageFonts(
......
......@@ -57,8 +57,6 @@ dependencies:
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"
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"
......@@ -90,6 +88,7 @@ dependencies:
watcher: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
dev_dependencies:
collection: 1.17.0
file_testing: 3.0.0
pubspec_parse: 1.2.1
......@@ -98,10 +97,9 @@ dev_dependencies:
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"
test: 1.22.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
collection: 1.17.0
dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
# PUBSPEC CHECKSUM: 408d
# PUBSPEC CHECKSUM: 65eb
......@@ -220,7 +220,7 @@ loading-units-spelled-wrong:
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, 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 {
......@@ -382,7 +382,7 @@ loading-units:
validator.displayResults();
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 {
......@@ -448,10 +448,9 @@ loading-units:
.childDirectory('main')
.childFile('AndroidManifest.xml');
expect(manifestOutput.existsSync(), true);
final String manifestOutputString = manifestOutput.readAsStringSync();
expect(manifestOutputString, contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'));
expect(manifestOutputString, isNot(contains('android:value="invalidmapping"')));
expect(manifestOutputString, contains("<!-- Don't delete the meta-data below."));
expect(manifestOutput.readAsStringSync().contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'), true);
expect(manifestOutput.readAsStringSync().contains('android:value="invalidmapping"'), false);
expect(manifestOutput.readAsStringSync().contains("<!-- Don't delete the meta-data below."), true);
});
testWithoutContext('androidStringMapping adds mapping when no existing mapping', () async {
......@@ -696,8 +695,8 @@ loading-units:
.childDirectory('main')
.childFile('AndroidManifest.xml');
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(), isNot(contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"'))));
expect(manifestOutput.readAsStringSync(), contains("<!-- Don't delete the meta-data below."));
expect(manifestOutput.readAsStringSync().contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'), true);
expect(manifestOutput.readAsStringSync().contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"')), false);
expect(manifestOutput.readAsStringSync().contains("<!-- Don't delete the meta-data below."), true);
});
}
......@@ -111,9 +111,8 @@ $fontsSection
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, containsAll(
<String>['AssetManifest.bin', 'AssetManifest.json', 'FontManifest.json', 'NOTICES.Z']
));
expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
expect(bundle.entries.containsKey('FontManifest.json'), isTrue);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
......
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:convert';
import 'dart:typed_data';
import 'package:file/file.dart';
import 'package:file/memory.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/globals.dart' as globals;
import 'package:standard_message_codec/standard_message_codec.dart';
import '../src/common.dart';
import '../src/context.dart';
......@@ -26,7 +24,6 @@ void main() {
// rolls into Flutter.
return path.replaceAll('/', globals.fs.path.separator);
}
void writePubspecFile(String path, String name, { List<String>? assets }) {
String assetsSection;
if (assets == null) {
......@@ -63,6 +60,37 @@ $assetsSection
..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) {
for (final String asset in assets) {
final String fullPath = fixPath(globals.fs.path.join(path, asset));
......@@ -73,391 +101,182 @@ $assetsSection
}
}
// TODO(andrewkolos): Delete this group once we stop producing AssetManifest.json
// as part of build.
group('Legacy asset manifest (AssetManifest.json)', () {
Future<void> buildAndVerifyAssets(
List<String> assets,
List<String> packages,
String? expectedAssetManifest, {
bool expectExists = true,
}) async {
late FileSystem testFileSystem;
setUp(() async {
testFileSystem = MemoryFileSystem(
style: globals.platform.isWindows
? FileSystemStyle.windows
: FileSystemStyle.posix,
);
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();
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) {
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,
);
}
}
}
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');
if (expectExists) {
expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedAssetManifest,
);
}
}
final List<String> assets = <String>['a/foo'];
writeAssets('p/p/', assets);
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
const String expectedAssetManifest = '{}';
expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedAssetManifest,
);
expect(
utf8.decode(await bundle.entries['FontManifest.json']!.contentsAsBytes()),
'[]',
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the package has and lists one '
'asset its pubspec', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assets = <String>['a/foo'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assets,
);
writeAssets('p/p/', assets);
const String expectedAssetManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled when the package has one asset, '
"listed in the app's pubspec", () async {
final List<String> assetEntries = <String>['packages/test_package/a/foo'];
writePubspecFile(
'pubspec.yaml',
'test',
assets: assetEntries,
);
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final List<String> assets = <String>['a/foo'];
writeAssets('p/p/lib/', assets);
const String expectedAssetManifest = '{"packages/test_package/a/foo":'
'["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
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 {
testFileSystem = MemoryFileSystem(
style: globals.platform.isWindows
? FileSystemStyle.windows
: FileSystemStyle.posix,
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,
);
testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
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();
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('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']
));
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('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('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');
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(
'p/p/pubspec.yaml',
'test_package',
......@@ -466,8 +285,8 @@ $assetsSection
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]"]}';
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
assets,
......@@ -476,584 +295,283 @@ $assetsSection
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
ProcessManager: () => FakeProcessManager.any(),
});
group('AssetBundle assets from scanned paths', () {
testUsingContext('Two assets are bundled when scanning their directory', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo', 'a/bar'];
final List<String> assetsOnManifest = <String>['a/'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetsOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
await buildAndVerifyAssets(
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,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":["packages/test_package/a/foo"],'
'"packages/test_package/abc/bar":["packages/test_package/abc/bar"]}';
await buildAndVerifyAssets(
assetsOnDisk,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled with variant, scanning wrong directory', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo','a/b/foo','a/bar'];
final List<String> assetOnManifest = <String>['a','a/bar']; // can't list 'a' as asset, should be 'a/'
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries['AssetManifest.bin'], isNull,
reason: 'Invalid pubspec.yaml should not generate AssetManifest.bin' );
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext("Two assets are bundled when 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(),
});
group('AssetBundle assets from scanned paths with MemoryFileSystem', () {
testUsingContext('One asset is bundled with variant, scanning directory', () 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>['a/',];
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/a/2x/foo"]}';
await buildAndVerifyAssets(
assetsOnDisk,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('No asset is bundled with variant, no assets or directories are listed', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo', 'a/2x/foo'];
final List<String> assetOnManifest = <String>[];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest = '{}';
await buildAndVerifyAssets(
assetOnManifest,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Expect error generating manifest, wrong non-existing directory is listed', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetOnManifest = <String>['c/'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
await buildAndVerifyAssets(
assetOnManifest,
<String>['test_package'],
null,
expectExists: false,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
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(),
});
});
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(
List<String> assets,
List<String> packages,
String? expectedAssetManifest, {
bool expectExists = true,
}) async {
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
testUsingContext('Asset paths can contain URL reserved characters', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assets = <String>['a/foo', 'a/foo [x]'];
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"],'
'"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) {
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,
);
}
}
}
group('AssetBundle assets from scanned paths', () {
testUsingContext('Two assets are bundled when scanning their directory', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
if (expectExists) {
final String actualAssetManifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(
actualAssetManifest,
expectedAssetManifest,
);
}
}
final List<String> assetsOnDisk = <String>['a/foo', 'a/bar'];
final List<String> assetsOnManifest = <String>['a/'];
void writeAssets(String path, List<String> assets) {
for (final String asset in assets) {
final String fullPath = fixPath(globals.fs.path.join(path, asset));
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetsOnManifest,
);
globals.fs.file(fullPath)
..createSync(recursive: true)
..writeAsStringSync(asset);
}
}
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
'"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
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 {
testFileSystem = MemoryFileSystem(
style: globals.platform.isWindows
? FileSystemStyle.windows
: FileSystemStyle.posix,
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,
);
testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
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();
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('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('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.json'], isNull,
reason: 'Invalid pubspec.yaml should not generate AssetManifest.json' );
}, 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');
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(
'p/p/pubspec.yaml',
'test_package',
assets: assets,
assets: assetOnManifest,
);
writeAssets('p/p/', assets);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":[],'
'"packages/test_package/a/foo [x]":[]}';
'{"packages/test_package/a/foo":["packages/test_package/a/foo","packages/test_package/a/2x/foo"]}';
await buildAndVerifyAssets(
assets,
assetsOnDisk,
<String>['test_package'],
expectedAssetManifest,
);
......@@ -1062,165 +580,53 @@ $assetsSection
ProcessManager: () => FakeProcessManager.any(),
});
group('AssetBundle assets from scanned paths', () {
testUsingContext('Two assets are bundled when scanning their directory', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo', 'a/bar'];
final List<String> assetsOnManifest = <String>['a/'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetsOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/bar":[],'
'"packages/test_package/a/foo":[]}';
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,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":[],'
'"packages/test_package/abc/bar":[]}';
await buildAndVerifyAssets(
assetsOnDisk,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('One asset is bundled with variant, scanning wrong directory', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo','a/b/foo','a/bar'];
final List<String> assetOnManifest = <String>['a','a/bar']; // can't list 'a' as asset, should be 'a/'
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries['AssetManifest.bin'], isNull,
reason: 'Invalid pubspec.yaml should not generate AssetManifest.bin' );
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('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(),
});
group('AssetBundle assets from scanned paths with MemoryFileSystem', () {
testUsingContext('One asset is bundled with variant, scanning directory', () 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>['a/',];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest =
'{"packages/test_package/a/foo":[{"asset":"packages/test_package/a/2x/foo","dpr":2.0}]}';
await buildAndVerifyAssets(
assetsOnDisk,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('No asset is bundled with variant, no assets or directories are listed', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetsOnDisk = <String>['a/foo', 'a/2x/foo'];
final List<String> assetOnManifest = <String>[];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
writeAssets('p/p/', assetsOnDisk);
const String expectedAssetManifest = '{}';
await buildAndVerifyAssets(
assetOnManifest,
<String>['test_package'],
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Expect error generating manifest, wrong non-existing directory is listed', () async {
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final List<String> assetOnManifest = <String>['c/'];
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
assets: assetOnManifest,
);
await buildAndVerifyAssets(
assetOnManifest,
<String>['test_package'],
null,
expectExists: false,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
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 @@
// found in the LICENSE file.
import 'dart:convert';
import 'dart:typed_data';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.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/devfs.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:standard_message_codec/standard_message_codec.dart';
import '../src/common.dart';
import '../src/context.dart';
......@@ -50,16 +48,11 @@ void main() {
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json', 'AssetManifest.bin']));
const String expectedJsonAssetManifest = '{}';
expect(bundle.entries.length, 1);
const String expectedAssetManifest = '{}';
expect(
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
expectedJsonAssetManifest,
);
const String expectedBinAssetManifest = '{}';
expect(
await _extractBinAssetManifestFromBundleAsJson(bundle),
expectedBinAssetManifest
expectedAssetManifest,
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
......@@ -79,8 +72,12 @@ flutter:
''');
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// Expected assets:
// - asset manifest
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
expect(bundle.needsBuild(), false);
// Simulate modifying the files by updating the filestat time manually.
......@@ -90,9 +87,13 @@ flutter:
expect(bundle.needsBuild(), true);
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt',
'assets/foo/fizz.txt']));
// Expected assets:
// - asset manifest
// - font manifest
// - license file
// - assets/foo/bar.txt
// - assets/foo/fizz.txt
expect(bundle.entries.length, 5);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
......@@ -111,8 +112,12 @@ flutter:
globals.fs.file('.packages').createSync();
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// Expected assets:
// - asset manifest
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
expect(bundle.needsBuild(), false);
// Delete the wildcard directory and update pubspec file.
......@@ -133,8 +138,12 @@ name: example''')
// supporting file deletion.
expect(bundle.needsBuild(), true);
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// Expected assets:
// - asset manifest
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
......@@ -157,8 +166,12 @@ flutter:
globals.fs.file('.packages').createSync();
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// Expected assets:
// - asset manifest
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
expect(bundle.needsBuild(), false);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
......@@ -190,8 +203,12 @@ flutter:
splitDeferredAssets: true,
).createBundle();
await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true);
expect(bundle.entries.keys, unorderedEquals(<String>['AssetManifest.json',
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z', 'assets/foo/bar.txt']));
// Expected assets:
// - asset manifest
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
expect(bundle.deferredComponentsEntries.length, 1);
expect(bundle.deferredComponentsEntries['component1']!.length, 2);
expect(bundle.needsBuild(), false);
......@@ -220,9 +237,12 @@ flutter:
''');
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['assets/foo/bar.txt',
'assets/bar/barbie.txt', 'assets/wild/dash.txt', 'AssetManifest.json',
'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
// Expected assets:
// - asset manifest
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 6);
expect(bundle.deferredComponentsEntries.isEmpty, true);
expect(bundle.needsBuild(), false);
}, overrides: <Type, Generator>{
......@@ -255,11 +275,14 @@ flutter:
splitDeferredAssets: true,
).createBundle();
await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true);
expect(bundle.entries.keys, unorderedEquals(<String>['assets/foo/bar.txt',
'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
expect(bundle.deferredComponentsEntries.keys, unorderedEquals(<String>['component1']));
expect(bundle.deferredComponentsEntries['component1']!.keys,
unorderedEquals(<String>['assets/bar/barbie.txt', 'assets/wild/dash.txt']));
// Expected assets:
// - asset manifest
// - font manifest
// - license file
// - assets/foo/bar.txt
expect(bundle.entries.length, 4);
expect(bundle.deferredComponentsEntries.length, 1);
expect(bundle.deferredComponentsEntries['component1']!.length, 2);
expect(bundle.needsBuild(), false);
// Simulate modifying the files by updating the filestat time manually.
......@@ -270,13 +293,9 @@ flutter:
expect(bundle.needsBuild(), true);
await bundle.build(packagesPath: '.packages', deferredComponentsEnabled: true);
expect(bundle.entries.keys, unorderedEquals(<String>['assets/foo/bar.txt',
'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
expect(bundle.entries.length, 4);
expect(bundle.deferredComponentsEntries.length, 1);
expect(bundle.deferredComponentsEntries['component1']!.keys,
unorderedEquals(<String>['assets/bar/barbie.txt', 'assets/wild/dash.txt',
'assets/wild/fizz.txt']));
expect(bundle.deferredComponentsEntries['component1']!.length, 3);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
......@@ -316,8 +335,7 @@ assets:
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
final DevFSContent? assetManifestBin = bundle.entries['AssetManifest.bin'];
final DevFSStringContent? assetManifestJson = bundle.entries['AssetManifest.json']
final DevFSStringContent? assetManifest = bundle.entries['AssetManifest.json']
as DevFSStringContent?;
final DevFSStringContent? fontManifest = bundle.entries['FontManifest.json']
as DevFSStringContent?;
......@@ -326,8 +344,7 @@ assets:
await bundle.build(packagesPath: '.packages');
expect(assetManifestBin, bundle.entries['AssetManifest.bin']);
expect(assetManifestJson, bundle.entries['AssetManifest.json']);
expect(assetManifest, bundle.entries['AssetManifest.json']);
expect(fontManifest, bundle.entries['FontManifest.json']);
expect(license, bundle.entries['NOTICES']);
}, overrides: <Type, Generator>{
......@@ -622,8 +639,7 @@ flutter:
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.keys, unorderedEquals(<String>['packages/foo/bar/fizz.txt',
'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
expect(bundle.entries, hasLength(4));
expect(bundle.needsBuild(), false);
// Does not track dependency's wildcard directories.
......@@ -723,7 +739,6 @@ flutter:
expect(await bundle.build(packagesPath: '.packages'), 0);
expect((bundle.entries['FontManifest.json']! as DevFSStringContent).string, '[]');
expect((bundle.entries['AssetManifest.json']! as DevFSStringContent).string, '{}');
expect(await _extractBinAssetManifestFromBundleAsJson(bundle), '{}');
expect(testLogger.errorText, contains(
'package:foo has `uses-material-design: true` set'
));
......@@ -759,8 +774,7 @@ flutter:
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
expect(await bundle.build(packagesPath: '.packages'), 0);
expect(bundle.entries.keys, unorderedEquals(<String>['assets/foo.txt',
'AssetManifest.json', 'AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']));
expect(bundle.entries.length, 4);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
......@@ -798,17 +812,9 @@ flutter:
// The assets from deferred components and regular assets
// 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(await _extractBinAssetManifestFromBundleAsJson(bundle), '{"assets/apple.jpg":[],"assets/bar.jpg":[],"assets/foo.jpg":[],"assets/zebra.jpg":[]}');
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
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 @@
// found in the LICENSE file.
import 'dart:convert';
import 'dart:typed_data';
import 'package:file/file.dart';
import 'package:file/memory.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/project.dart';
import 'package:standard_message_codec/standard_message_codec.dart';
import '../src/common.dart';
void main() {
// TODO(andrewkolos): Delete this group once we stop producing AssetManifest.json
// as part of build.
group('Legacy asset manifest (AssetManifest.json)', () {
Future<Map<String, List<String>>> extractAssetManifestFromBundle(ManifestAssetBundle bundle) async {
final String manifestJson = utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes());
final Map<String, dynamic> parsedJson = json.decode(manifestJson) 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>),
};
return parsedManifest;
}
group('AssetBundle asset variants (with Unix-style paths)', () {
late Platform platform;
late FileSystem fs;
setUp(() {
platform = FakePlatform();
fs = MemoryFileSystem.test();
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fs,
userMessages: UserMessages()
);
fs.file('.packages').createSync();
fs.file('pubspec.yaml').writeAsStringSync(
'''
name: test
dependencies:
flutter:
sdk: flutter
Future<Map<String, List<String>>> extractAssetManifestFromBundle(ManifestAssetBundle bundle) async {
final String manifestJson = utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes());
final Map<String, dynamic> parsedJson = json.decode(manifestJson) 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>),
};
return parsedManifest;
}
group('AssetBundle asset variants (with Unix-style paths)', () {
late Platform platform;
late FileSystem fs;
setUp(() {
platform = FakePlatform();
fs = MemoryFileSystem.test();
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fs,
userMessages: UserMessages()
);
fs.file('.packages').createSync();
fs.file('pubspec.yaml').writeAsStringSync(
'''
name: test
dependencies:
flutter:
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]));
});
sdk: flutter
flutter:
assets:
- assets/
'''
);
});
testWithoutContext('Only images in folders named with device pixel ratios (e.g. 2x, 3.0x) should be considered as variants of other images', () async {
const String image = 'assets/image.jpg';
const String image2xVariant = 'assets/2x/image.jpg';
const String imageNonVariant = 'assets/notAVariant/image.jpg';
final List<String> assets = <String>[
image,
image2xVariant,
imageNonVariant
];
for (final String asset in assets) {
final File assetFile = fs.file(asset);
assetFile.createSync(recursive: true);
assetFile.writeAsStringSync(asset);
}
final ManifestAssetBundle bundle = ManifestAssetBundle(
logger: BufferLogger.test(),
fileSystem: fs,
platform: platform,
);
await bundle.build(
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
);
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)', () {
late final Platform platform;
late final FileSystem fs;
setUp(() {
platform = FakePlatform(operatingSystem: 'windows');
fs = MemoryFileSystem.test(style: FileSystemStyle.windows);
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fs,
userMessages: UserMessages()
);
fs.file('.packages').createSync();
fs.file('pubspec.yaml').writeAsStringSync(
'''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
assets:
- assets/
'''
);
});
testWithoutContext('Variant detection works with windows-style filepaths', () async {
const List<String> assets = <String>[
r'assets\foo.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);
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']));
});
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]));
});
});
group('Current asset manifest (AssetManifest.bin)', () {
Future<String> extractAssetManifestFromBundleAsJson(ManifestAssetBundle bundle) async {
final List<int> manifestBytes = await bundle.entries['AssetManifest.bin']!.contentsAsBytes();
return json.encode(const StandardMessageCodec().decodeMessage(
ByteData.sublistView(Uint8List.fromList(manifestBytes))
));
}
group('AssetBundle asset variants (with Unix-style paths)', () {
late Platform platform;
late FileSystem fs;
setUp(() {
platform = FakePlatform();
fs = MemoryFileSystem.test();
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fs,
userMessages: UserMessages()
);
fs.file('.packages').createSync();
fs.file('pubspec.yaml').writeAsStringSync(
'''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
assets:
- assets/
'''
);
});
testWithoutContext('Only images in folders named with device pixel ratios (e.g. 2x, 3.0x) should be considered as variants of other images', () async {
const String image = 'assets/image.jpg';
const String image2xVariant = 'assets/2x/image.jpg';
const String imageNonVariant = 'assets/notAVariant/image.jpg';
final List<String> assets = <String>[
image,
image2xVariant,
imageNonVariant
];
for (final String asset in assets) {
final File assetFile = fs.file(asset);
assetFile.createSync(recursive: true);
assetFile.writeAsStringSync(asset);
}
final ManifestAssetBundle bundle = ManifestAssetBundle(
logger: BufferLogger.test(),
fileSystem: fs,
platform: platform,
);
await bundle.build(
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
);
const String expectedManifest = '{"$image":[{"asset":"$image2xVariant","dpr":2.0}],'
'"$imageNonVariant":[]}';
final String manifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(manifest, equals(expectedManifest));
});
testWithoutContext('Asset directories are recursively searched for assets', () async {
const String topLevelImage = 'assets/image.jpg';
const String secondLevelImage = 'assets/folder/secondLevel.jpg';
const String secondLevel2xVariant = 'assets/folder/2x/secondLevel.jpg';
final List<String> assets = <String>[
topLevelImage,
secondLevelImage,
secondLevel2xVariant
];
for (final String asset in assets) {
final File assetFile = fs.file(asset);
assetFile.createSync(recursive: true);
assetFile.writeAsStringSync(asset);
}
final ManifestAssetBundle bundle = ManifestAssetBundle(
logger: BufferLogger.test(),
fileSystem: fs,
platform: platform,
);
await bundle.build(
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
);
const String expectedManifest = '{'
'"$secondLevelImage":[{"asset":"$secondLevel2xVariant","dpr":2.0}],'
'"$topLevelImage":[]'
'}';
final String manifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(manifest, equals(expectedManifest));
});
testWithoutContext('Asset paths should never be URI-encoded', () async {
const String image = 'assets/normalFolder/i have URI-reserved_characters.jpg';
const String imageVariant = 'assets/normalFolder/3x/i have URI-reserved_characters.jpg';
final List<String> assets = <String>[
image,
imageVariant
];
for (final String asset in assets) {
final File assetFile = fs.file(asset);
assetFile.createSync(recursive: true);
assetFile.writeAsStringSync(asset);
}
final ManifestAssetBundle bundle = ManifestAssetBundle(
logger: BufferLogger.test(),
fileSystem: fs,
platform: platform,
);
await bundle.build(
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
);
const String expectedManifest = '{"$image":[{"asset":"$imageVariant","dpr":3.0}]}';
final String manifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(manifest, equals(expectedManifest));
});
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]));
});
});
group('AssetBundle asset variants (with Windows-style filepaths)', () {
late final Platform platform;
late final FileSystem fs;
group('AssetBundle asset variants (with Windows-style filepaths)', () {
late final Platform platform;
late final FileSystem fs;
setUp(() {
platform = FakePlatform(operatingSystem: 'windows');
fs = MemoryFileSystem.test(style: FileSystemStyle.windows);
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fs,
userMessages: UserMessages()
);
setUp(() {
platform = FakePlatform(operatingSystem: 'windows');
fs = MemoryFileSystem.test(style: FileSystemStyle.windows);
Cache.flutterRoot = Cache.defaultFlutterRoot(
platform: platform,
fileSystem: fs,
userMessages: UserMessages()
);
fs.file('.packages').createSync();
fs.file('.packages').createSync();
fs.file('pubspec.yaml').writeAsStringSync(
'''
name: test
dependencies:
flutter:
sdk: flutter
fs.file('pubspec.yaml').writeAsStringSync(
'''
name: test
dependencies:
flutter:
assets:
- assets/
'''
);
});
testWithoutContext('Variant detection works with windows-style filepaths', () async {
const List<String> assets = <String>[
r'assets\foo.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);
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 = '{"assets/foo.jpg":[{"asset":"assets/2x/foo.jpg","dpr":2.0}],'
'"assets/somewhereElse/bar.jpg":[{"asset":"assets/somewhereElse/2x/bar.jpg","dpr":2.0}]}';
final String manifest = await extractAssetManifestFromBundleAsJson(bundle);
expect(manifest, equals(expectedManifest));
});
sdk: flutter
flutter:
assets:
- assets/
'''
);
});
testWithoutContext('Variant detection works with windows-style filepaths', () async {
const List<String> assets = <String>[
r'assets\foo.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);
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']));
});
});
}
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