| // 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.bin']!.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; |
| late String flutterRoot; |
| |
| setUp(() { |
| platform = FakePlatform(); |
| fs = MemoryFileSystem.test(); |
| 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, |
| flutterRoot: flutterRoot, |
| ); |
| |
| 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, |
| flutterRoot: flutterRoot, |
| 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, |
| flutterRoot: flutterRoot, |
| ); |
| |
| 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, |
| flutterRoot: flutterRoot, |
| ); |
| |
| 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; |
| late final String flutterRoot; |
| |
| setUp(() { |
| platform = FakePlatform(operatingSystem: 'windows'); |
| fs = MemoryFileSystem.test(style: FileSystemStyle.windows); |
| 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, |
| flutterRoot: flutterRoot, |
| ); |
| |
| 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>))); |
| } |