flutter/packages/flutter_tools/test/general.shard/asset_bundle_variant_test.dart
Andrew Kolos 759ebef689
Do not try to load main/default asset image if only higher-res variants exist (#128143)
Fixes https://github.com/flutter/flutter/issues/127090.

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

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

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

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

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

This fix also includes a non-user-visible behavior change:
* `"dpr"` is no longer a required field in the asset manifest's underlying structure. For the main asset entry, we do not include `"dpr"`. It makes less sense for the tool to decide what the default target dpr for an image should be. This should be left to the framework.
2023-06-07 03:19:15 +00:00

365 lines
12 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:typed_data';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
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() {
Future<Map<String, List<String>>> extractAssetManifestJsonFromBundle(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;
}
Future<Map<Object?, Object?>> extractAssetManifestSmcBinFromBundle(ManifestAssetBundle bundle) async {
final List<int> manifest = await bundle.entries['AssetManifest.smcbin']!.contentsAsBytes();
final ByteData asByteData = ByteData.view(Uint8List.fromList(manifest).buffer);
final Map<Object?, Object?> decoded = const StandardMessageCodec().decodeMessage(asByteData)! as Map<Object?, Object?>;
return decoded;
}
group('AssetBundle asset variants (with Unix-style paths)', () {
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();
});
void createPubspec({
required List<String> assets,
}) {
fs.file('pubspec.yaml').writeAsStringSync(
'''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
assets:
${assets.map((String entry) => ' - $entry').join('\n')}
'''
);
}
testWithoutContext('Only images in folders named with device pixel ratios (e.g. 2x, 3.0x) should be considered as variants of other images', () async {
createPubspec(assets: <String>['assets/', 'assets/notAVariant/']);
const String image = 'assets/image.jpg';
const String 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>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
final Map<String, List<Map<String, Object>>> expectedAssetManifest = <String, List<Map<String, Object>>>{
image: <Map<String, Object>>[
<String, String>{
'asset': image,
},
<String, Object>{
'asset': image2xVariant,
'dpr': 2.0,
}
],
imageNonVariant: <Map<String, String>>[
<String, String>{
'asset': imageNonVariant,
}
],
};
expect(smcBinManifest, equals(expectedAssetManifest));
expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest)));
});
testWithoutContext('Asset directories have their subdirectories searched for asset variants', () async {
createPubspec(assets: <String>['assets/', 'assets/folder/']);
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>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
expect(jsonManifest, hasLength(2));
expect(jsonManifest[topLevelImage], equals(<String>[topLevelImage]));
expect(jsonManifest[secondLevelImage], equals(<String>[secondLevelImage, secondLevel2xVariant]));
final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
final Map<String, List<Map<String, Object>>> expectedAssetManifest = <String, List<Map<String, Object>>>{
topLevelImage: <Map<String, Object>>[
<String, String>{
'asset': topLevelImage,
},
],
secondLevelImage: <Map<String, Object>>[
<String, String>{
'asset': secondLevelImage,
},
<String, Object>{
'asset': secondLevel2xVariant,
'dpr': 2.0,
},
],
};
expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest)));
expect(smcBinManifest, equals(expectedAssetManifest));
});
testWithoutContext('Asset paths should never be URI-encoded', () async {
createPubspec(assets: <String>['assets/normalFolder/']);
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>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
final Map<String, List<Map<String, Object>>> expectedAssetManifest = <String, List<Map<String, Object>>>{
image: <Map<String, Object>>[
<String, Object>{
'asset': image,
},
<String, Object>{
'asset': imageVariant,
'dpr': 3.0
},
],
};
expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest)));
expect(smcBinManifest, equals(expectedAssetManifest));
});
testWithoutContext('Main assets are not included if the file does not exist', () async {
createPubspec(assets: <String>['assets/image.png']);
// We intentionally do not add a 'assets/image.png'.
const String imageVariant = 'assets/2x/image.png';
final List<String> assets = <String>[
imageVariant,
];
for (final String asset in assets) {
final File assetFile = fs.file(asset);
assetFile.createSync(recursive: true);
assetFile.writeAsStringSync(asset);
}
final ManifestAssetBundle bundle = ManifestAssetBundle(
logger: BufferLogger.test(),
fileSystem: fs,
platform: platform,
);
await bundle.build(
packagesPath: '.packages',
flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory),
);
final Map<String, List<Map<String, Object>>> expectedManifest = <String, List<Map<String, Object>>>{
'assets/image.png': <Map<String, Object>>[
<String, Object>{
'asset': imageVariant,
'dpr': 2.0
},
],
};
final Map<String, List<String>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
expect(jsonManifest, equals(_assetManifestBinToJson(expectedManifest)));
expect(smcBinManifest, equals(expectedManifest));
});
});
group('AssetBundle asset variants (with Windows-style filepaths)', () {
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/
- assets/somewhereElse/
'''
);
});
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<Map<String, Object>>> expectedAssetManifest = <String, List<Map<String, Object>>>{
'assets/foo.jpg': <Map<String, Object>>[
<String, Object>{
'asset': 'assets/foo.jpg',
},
<String, Object>{
'asset': 'assets/2x/foo.jpg',
'dpr': 2.0,
},
],
'assets/somewhereElse/bar.jpg': <Map<String, Object>>[
<String, Object>{
'asset': 'assets/somewhereElse/bar.jpg',
},
<String, Object>{
'asset': 'assets/somewhereElse/2x/bar.jpg',
'dpr': 2.0,
},
],
};
final Map<String, List<String>> jsonManifest = await extractAssetManifestJsonFromBundle(bundle);
final Map<Object?, Object?> smcBinManifest = await extractAssetManifestSmcBinFromBundle(bundle);
expect(jsonManifest, equals(_assetManifestBinToJson(expectedAssetManifest)));
expect(smcBinManifest, equals(expectedAssetManifest));
});
});
}
Map<Object, Object> _assetManifestBinToJson(Map<Object, Object> manifest) {
List<Object> convertList(List<Object> variants) => variants
.map((Object variant) => (variant as Map<Object?, Object?>)['asset']!)
.toList();
return manifest.map((Object key, Object value) => MapEntry<Object, Object>(key, convertList(value as List<Object>)));
}