Unverified Commit 9df362a7 authored by Gary Qian's avatar Gary Qian Committed by GitHub

[flutter_tools] Refactor DeferredComponents validator and add build target (#76400)

parent d60e9de7
// 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.
// @dart = 2.8
import 'package:xml/xml.dart';
import 'package:yaml/yaml.dart';
import '../base/deferred_component.dart';
import '../base/error_handling_io.dart';
import '../base/file_system.dart';
import '../build_system/build_system.dart';
import 'deferred_components_validator.dart';
/// A class to configure and run deferred component setup verification checks
/// and tasks.
///
/// Once constructed, checks and tasks can be executed by calling the respective
/// methods. The results of the checks are stored internally and can be
/// displayed to the user by calling [displayResults].
class DeferredComponentsGenSnapshotValidator extends DeferredComponentsValidator {
/// Constructs a validator instance.
///
/// The [env] property is used to locate the project files that are checked.
///
/// The [templatesDir] parameter is optional. If null, the tool's default
/// templates directory will be used.
///
/// When [exitOnFail] is set to true, the [handleResults] and [attemptToolExit]
/// methods will exit the tool when this validator detects a recommended
/// change. This defaults to true.
DeferredComponentsGenSnapshotValidator(Environment env, {
bool exitOnFail = true,
String title,
}) : super(env, exitOnFail: exitOnFail, title: title);
// The key used to identify the metadata element as the loading unit id to
// deferred component mapping.
static const String _mappingKey = 'io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping';
/// Checks if the base module `app`'s `AndroidManifest.xml` contains the
/// required meta-data that maps loading units to deferred components.
///
/// Returns true if the check passed with no recommended changes, and false
/// otherwise.
///
/// Flutter engine uses a manifest meta-data mapping to determine which
/// deferred component includes a particular loading unit id. This method
/// checks if `app`'s `AndroidManifest.xml` contains this metadata. If not, it
/// will generate a modified AndroidManifest.xml with the correct metadata
/// entry.
///
/// An example mapping:
///
/// 2:componentA,3:componentB,4:componentC
///
/// Where loading unit 2 is included in componentA, loading unit 3 is included
/// in componentB, and loading unit 4 is included in componentC.
bool checkAppAndroidManifestComponentLoadingUnitMapping(List<DeferredComponent> components, List<LoadingUnit> generatedLoadingUnits) {
final Directory androidDir = env.projectDir.childDirectory('android');
inputs.add(env.projectDir.childFile('pubspec.yaml'));
// We do not use the Xml package to handle the writing, as we do not want to
// erase any user applied formatting and comments. The changes can be
// applied with dart io and custom parsing.
final File appManifestFile = androidDir
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childFile('AndroidManifest.xml');
inputs.add(appManifestFile);
if (!appManifestFile.existsSync()) {
invalidFiles[appManifestFile.path] = 'Error: $appManifestFile does not '
'exist or could not be found. Please ensure an AndroidManifest.xml '
'exists for the app\'s base module.';
return false;
}
XmlDocument document;
try {
document = XmlDocument.parse(appManifestFile.readAsStringSync());
} on XmlParserException {
invalidFiles[appManifestFile.path] = 'Error parsing $appManifestFile '
'Please ensure that the android manifest is a valid XML document and '
'try again.';
return false;
} on FileSystemException {
invalidFiles[appManifestFile.path] = 'Error reading $appManifestFile '
'even though it exists. Please ensure that you have read permission for '
'this file and try again.';
return false;
}
// Create loading unit mapping.
final Map<int, String> mapping = <int, String>{};
for (final DeferredComponent component in components) {
component.assignLoadingUnits(generatedLoadingUnits);
for (final LoadingUnit unit in component.loadingUnits) {
if (!mapping.containsKey(unit.id)) {
mapping[unit.id] = component.name;
}
}
}
// Encode the mapping as a string.
final StringBuffer mappingBuffer = StringBuffer();
for (final int key in mapping.keys) {
mappingBuffer.write('$key:${mapping[key]},');
}
String encodedMapping = mappingBuffer.toString();
// remove trailing comma.
encodedMapping = encodedMapping.substring(0, encodedMapping.length - 1);
// Check for existing metadata entry and see if needs changes.
bool exists = false;
bool modified = false;
for (final XmlElement metaData in document.findAllElements('meta-data')) {
final String name = metaData.getAttribute('android:name');
if (name == _mappingKey) {
exists = true;
final String storedMappingString = metaData.getAttribute('android:value');
if (storedMappingString != encodedMapping) {
metaData.setAttribute('android:value', encodedMapping);
modified = true;
}
}
}
if (!exists) {
// Create an meta-data XmlElement that contains the mapping.
final XmlElement mappingMetadataElement = XmlElement(XmlName.fromString('meta-data'),
<XmlAttribute>[
XmlAttribute(XmlName.fromString('android:name'), _mappingKey),
XmlAttribute(XmlName.fromString('android:value'), encodedMapping),
],
);
for (final XmlElement application in document.findAllElements('application')) {
application.children.add(mappingMetadataElement);
break;
}
}
if (!exists || modified) {
final File manifestOutput = outputDir
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childFile('AndroidManifest.xml');
ErrorHandlingFileSystem.deleteIfExists(manifestOutput);
manifestOutput.createSync(recursive: true);
manifestOutput.writeAsStringSync(document.toXmlString(pretty: true), flush: true);
modifiedFiles.add(manifestOutput.path);
return false;
}
return true;
}
/// Compares the provided loading units against the contents of the
/// `deferred_components_loading_units.yaml` file.
///
/// Returns true if a loading unit cache file exists and all loading units
/// match, and false otherwise.
///
/// This method will parse the cached loading units file if it exists and
/// compare it to the provided generatedLoadingUnits. It will distinguish
/// between newly added loading units and no longer existing loading units. If
/// the cache file does not exist, then all generatedLoadingUnits will be
/// considered new.
bool checkAgainstLoadingUnitsCache(
List<LoadingUnit> generatedLoadingUnits) {
final List<LoadingUnit> cachedLoadingUnits = _parseLodingUnitsCache(env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName));
loadingUnitComparisonResults = <String, dynamic>{};
final Set<LoadingUnit> unmatchedLoadingUnits = <LoadingUnit>{};
final List<LoadingUnit> newLoadingUnits = <LoadingUnit>[];
if (generatedLoadingUnits == null || cachedLoadingUnits == null) {
loadingUnitComparisonResults['new'] = newLoadingUnits;
loadingUnitComparisonResults['missing'] = unmatchedLoadingUnits;
loadingUnitComparisonResults['match'] = false;
return false;
}
unmatchedLoadingUnits.addAll(cachedLoadingUnits);
final Set<int> addedNewIds = <int>{};
for (final LoadingUnit genUnit in generatedLoadingUnits) {
bool matched = false;
for (final LoadingUnit cacheUnit in cachedLoadingUnits) {
if (genUnit.equalsIgnoringPath(cacheUnit)) {
matched = true;
unmatchedLoadingUnits.remove(cacheUnit);
break;
}
}
if (!matched && !addedNewIds.contains(genUnit.id)) {
newLoadingUnits.add(genUnit);
addedNewIds.add(genUnit.id);
}
}
loadingUnitComparisonResults['new'] = newLoadingUnits;
loadingUnitComparisonResults['missing'] = unmatchedLoadingUnits;
loadingUnitComparisonResults['match'] = newLoadingUnits.isEmpty && unmatchedLoadingUnits.isEmpty;
return loadingUnitComparisonResults['match'] as bool;
}
List<LoadingUnit> _parseLodingUnitsCache(File cacheFile) {
final List<LoadingUnit> loadingUnits = <LoadingUnit>[];
inputs.add(cacheFile);
if (!cacheFile.existsSync()) {
return loadingUnits;
}
final YamlMap data = loadYaml(cacheFile.readAsStringSync()) as YamlMap;
// validate yaml format.
if (!data.containsKey('loading-units')) {
invalidFiles[cacheFile.path] = 'Invalid loading units yaml file, \'loading-units\' '
'entry did not exist.';
return loadingUnits;
} else {
if (data['loading-units'] is! YamlList && data['loading-units'] != null) {
invalidFiles[cacheFile.path] = 'Invalid loading units yaml file, \'loading-units\' '
'is not a list.';
return loadingUnits;
}
if (data['loading-units'] != null) {
for (final dynamic loadingUnitData in data['loading-units']) {
if (loadingUnitData is! YamlMap) {
invalidFiles[cacheFile.path] = 'Invalid loading units yaml file, \'loading-units\' '
'is not a list of maps.';
return loadingUnits;
}
final YamlMap loadingUnitDataMap = loadingUnitData as YamlMap;
if (loadingUnitDataMap['id'] == null) {
invalidFiles[cacheFile.path] = 'Invalid loading units yaml file, all '
'loading units must have an \'id\'';
return loadingUnits;
}
if (loadingUnitDataMap['libraries'] != null) {
if (loadingUnitDataMap['libraries'] is! YamlList) {
invalidFiles[cacheFile.path] = 'Invalid loading units yaml file, \'libraries\' '
'is not a list.';
return loadingUnits;
}
for (final dynamic node in loadingUnitDataMap['libraries'] as YamlList) {
if (node is! String) {
invalidFiles[cacheFile.path] = 'Invalid loading units yaml file, \'libraries\' '
'is not a list of strings.';
return loadingUnits;
}
}
}
}
}
}
// Parse out validated yaml.
if (data.containsKey('loading-units')) {
if (data['loading-units'] != null) {
for (final dynamic loadingUnitData in data['loading-units']) {
final YamlMap loadingUnitDataMap = loadingUnitData as YamlMap;
final List<String> libraries = <String>[];
if (loadingUnitDataMap['libraries'] != null) {
for (final dynamic node in loadingUnitDataMap['libraries'] as YamlList) {
libraries.add(node as String);
}
}
loadingUnits.add(
LoadingUnit(
id: loadingUnitDataMap['id'] as int,
path: null,
libraries: libraries,
));
}
}
}
return loadingUnits;
}
/// Writes the provided generatedLoadingUnits as `deferred_components_loading_units.yaml`
///
/// This cache file is used to detect any changes in the loading units
/// produced by gen_snapshot. Running [checkAgainstLoadingUnitCache] with a
/// mismatching or missing cache will result in a failed validation. This
/// prevents unexpected changes in loading units causing misconfigured
/// deferred components.
void writeLoadingUnitsCache(List<LoadingUnit> generatedLoadingUnits) {
generatedLoadingUnits ??= <LoadingUnit>[];
final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
outputs.add(cacheFile);
ErrorHandlingFileSystem.deleteIfExists(cacheFile);
cacheFile.createSync(recursive: true);
final StringBuffer buffer = StringBuffer();
buffer.write('''
# ==============================================================================
# The contents of this file are automatically generated and it is not
# recommended to modify this file manually.
# ==============================================================================
#
# In order to prevent unexpected splitting of deferred apps, this file records
# the last generated set of loading units. It only possible to obtain the final
# configuration of loading units after compilation is complete. This means
# improperly setup deferred imports can only be detected after compilation.
#
# This file allows the build tool to detect any changes in the generated
# loading units. During the next build attempt, loading units in this file are
# compared against the newly generated loading units to check for any new or
# removed loading units. In the case where loading units do not match, the build
# will fail and ask the developer to verify that the `deferred-components`
# configuration in `pubspec.yaml` is correct. Developers should make any
# necessary changes to integrate new and changed loading units or remove no
# longer existing loading units from the configuration. The build command should
# then be re-run to continue the build process.
#
# Sometimes, changes to the generated loading units may be unintentional. If
# the list of loading units in this file is not what is expected, the app's
# deferred imports should be reviewed. Third party plugins and packages may
# also introduce deferred imports that result in unexpected loading units.
loading-units:
''');
final Set<int> usedIds = <int>{};
for (final LoadingUnit unit in generatedLoadingUnits) {
if (usedIds.contains(unit.id)) {
continue;
}
buffer.write(' - id: ${unit.id}\n');
if (unit.libraries != null && unit.libraries.isNotEmpty) {
buffer.write(' libraries:\n');
for (final String lib in unit.libraries) {
buffer.write(' - $lib\n');
}
}
usedIds.add(unit.id);
}
cacheFile.writeAsStringSync(buffer.toString(), flush: true);
}
}
...@@ -6,17 +6,15 @@ ...@@ -6,17 +6,15 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
import 'package:yaml/yaml.dart';
import '../base/common.dart';
import '../base/deferred_component.dart'; import '../base/deferred_component.dart';
import '../base/error_handling_io.dart'; import '../base/error_handling_io.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/terminal.dart';
import '../build_system/build_system.dart'; import '../build_system/build_system.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../project.dart'; import '../project.dart';
import '../template.dart'; import '../template.dart';
import 'deferred_components_validator.dart';
/// A class to configure and run deferred component setup verification checks /// A class to configure and run deferred component setup verification checks
/// and tasks. /// and tasks.
...@@ -24,7 +22,7 @@ import '../template.dart'; ...@@ -24,7 +22,7 @@ import '../template.dart';
/// Once constructed, checks and tasks can be executed by calling the respective /// Once constructed, checks and tasks can be executed by calling the respective
/// methods. The results of the checks are stored internally and can be /// methods. The results of the checks are stored internally and can be
/// displayed to the user by calling [displayResults]. /// displayed to the user by calling [displayResults].
class DeferredComponentsSetupValidator { class DeferredComponentsPrebuildValidator extends DeferredComponentsValidator {
/// Constructs a validator instance. /// Constructs a validator instance.
/// ///
/// The [env] property is used to locate the project files that are checked. /// The [env] property is used to locate the project files that are checked.
...@@ -35,78 +33,14 @@ class DeferredComponentsSetupValidator { ...@@ -35,78 +33,14 @@ class DeferredComponentsSetupValidator {
/// When [exitOnFail] is set to true, the [handleResults] and [attemptToolExit] /// When [exitOnFail] is set to true, the [handleResults] and [attemptToolExit]
/// methods will exit the tool when this validator detects a recommended /// methods will exit the tool when this validator detects a recommended
/// change. This defaults to true. /// change. This defaults to true.
DeferredComponentsSetupValidator(this.env, { DeferredComponentsPrebuildValidator(Environment env, {
this.exitOnFail = true, bool exitOnFail = true,
String title, String title,
Directory templatesDir, Directory templatesDir,
}) : _outputDir = env.projectDir }) : _templatesDir = templatesDir,
.childDirectory('build') super(env, exitOnFail: exitOnFail, title: title);
.childDirectory(kDeferredComponentsTempDirectory),
_inputs = <File>[],
_outputs = <File>[],
_title = title ?? 'Deferred components setup verification',
_templatesDir = templatesDir,
_generatedFiles = <String>[],
_modifiedFiles = <String>[],
_invalidFiles = <String, String>{},
_diffLines = <String>[];
/// The build environment that should be used to find the input files to run
/// checks against.
///
/// The checks in this class are meant to be used as part of a build process,
/// so an environment should be available.
final Environment env;
/// When true, failed checks and tasks will result in [attemptToolExit]
/// triggering [throwToolExit].
final bool exitOnFail;
/// The name of the golden file that tracks the latest loading units
/// generated.
@visibleForTesting
static const String kDeferredComponentsGoldenFileName = 'deferred_components_golden.yaml';
/// The directory in the build folder to generate missing/modified files into.
@visibleForTesting
static const String kDeferredComponentsTempDirectory = 'android_deferred_components_setup_files';
final String _title;
final Directory _templatesDir; final Directory _templatesDir;
final Directory _outputDir;
// Files that were newly generated by this validator.
final List<String> _generatedFiles;
// Existing files that were modified by this validator.
final List<String> _modifiedFiles;
// Files that were invalid and unable to be checked. These files are input
// files that the validator tries to read rather than output files the
// validator generates. The key is the file name and the value is the message
// or reason it was invalid.
final Map<String, String> _invalidFiles;
// Output of the diff task.
// TODO(garyq): implement the diff task.
final List<String> _diffLines;
// Tracks the new and missing loading units.
Map<String, dynamic> _goldenComparisonResults;
/// All files read by the validator.
List<File> get inputs => _inputs;
final List<File> _inputs;
/// All files output by the validator.
List<File> get outputs => _outputs;
final List<File> _outputs;
/// Returns true if there were any recommended changes that should
/// be applied.
///
/// Retuns false if no problems or recommendations were detected.
///
/// If no checks are run, then this will default to false and will remain so
/// until a failing check finishes running.
bool get changesNeeded => _generatedFiles.isNotEmpty
|| _modifiedFiles.isNotEmpty
|| _invalidFiles.isNotEmpty
|| (_goldenComparisonResults != null && !(_goldenComparisonResults['match'] as bool));
/// Checks if an android dynamic feature module exists for each deferred /// Checks if an android dynamic feature module exists for each deferred
/// component. /// component.
...@@ -122,6 +56,10 @@ class DeferredComponentsSetupValidator { ...@@ -122,6 +56,10 @@ class DeferredComponentsSetupValidator {
/// This method does not check if the contents of either of the files are /// This method does not check if the contents of either of the files are
/// valid, as there are many ways that they can be validly configured. /// valid, as there are many ways that they can be validly configured.
Future<bool> checkAndroidDynamicFeature(List<DeferredComponent> components) async { Future<bool> checkAndroidDynamicFeature(List<DeferredComponent> components) async {
inputs.add(env.projectDir.childFile('pubspec.yaml'));
if (components == null || components.isEmpty) {
return false;
}
bool changesMade = false; bool changesMade = false;
for (final DeferredComponent component in components) { for (final DeferredComponent component in components) {
final _DeferredComponentAndroidFiles androidFiles = _DeferredComponentAndroidFiles( final _DeferredComponentAndroidFiles androidFiles = _DeferredComponentAndroidFiles(
...@@ -133,135 +71,20 @@ class DeferredComponentsSetupValidator { ...@@ -133,135 +71,20 @@ class DeferredComponentsSetupValidator {
// generate into temp directory // generate into temp directory
final Map<String, List<File>> results = final Map<String, List<File>> results =
await androidFiles.generateFiles( await androidFiles.generateFiles(
alternateAndroidDir: _outputDir, alternateAndroidDir: outputDir,
clearAlternateOutputDir: true, clearAlternateOutputDir: true,
); );
for (final File file in results['outputs']) { for (final File file in results['outputs']) {
_generatedFiles.add(file.path); generatedFiles.add(file.path);
changesMade = true; changesMade = true;
} }
_outputs.addAll(results['outputs']); outputs.addAll(results['outputs']);
_inputs.addAll(results['inputs']); inputs.addAll(results['inputs']);
} }
} }
return !changesMade; return !changesMade;
} }
// The key used to identify the metadata element as the loading unit id to
// deferred component mapping.
static const String _mappingKey = 'io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping';
/// Checks if the base module `app`'s `AndroidManifest.xml` contains the
/// required meta-data that maps loading units to deferred components.
///
/// Returns true if the check passed with no recommended changes, and false
/// otherwise.
///
/// Flutter engine uses a manifest meta-data mapping to determine which
/// deferred component includes a particular loading unit id. This method
/// checks if `app`'s `AndroidManifest.xml` contains this metadata. If not, it
/// will generate a modified AndroidManifest.xml with the correct metadata
/// entry.
///
/// An example mapping:
///
/// 2:componentA,3:componentB,4:componentC
///
/// Where loading unit 2 is included in componentA, loading unit 3 is included
/// in componentB, and loading unit 4 is included in componentC.
bool checkAppAndroidManifestComponentLoadingUnitMapping(List<DeferredComponent> components, List<LoadingUnit> generatedLoadingUnits) {
final Directory androidDir = env.projectDir.childDirectory('android');
// We do not use the Xml package to handle the writing, as we do not want to
// erase any user applied formatting and comments. The changes can be
// applied with dart io and custom parsing.
final File appManifestFile = androidDir
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childFile('AndroidManifest.xml');
_inputs.add(appManifestFile);
if (!appManifestFile.existsSync()) {
_invalidFiles[appManifestFile.path] = 'Error: $appManifestFile does not '
'exist or could not be found. Please ensure an AndroidManifest.xml '
'exists for the app\'s base module.';
return false;
}
XmlDocument document;
try {
document = XmlDocument.parse(appManifestFile.readAsStringSync());
} on XmlParserException {
_invalidFiles[appManifestFile.path] = 'Error parsing $appManifestFile '
'Please ensure that the android manifest is a valid XML document and '
'try again.';
return false;
} on FileSystemException {
_invalidFiles[appManifestFile.path] = 'Error reading $appManifestFile '
'even though it exists. Please ensure that you have read permission for '
'this file and try again.';
return false;
}
// Create loading unit mapping.
final Map<int, String> mapping = <int, String>{};
for (final DeferredComponent component in components) {
component.assignLoadingUnits(generatedLoadingUnits);
for (final LoadingUnit unit in component.loadingUnits) {
if (!mapping.containsKey(unit.id)) {
mapping[unit.id] = component.name;
}
}
}
// Encode the mapping as a string.
final StringBuffer mappingBuffer = StringBuffer();
for (final int key in mapping.keys) {
mappingBuffer.write('$key:${mapping[key]},');
}
String encodedMapping = mappingBuffer.toString();
// remove trailing comma.
encodedMapping = encodedMapping.substring(0, encodedMapping.length - 1);
// Check for existing metadata entry and see if needs changes.
bool exists = false;
bool modified = false;
for (final XmlElement metaData in document.findAllElements('meta-data')) {
final String name = metaData.getAttribute('android:name');
if (name == _mappingKey) {
exists = true;
final String storedMappingString = metaData.getAttribute('android:value');
if (storedMappingString != encodedMapping) {
metaData.setAttribute('android:value', encodedMapping);
modified = true;
}
}
}
if (!exists) {
// Create an meta-data XmlElement that contains the mapping.
final XmlElement mappingMetadataElement = XmlElement(XmlName.fromString('meta-data'),
<XmlAttribute>[
XmlAttribute(XmlName.fromString('android:name'), _mappingKey),
XmlAttribute(XmlName.fromString('android:value'), encodedMapping),
],
);
for (final XmlElement application in document.findAllElements('application')) {
application.children.add(mappingMetadataElement);
break;
}
}
if (!exists || modified) {
final File manifestOutput = _outputDir
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childFile('AndroidManifest.xml');
ErrorHandlingFileSystem.deleteIfExists(manifestOutput);
manifestOutput.createSync(recursive: true);
manifestOutput.writeAsStringSync(document.toXmlString(pretty: true), flush: true);
_modifiedFiles.add(manifestOutput.path);
return false;
}
return true;
}
/// Checks if the base module `app`'s `strings.xml` contain string /// Checks if the base module `app`'s `strings.xml` contain string
/// resources for each component's name. /// resources for each component's name.
/// ///
...@@ -284,6 +107,7 @@ class DeferredComponentsSetupValidator { ...@@ -284,6 +107,7 @@ class DeferredComponentsSetupValidator {
/// `Name` as a suffix, and the text contents should be the component name. /// `Name` as a suffix, and the text contents should be the component name.
bool checkAndroidResourcesStrings(List<DeferredComponent> components) { bool checkAndroidResourcesStrings(List<DeferredComponent> components) {
final Directory androidDir = env.projectDir.childDirectory('android'); final Directory androidDir = env.projectDir.childDirectory('android');
inputs.add(env.projectDir.childFile('pubspec.yaml'));
// Add component name mapping to strings.xml // Add component name mapping to strings.xml
final File stringRes = androidDir final File stringRes = androidDir
...@@ -293,8 +117,8 @@ class DeferredComponentsSetupValidator { ...@@ -293,8 +117,8 @@ class DeferredComponentsSetupValidator {
.childDirectory('res') .childDirectory('res')
.childDirectory('values') .childDirectory('values')
.childFile('strings.xml'); .childFile('strings.xml');
_inputs.add(stringRes); inputs.add(stringRes);
final File stringResOutput = _outputDir final File stringResOutput = outputDir
.childDirectory('app') .childDirectory('app')
.childDirectory('src') .childDirectory('src')
.childDirectory('main') .childDirectory('main')
...@@ -302,6 +126,9 @@ class DeferredComponentsSetupValidator { ...@@ -302,6 +126,9 @@ class DeferredComponentsSetupValidator {
.childDirectory('values') .childDirectory('values')
.childFile('strings.xml'); .childFile('strings.xml');
ErrorHandlingFileSystem.deleteIfExists(stringResOutput); ErrorHandlingFileSystem.deleteIfExists(stringResOutput);
if (components == null || components.isEmpty) {
return true;
}
final Map<String, String> requiredEntriesMap = <String, String>{}; final Map<String, String> requiredEntriesMap = <String, String>{};
for (final DeferredComponent component in components) { for (final DeferredComponent component in components) {
requiredEntriesMap['${component.name}Name'] = component.name; requiredEntriesMap['${component.name}Name'] = component.name;
...@@ -312,7 +139,7 @@ class DeferredComponentsSetupValidator { ...@@ -312,7 +139,7 @@ class DeferredComponentsSetupValidator {
try { try {
document = XmlDocument.parse(stringRes.readAsStringSync()); document = XmlDocument.parse(stringRes.readAsStringSync());
} on XmlParserException { } on XmlParserException {
_invalidFiles[stringRes.path] = 'Error parsing $stringRes ' invalidFiles[stringRes.path] = 'Error parsing $stringRes '
'Please ensure that the strings.xml is a valid XML document and ' 'Please ensure that the strings.xml is a valid XML document and '
'try again.'; 'try again.';
return false; return false;
...@@ -348,7 +175,7 @@ class DeferredComponentsSetupValidator { ...@@ -348,7 +175,7 @@ class DeferredComponentsSetupValidator {
if (modified) { if (modified) {
stringResOutput.createSync(recursive: true); stringResOutput.createSync(recursive: true);
stringResOutput.writeAsStringSync(document.toXmlString(pretty: true)); stringResOutput.writeAsStringSync(document.toXmlString(pretty: true));
_modifiedFiles.add(stringResOutput.path); modifiedFiles.add(stringResOutput.path);
return false; return false;
} }
return true; return true;
...@@ -369,321 +196,15 @@ class DeferredComponentsSetupValidator { ...@@ -369,321 +196,15 @@ class DeferredComponentsSetupValidator {
'''); ''');
stringResOutput.writeAsStringSync(buffer.toString(), flush: true, mode: FileMode.append); stringResOutput.writeAsStringSync(buffer.toString(), flush: true, mode: FileMode.append);
_generatedFiles.add(stringResOutput.path); generatedFiles.add(stringResOutput.path);
return false; return false;
} }
/// Compares the provided loading units against the contents of the
/// `deferred_components_golden.yaml` file.
///
/// Returns true if a golden exists and all loading units match, and false
/// otherwise.
///
/// This method will parse the golden file if it exists and compare it to
/// the provided generatedLoadingUnits. It will distinguish between newly
/// added loading units and no longer existing loading units. If the golden
/// file does not exist, then all generatedLoadingUnits will be considered
/// new.
bool checkAgainstLoadingUnitGolden(
List<LoadingUnit> generatedLoadingUnits) {
final List<LoadingUnit> goldenLoadingUnits = _parseGolden(env.projectDir.childFile(kDeferredComponentsGoldenFileName));
_goldenComparisonResults = <String, dynamic>{};
final Set<LoadingUnit> unmatchedLoadingUnits = <LoadingUnit>{};
final List<LoadingUnit> newLoadingUnits = <LoadingUnit>[];
if (generatedLoadingUnits == null || goldenLoadingUnits == null) {
_goldenComparisonResults['new'] = newLoadingUnits;
_goldenComparisonResults['missing'] = unmatchedLoadingUnits;
_goldenComparisonResults['match'] = false;
return false;
}
_inputs.add(env.projectDir.childFile(kDeferredComponentsGoldenFileName));
unmatchedLoadingUnits.addAll(goldenLoadingUnits);
final Set<int> addedNewIds = <int>{};
for (final LoadingUnit genUnit in generatedLoadingUnits) {
bool matched = false;
for (final LoadingUnit goldUnit in goldenLoadingUnits) {
if (genUnit.equalsIgnoringPath(goldUnit)) {
matched = true;
unmatchedLoadingUnits.remove(goldUnit);
break;
}
}
if (!matched && !addedNewIds.contains(genUnit.id)) {
newLoadingUnits.add(genUnit);
addedNewIds.add(genUnit.id);
}
}
_goldenComparisonResults['new'] = newLoadingUnits;
_goldenComparisonResults['missing'] = unmatchedLoadingUnits;
_goldenComparisonResults['match'] = newLoadingUnits.isEmpty && unmatchedLoadingUnits.isEmpty;
return _goldenComparisonResults['match'] as bool;
}
List<LoadingUnit> _parseGolden(File goldenFile) {
final List<LoadingUnit> loadingUnits = <LoadingUnit>[];
_inputs.add(goldenFile);
if (!goldenFile.existsSync()) {
return loadingUnits;
}
final YamlMap data = loadYaml(goldenFile.readAsStringSync()) as YamlMap;
// validate yaml format.
if (!data.containsKey('loading-units')) {
_invalidFiles[goldenFile.path] = 'Invalid golden yaml file, \'loading-units\' '
'entry did not exist.';
return loadingUnits;
} else {
if (data['loading-units'] is! YamlList && data['loading-units'] != null) {
_invalidFiles[goldenFile.path] = 'Invalid golden yaml file, \'loading-units\' '
'is not a list.';
return loadingUnits;
}
if (data['loading-units'] != null) {
for (final dynamic loadingUnitData in data['loading-units']) {
if (loadingUnitData is! YamlMap) {
_invalidFiles[goldenFile.path] = 'Invalid golden yaml file, \'loading-units\' '
'is not a list of maps.';
return loadingUnits;
}
final YamlMap loadingUnitDataMap = loadingUnitData as YamlMap;
if (loadingUnitDataMap['id'] == null) {
_invalidFiles[goldenFile.path] = 'Invalid golden yaml file, all '
'loading units must have an \'id\'';
return loadingUnits;
}
if (loadingUnitDataMap['libraries'] != null) {
if (loadingUnitDataMap['libraries'] is! YamlList) {
_invalidFiles[goldenFile.path] = 'Invalid golden yaml file, \'libraries\' '
'is not a list.';
return loadingUnits;
}
for (final dynamic node in loadingUnitDataMap['libraries'] as YamlList) {
if (node is! String) {
_invalidFiles[goldenFile.path] = 'Invalid golden yaml file, \'libraries\' '
'is not a list of strings.';
return loadingUnits;
}
}
}
}
}
}
// Parse out validated yaml.
if (data.containsKey('loading-units')) {
if (data['loading-units'] != null) {
for (final dynamic loadingUnitData in data['loading-units']) {
final YamlMap loadingUnitDataMap = loadingUnitData as YamlMap;
final List<String> libraries = <String>[];
if (loadingUnitDataMap['libraries'] != null) {
for (final dynamic node in loadingUnitDataMap['libraries'] as YamlList) {
libraries.add(node as String);
}
}
loadingUnits.add(
LoadingUnit(
id: loadingUnitDataMap['id'] as int,
path: null,
libraries: libraries,
));
}
}
}
return loadingUnits;
}
/// Writes the provided generatedLoadingUnits as `deferred_components_golden.yaml`
///
/// This golden file is used to detect any changes in the loading units
/// produced by gen_snapshot. Running [checkAgainstLoadingUnitGolden] with a
/// mismatching or missing golden will result in a failed validation. This
/// prevents unexpected changes in loading units causing misconfigured
/// deferred components.
void writeGolden(List<LoadingUnit> generatedLoadingUnits) {
generatedLoadingUnits ??= <LoadingUnit>[];
final File goldenFile = env.projectDir.childFile(kDeferredComponentsGoldenFileName);
_outputs.add(goldenFile);
ErrorHandlingFileSystem.deleteIfExists(goldenFile);
goldenFile.createSync(recursive: true);
final StringBuffer buffer = StringBuffer();
buffer.write('''
# ===============================================================================
# The contents of this file are automatically generated and it is not recommended
# to modify this file manually.
# ===============================================================================
#
# In order to prevent unexpected splitting of deferred apps, this golden
# file records the last generated set of loading units. It only possible
# to obtain the final configuration of loading units after compilation is
# complete. This means improperly setup imports can only be detected after
# compilation.
#
# This golden file allows the build tool to detect any changes in the generated
# loading units. During the next build attempt, loading units in this file are
# compared against the newly generated loading units to check for any new or
# removed loading units. In the case where loading units do not match, the build
# will fail and ask the developer to verify that the `deferred-components`
# configuration in `pubspec.yaml` is correct. Developers should make any necessary
# changes to integrate new and changed loading units or remove no longer existing
# loading units from the configuration. The build command should then be
# re-run to continue the build process.
#
# Sometimes, changes to the generated loading units may be unintentional. If
# the list of loading units in this golden is not what is expected, the app's
# deferred imports should be reviewed. Third party plugins and packages may
# also introduce deferred imports that result in unexpected loading units.
loading-units:
''');
final Set<int> usedIds = <int>{};
for (final LoadingUnit unit in generatedLoadingUnits) {
if (usedIds.contains(unit.id)) {
continue;
}
buffer.write(' - id: ${unit.id}\n');
if (unit.libraries != null && unit.libraries.isNotEmpty) {
buffer.write(' libraries:\n');
for (final String lib in unit.libraries) {
buffer.write(' - $lib\n');
}
}
usedIds.add(unit.id);
}
goldenFile.writeAsStringSync(buffer.toString(), flush: true);
}
/// Deletes all files inside of the validator's output directory. /// Deletes all files inside of the validator's output directory.
void clearOutputDir() { void clearOutputDir() {
final Directory dir = env.projectDir.childDirectory('build').childDirectory(kDeferredComponentsTempDirectory); final Directory dir = env.projectDir.childDirectory('build').childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory);
ErrorHandlingFileSystem.deleteIfExists(dir, recursive: true); ErrorHandlingFileSystem.deleteIfExists(dir, recursive: true);
} }
/// Handles the results of all executed checks by calling [displayResults] and
/// [attemptToolExit].
///
/// This should be called after all desired checks and tasks are executed.
void handleResults() {
displayResults();
attemptToolExit();
}
static const String _thickDivider = '=================================================================================';
static const String _thinDivider = '---------------------------------------------------------------------------------';
/// Displays the results of this validator's executed checks and tasks in a
/// human readable format.
///
/// All checks that are desired should be run before calling this method.
void displayResults() {
if (changesNeeded) {
env.logger.printStatus(_thickDivider);
env.logger.printStatus(_title, indent: (_thickDivider.length - _title.length) ~/ 2, emphasis: true);
env.logger.printStatus(_thickDivider);
// Log any file reading/existence errors.
if (_invalidFiles.isNotEmpty) {
env.logger.printStatus('Errors checking the following files:\n', emphasis: true);
for (final String key in _invalidFiles.keys) {
env.logger.printStatus(' - $key: ${_invalidFiles[key]}\n');
}
}
// Log diff file contents, with color highlighting
if (_diffLines != null && _diffLines.isNotEmpty) {
env.logger.printStatus('Diff between `android` and expected files:', emphasis: true);
env.logger.printStatus('');
for (final String line in _diffLines) {
// We only care about diffs in files that have
// counterparts.
if (line.startsWith('Only in android')) {
continue;
}
TerminalColor color = TerminalColor.grey;
if (line.startsWith('+')) {
color = TerminalColor.green;
} else if (line.startsWith('-')) {
color = TerminalColor.red;
}
env.logger.printStatus(line, color: color);
}
env.logger.printStatus('');
}
// Log any newly generated and modified files.
if (_generatedFiles.isNotEmpty) {
env.logger.printStatus('Newly generated android files:', emphasis: true);
for (final String filePath in _generatedFiles) {
final String shortenedPath = filePath.substring(env.projectDir.parent.path.length + 1);
env.logger.printStatus(' - $shortenedPath', color: TerminalColor.grey);
}
env.logger.printStatus('');
}
if (_modifiedFiles.isNotEmpty) {
env.logger.printStatus('Modified android files:', emphasis: true);
for (final String filePath in _modifiedFiles) {
final String shortenedPath = filePath.substring(env.projectDir.parent.path.length + 1);
env.logger.printStatus(' - $shortenedPath', color: TerminalColor.grey);
}
env.logger.printStatus('');
}
if (_generatedFiles.isNotEmpty || _modifiedFiles.isNotEmpty) {
env.logger.printStatus('''
The above files have been placed into `build/$kDeferredComponentsTempDirectory`,
a temporary directory. The files should be reviewed and moved into the project's
`android` directory.''');
if (_diffLines != null && _diffLines.isNotEmpty && !globals.platform.isWindows) {
env.logger.printStatus(r'''
The recommended changes can be quickly applied by running:
$ patch -p0 < build/setup_deferred_components.diff
''');
}
env.logger.printStatus('$_thinDivider\n');
}
// Log loading unit golden changes, if any.
if (_goldenComparisonResults != null) {
if ((_goldenComparisonResults['new'] as List<LoadingUnit>).isNotEmpty) {
env.logger.printStatus('New loading units were found:', emphasis: true);
for (final LoadingUnit unit in _goldenComparisonResults['new'] as List<LoadingUnit>) {
env.logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2);
}
env.logger.printStatus('');
}
if ((_goldenComparisonResults['missing'] as Set<LoadingUnit>).isNotEmpty) {
env.logger.printStatus('Previously existing loading units no longer exist:', emphasis: true);
for (final LoadingUnit unit in _goldenComparisonResults['missing'] as Set<LoadingUnit>) {
env.logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2);
}
env.logger.printStatus('');
}
if (_goldenComparisonResults['match'] as bool) {
env.logger.printStatus('No change in generated loading units.\n');
} else {
env.logger.printStatus('''
It is recommended to verify that the changed loading units are expected
and to update the `deferred-components` section in `pubspec.yaml` to
incorporate any changes. The full list of generated loading units can be
referenced in the $kDeferredComponentsGoldenFileName file located alongside
pubspec.yaml.
This loading unit check will not fail again on the next build attempt
if no additional changes to the loading units are detected.
$_thinDivider\n''');
}
}
// TODO(garyq): Add link to web tutorial/guide once it is written.
env.logger.printStatus('''
Setup verification can be skipped by passing the `--no-verify-deferred-components`
flag, however, doing so may put your app at risk of not functioning even if the
build is successful.
$_thickDivider''');
return;
}
env.logger.printStatus('$_title passed.');
}
void attemptToolExit() {
if (exitOnFail && changesNeeded) {
throwToolExit('Setup for deferred components incomplete. See recommended actions.', exitCode: 1);
}
}
} }
// Handles a single deferred component's android dynamic feature module // Handles a single deferred component's android dynamic feature module
......
// 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.
// @dart = 2.8
import '../base/common.dart';
import '../base/deferred_component.dart';
import '../base/file_system.dart';
import '../base/terminal.dart';
import '../build_system/build_system.dart';
import '../globals.dart' as globals;
/// A class to configure and run deferred component setup verification checks
/// and tasks.
///
/// Once constructed, checks and tasks can be executed by calling the respective
/// methods. The results of the checks are stored internally and can be
/// displayed to the user by calling [displayResults].
///
/// The results of each check are handled internally as they are not meant to
/// be run isolated.
abstract class DeferredComponentsValidator {
DeferredComponentsValidator(this.env, {
this.exitOnFail = true,
String title,
}) : outputDir = env.projectDir
.childDirectory('build')
.childDirectory(kDeferredComponentsTempDirectory),
inputs = <File>[],
outputs = <File>[],
title = title ?? 'Deferred components setup verification',
generatedFiles = <String>[],
modifiedFiles = <String>[],
invalidFiles = <String, String>{},
diffLines = <String>[];
/// The build environment that should be used to find the input files to run
/// checks against.
///
/// The checks in this class are meant to be used as part of a build process,
/// so an environment should be available.
final Environment env;
/// When true, failed checks and tasks will result in [attemptToolExit]
/// triggering [throwToolExit].
final bool exitOnFail;
/// The name of the golden file that tracks the latest loading units
/// generated.
static const String kLoadingUnitsCacheFileName = 'deferred_components_loading_units.yaml';
/// The directory in the build folder to generate missing/modified files into.
static const String kDeferredComponentsTempDirectory = 'android_deferred_components_setup_files';
/// The title printed at the top of the results of [displayResults]
final String title;
/// The temporary directory that the validator writes recommended files into.
final Directory outputDir;
/// Files that were newly generated by this validator.
final List<String> generatedFiles;
/// Existing files that were modified by this validator.
final List<String> modifiedFiles;
/// Files that were invalid and unable to be checked. These files are input
/// files that the validator tries to read rather than output files the
/// validator generates. The key is the file name and the value is the message
/// or reason it was invalid.
final Map<String, String> invalidFiles;
// TODO(garyq): implement the diff task.
/// Output of the diff task.
final List<String> diffLines;
/// Tracks the new and missing loading units.
Map<String, dynamic> loadingUnitComparisonResults;
/// All files read by the validator.
final List<File> inputs;
/// All files output by the validator.
final List<File> outputs;
/// Returns true if there were any recommended changes that should
/// be applied.
///
/// Retuns false if no problems or recommendations were detected.
///
/// If no checks are run, then this will default to false and will remain so
/// until a failing check finishes running.
bool get changesNeeded => generatedFiles.isNotEmpty
|| modifiedFiles.isNotEmpty
|| invalidFiles.isNotEmpty
|| (loadingUnitComparisonResults != null && !(loadingUnitComparisonResults['match'] as bool));
/// Handles the results of all executed checks by calling [displayResults] and
/// [attemptToolExit].
///
/// This should be called after all desired checks and tasks are executed.
void handleResults() {
displayResults();
attemptToolExit();
}
static const String _thickDivider = '=================================================================================';
static const String _thinDivider = '---------------------------------------------------------------------------------';
/// Displays the results of this validator's executed checks and tasks in a
/// human readable format.
///
/// All checks that are desired should be run before calling this method.
void displayResults() {
if (changesNeeded) {
env.logger.printStatus(_thickDivider);
env.logger.printStatus(title, indent: (_thickDivider.length - title.length) ~/ 2, emphasis: true);
env.logger.printStatus(_thickDivider);
// Log any file reading/existence errors.
if (invalidFiles.isNotEmpty) {
env.logger.printStatus('Errors checking the following files:\n', emphasis: true);
for (final String key in invalidFiles.keys) {
env.logger.printStatus(' - $key: ${invalidFiles[key]}\n');
}
}
// Log diff file contents, with color highlighting
if (diffLines != null && diffLines.isNotEmpty) {
env.logger.printStatus('Diff between `android` and expected files:', emphasis: true);
env.logger.printStatus('');
for (final String line in diffLines) {
// We only care about diffs in files that have
// counterparts.
if (line.startsWith('Only in android')) {
continue;
}
TerminalColor color = TerminalColor.grey;
if (line.startsWith('+')) {
color = TerminalColor.green;
} else if (line.startsWith('-')) {
color = TerminalColor.red;
}
env.logger.printStatus(line, color: color);
}
env.logger.printStatus('');
}
// Log any newly generated and modified files.
if (generatedFiles.isNotEmpty) {
env.logger.printStatus('Newly generated android files:', emphasis: true);
for (final String filePath in generatedFiles) {
final String shortenedPath = filePath.substring(env.projectDir.parent.path.length + 1);
env.logger.printStatus(' - $shortenedPath', color: TerminalColor.grey);
}
env.logger.printStatus('');
}
if (modifiedFiles.isNotEmpty) {
env.logger.printStatus('Modified android files:', emphasis: true);
for (final String filePath in modifiedFiles) {
final String shortenedPath = filePath.substring(env.projectDir.parent.path.length + 1);
env.logger.printStatus(' - $shortenedPath', color: TerminalColor.grey);
}
env.logger.printStatus('');
}
if (generatedFiles.isNotEmpty || modifiedFiles.isNotEmpty) {
env.logger.printStatus('''
The above files have been placed into `build/$kDeferredComponentsTempDirectory`,
a temporary directory. The files should be reviewed and moved into the project's
`android` directory.''');
if (diffLines != null && diffLines.isNotEmpty && !globals.platform.isWindows) {
env.logger.printStatus(r'''
The recommended changes can be quickly applied by running:
$ patch -p0 < build/setup_deferred_components.diff
''');
}
env.logger.printStatus('$_thinDivider\n');
}
// Log loading unit golden changes, if any.
if (loadingUnitComparisonResults != null) {
if ((loadingUnitComparisonResults['new'] as List<LoadingUnit>).isNotEmpty) {
env.logger.printStatus('New loading units were found:', emphasis: true);
for (final LoadingUnit unit in loadingUnitComparisonResults['new'] as List<LoadingUnit>) {
env.logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2);
}
env.logger.printStatus('');
}
if ((loadingUnitComparisonResults['missing'] as Set<LoadingUnit>).isNotEmpty) {
env.logger.printStatus('Previously existing loading units no longer exist:', emphasis: true);
for (final LoadingUnit unit in loadingUnitComparisonResults['missing'] as Set<LoadingUnit>) {
env.logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2);
}
env.logger.printStatus('');
}
if (loadingUnitComparisonResults['match'] as bool) {
env.logger.printStatus('No change in generated loading units.\n');
} else {
env.logger.printStatus('''
It is recommended to verify that the changed loading units are expected
and to update the `deferred-components` section in `pubspec.yaml` to
incorporate any changes. The full list of generated loading units can be
referenced in the $kLoadingUnitsCacheFileName file located alongside
pubspec.yaml.
This loading unit check will not fail again on the next build attempt
if no additional changes to the loading units are detected.
$_thinDivider\n''');
}
}
// TODO(garyq): Add link to web tutorial/guide once it is written.
env.logger.printStatus('''
Setup verification can be skipped by passing the `--no-verify-deferred-components`
flag, however, doing so may put your app at risk of not functioning even if the
build is successful.
$_thickDivider''');
return;
}
env.logger.printStatus('$title passed.');
}
void attemptToolExit() {
if (exitOnFail && changesNeeded) {
throwToolExit('Setup for deferred components incomplete. See recommended actions.', exitCode: 1);
}
}
}
...@@ -250,6 +250,29 @@ abstract class Target { ...@@ -250,6 +250,29 @@ abstract class Target {
} }
} }
/// Target that contains multiple other targets.
///
/// This target does not do anything in its own [build]
/// and acts as a wrapper around multiple other targets.
class CompositeTarget extends Target {
CompositeTarget(this.dependencies);
@override
final List<Target> dependencies;
@override
String get name => '_composite';
@override
Future<void> build(Environment environment) async { }
@override
List<Source> get inputs => <Source>[];
@override
List<Source> get outputs => <Source>[];
}
/// The [Environment] defines several constants for use during the build. /// The [Environment] defines several constants for use during the build.
/// ///
/// The environment contains configuration and file paths that are safe to /// The environment contains configuration and file paths that are safe to
......
// 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.
// @dart = 2.8
import 'package:meta/meta.dart';
import '../../android/deferred_components_gen_snapshot_validator.dart';
import '../../base/deferred_component.dart';
import '../../project.dart';
import '../build_system.dart';
import '../depfile.dart';
/// Creates a [DeferredComponentsGenSnapshotValidator], runs the checks, and displays the validator
/// output to the developer if changes are recommended.
class DeferredComponentsGenSnapshotValidatorTarget extends Target {
/// Create an [AndroidAotDeferredComponentsBundle] implementation for a given [targetPlatform] and [buildMode].
DeferredComponentsGenSnapshotValidatorTarget({
@required this.dependency,
@required this.abis,
this.title,
this.exitOnFail = true,
String name = 'deferred_components_setup_validator',
}) : _name = name;
/// The [AndroidAotDeferredComponentsBundle] derived target instances this rule depends on packed
/// as a [CompositeTarget].
final CompositeTarget dependency;
/// The title of the [DeferredComponentsGenSnapshotValidator] that is
/// displayed to the developer when logging results.
final String title;
/// Whether to exit the tool if a recommended change is found by the
/// [DeferredComponentsGenSnapshotValidator].
final bool exitOnFail;
/// The abis to validate.
final List<String> abis;
@override
String get name => _name;
final String _name;
@override
List<Source> get inputs => const <Source>[];
@override
List<Source> get outputs => const <Source>[];
@override
List<String> get depfiles => <String>[
'flutter_$name.d',
];
@override
List<Target> get dependencies => dependency == null ? <Target>[] : <Target>[dependency];
@visibleForTesting
DeferredComponentsGenSnapshotValidator validator;
@override
Future<void> build(Environment environment) async {
final DepfileService depfileService = DepfileService(
fileSystem: environment.fileSystem,
logger: environment.logger,
);
validator = DeferredComponentsGenSnapshotValidator(
environment,
title: title,
exitOnFail: exitOnFail,
);
final List<LoadingUnit> generatedLoadingUnits = LoadingUnit.parseGeneratedLoadingUnits(
environment.outputDir,
environment.logger,
abis: abis
);
validator
..checkAppAndroidManifestComponentLoadingUnitMapping(
FlutterProject.current().manifest.deferredComponents,
generatedLoadingUnits,
)
..checkAgainstLoadingUnitsCache(generatedLoadingUnits)
..writeLoadingUnitsCache(generatedLoadingUnits);
validator.handleResults();
depfileService.writeToFile(
Depfile(validator.inputs, validator.outputs),
environment.buildDir.childFile('flutter_$name.d'),
);
}
}
...@@ -227,7 +227,7 @@ class AssembleCommand extends FlutterCommand { ...@@ -227,7 +227,7 @@ class AssembleCommand extends FlutterCommand {
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
final List<Target> targets = createTargets(); final List<Target> targets = createTargets();
final Target target = targets.length == 1 ? targets.single : _CompositeTarget(targets); final Target target = targets.length == 1 ? targets.single : CompositeTarget(targets);
final BuildResult result = await globals.buildSystem.build( final BuildResult result = await globals.buildSystem.build(
target, target,
createEnvironment(), createEnvironment(),
...@@ -308,22 +308,3 @@ void writePerformanceData(Iterable<PerformanceMeasurement> measurements, File ou ...@@ -308,22 +308,3 @@ void writePerformanceData(Iterable<PerformanceMeasurement> measurements, File ou
} }
outFile.writeAsStringSync(json.encode(jsonData)); outFile.writeAsStringSync(json.encode(jsonData));
} }
class _CompositeTarget extends Target {
_CompositeTarget(this.dependencies);
@override
final List<Target> dependencies;
@override
String get name => '_composite';
@override
Future<void> build(Environment environment) async { }
@override
List<Source> get inputs => <Source>[];
@override
List<Source> get outputs => <Source>[];
}
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
// @dart = 2.8 // @dart = 2.8
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/deferred_components_setup_validator.dart'; import 'package:flutter_tools/src/android/deferred_components_gen_snapshot_validator.dart';
import 'package:flutter_tools/src/android/deferred_components_validator.dart';
import 'package:flutter_tools/src/base/deferred_component.dart'; import 'package:flutter_tools/src/base/deferred_component.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
...@@ -14,7 +15,6 @@ import 'package:flutter_tools/src/build_system/targets/common.dart'; ...@@ -14,7 +15,6 @@ import 'package:flutter_tools/src/build_system/targets/common.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart';
void main() { void main() {
FileSystem fileSystem; FileSystem fileSystem;
...@@ -48,7 +48,7 @@ void main() { ...@@ -48,7 +48,7 @@ void main() {
}); });
testWithoutContext('No checks passes', () async { testWithoutContext('No checks passes', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
...@@ -58,29 +58,17 @@ void main() { ...@@ -58,29 +58,17 @@ void main() {
expect(logger.statusText, 'test check passed.\n'); expect(logger.statusText, 'test check passed.\n');
}); });
testWithoutContext('clearTempDir passes', () async { testWithoutContext('writeCache passes', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
env, if (cacheFile.existsSync()) {
exitOnFail: false, cacheFile.deleteSync();
title: 'test check',
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText, 'test check passed.\n');
});
testWithoutContext('writeGolden passes', () async {
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
} }
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
validator.writeGolden( validator.writeLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 2, libraries: <String>['lib1']), LoadingUnit(id: 2, libraries: <String>['lib1']),
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
...@@ -91,7 +79,7 @@ void main() { ...@@ -91,7 +79,7 @@ void main() {
expect(logger.statusText, 'test check passed.\n'); expect(logger.statusText, 'test check passed.\n');
final File expectedFile = env.projectDir.childFile('deferred_components_golden.yaml'); final File expectedFile = env.projectDir.childFile('deferred_components_loading_units.yaml');
expect(expectedFile.existsSync(), true); expect(expectedFile.existsSync(), true);
const String expectedContents = const String expectedContents =
...@@ -108,18 +96,18 @@ loading-units: ...@@ -108,18 +96,18 @@ loading-units:
expect(expectedFile.readAsStringSync().contains(expectedContents), true); expect(expectedFile.readAsStringSync().contains(expectedContents), true);
}); });
testWithoutContext('loadingUnitGolden identical passes', () async { testWithoutContext('loadingUnitCache identical passes', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units: loading-units:
- id: 2 - id: 2
libraries: libraries:
...@@ -129,7 +117,7 @@ loading-units: ...@@ -129,7 +117,7 @@ loading-units:
- lib2 - lib2
- lib3 - lib3
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 2, libraries: <String>['lib1']), LoadingUnit(id: 2, libraries: <String>['lib1']),
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
...@@ -141,25 +129,25 @@ loading-units: ...@@ -141,25 +129,25 @@ loading-units:
expect(logger.statusText, 'test check passed.\n'); expect(logger.statusText, 'test check passed.\n');
}); });
testWithoutContext('loadingUnitGolden finds new loading units', () async { testWithoutContext('loadingUnitCache finds new loading units', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units: loading-units:
- id: 3 - id: 3
libraries: libraries:
- lib2 - lib2
- lib3 - lib3
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 2, libraries: <String>['lib1']), LoadingUnit(id: 2, libraries: <String>['lib1']),
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
...@@ -171,18 +159,18 @@ loading-units: ...@@ -171,18 +159,18 @@ loading-units:
expect(logger.statusText.contains('New loading units were found:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), true); expect(logger.statusText.contains('New loading units were found:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), true);
}); });
testWithoutContext('loadingUnitGolden finds missing loading units', () async { testWithoutContext('loadingUnitCache finds missing loading units', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units: loading-units:
- id: 2 - id: 2
libraries: libraries:
...@@ -192,7 +180,7 @@ loading-units: ...@@ -192,7 +180,7 @@ loading-units:
- lib2 - lib2
- lib3 - lib3
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
], ],
...@@ -203,17 +191,17 @@ loading-units: ...@@ -203,17 +191,17 @@ loading-units:
expect(logger.statusText.contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), true); expect(logger.statusText.contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), true);
}); });
testWithoutContext('missing golden file counts as all new loading units', () async { testWithoutContext('missing cache file counts as all new loading units', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 2, libraries: <String>['lib1']), LoadingUnit(id: 2, libraries: <String>['lib1']),
], ],
...@@ -224,18 +212,18 @@ loading-units: ...@@ -224,18 +212,18 @@ loading-units:
expect(logger.statusText.contains('New loading units were found:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), true); expect(logger.statusText.contains('New loading units were found:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), true);
}); });
testWithoutContext('loadingUnitGolden validator detects malformed file: missing main entry', () async { testWithoutContext('loadingUnitCache validator detects malformed file: missing main entry', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units-spelled-wrong: loading-units-spelled-wrong:
- id: 2 - id: 2
libraries: libraries:
...@@ -245,7 +233,7 @@ loading-units-spelled-wrong: ...@@ -245,7 +233,7 @@ loading-units-spelled-wrong:
- lib2 - lib2
- lib3 - lib3
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
], ],
...@@ -254,26 +242,26 @@ loading-units-spelled-wrong: ...@@ -254,26 +242,26 @@ loading-units-spelled-wrong:
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Errors checking the following files:'), true); expect(logger.statusText.contains('Errors checking the following files:'), true);
expect(logger.statusText.contains('Invalid golden yaml file, \'loading-units\' entry did not exist.'), true); expect(logger.statusText.contains('Invalid loading units yaml file, \'loading-units\' entry did not exist.'), true);
expect(logger.statusText.contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), false); expect(logger.statusText.contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), false);
}); });
testWithoutContext('loadingUnitGolden validator detects malformed file: not a list', () async { testWithoutContext('loadingUnitCache validator detects malformed file: not a list', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units: hello loading-units: hello
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
], ],
...@@ -282,26 +270,26 @@ loading-units: hello ...@@ -282,26 +270,26 @@ loading-units: hello
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Errors checking the following files:'), true); expect(logger.statusText.contains('Errors checking the following files:'), true);
expect(logger.statusText.contains('Invalid golden yaml file, \'loading-units\' is not a list.'), true); expect(logger.statusText.contains('Invalid loading units yaml file, \'loading-units\' is not a list.'), true);
}); });
testWithoutContext('loadingUnitGolden validator detects malformed file: not a list', () async { testWithoutContext('loadingUnitCache validator detects malformed file: not a list', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units: loading-units:
- 2 - 2
- 3 - 3
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
], ],
...@@ -310,21 +298,21 @@ loading-units: ...@@ -310,21 +298,21 @@ loading-units:
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Errors checking the following files:'), true); expect(logger.statusText.contains('Errors checking the following files:'), true);
expect(logger.statusText.contains('Invalid golden yaml file, \'loading-units\' is not a list of maps.'), true); expect(logger.statusText.contains('Invalid loading units yaml file, \'loading-units\' is not a list of maps.'), true);
}); });
testWithoutContext('loadingUnitGolden validator detects malformed file: missing id', () async { testWithoutContext('loadingUnitCache validator detects malformed file: missing id', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units: loading-units:
- id: 2 - id: 2
libraries: libraries:
...@@ -333,7 +321,7 @@ loading-units: ...@@ -333,7 +321,7 @@ loading-units:
- lib2 - lib2
- lib3 - lib3
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
], ],
...@@ -342,21 +330,21 @@ loading-units: ...@@ -342,21 +330,21 @@ loading-units:
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Errors checking the following files:'), true); expect(logger.statusText.contains('Errors checking the following files:'), true);
expect(logger.statusText.contains('Invalid golden yaml file, all loading units must have an \'id\''), true); expect(logger.statusText.contains('Invalid loading units yaml file, all loading units must have an \'id\''), true);
}); });
testWithoutContext('loadingUnitGolden validator detects malformed file: libraries is list', () async { testWithoutContext('loadingUnitCache validator detects malformed file: libraries is list', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units: loading-units:
- id: 2 - id: 2
libraries: libraries:
...@@ -364,7 +352,7 @@ loading-units: ...@@ -364,7 +352,7 @@ loading-units:
- id: 3 - id: 3
libraries: hello libraries: hello
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
], ],
...@@ -373,21 +361,21 @@ loading-units: ...@@ -373,21 +361,21 @@ loading-units:
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Errors checking the following files:'), true); expect(logger.statusText.contains('Errors checking the following files:'), true);
expect(logger.statusText.contains('Invalid golden yaml file, \'libraries\' is not a list.'), true); expect(logger.statusText.contains('Invalid loading units yaml file, \'libraries\' is not a list.'), true);
}); });
testWithoutContext('loadingUnitGolden validator detects malformed file: libraries is list of strings', () async { testWithoutContext('loadingUnitCache validator detects malformed file: libraries is list of strings', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units: loading-units:
- id: 2 - id: 2
libraries: libraries:
...@@ -397,7 +385,7 @@ loading-units: ...@@ -397,7 +385,7 @@ loading-units:
- blah: hello - blah: hello
blah2: hello2 blah2: hello2
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
], ],
...@@ -406,21 +394,21 @@ loading-units: ...@@ -406,21 +394,21 @@ loading-units:
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Errors checking the following files:'), true); expect(logger.statusText.contains('Errors checking the following files:'), true);
expect(logger.statusText.contains('Invalid golden yaml file, \'libraries\' is not a list of strings.'), true); expect(logger.statusText.contains('Invalid loading units yaml file, \'libraries\' is not a list of strings.'), true);
}); });
testWithoutContext('loadingUnitGolden validator detects malformed file: empty libraries allowed', () async { testWithoutContext('loadingUnitCache validator detects malformed file: empty libraries allowed', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName); final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName);
if (goldenFile.existsSync()) { if (cacheFile.existsSync()) {
goldenFile.deleteSync(); cacheFile.deleteSync();
} }
goldenFile.createSync(recursive: true); cacheFile.createSync(recursive: true);
goldenFile.writeAsStringSync(''' cacheFile.writeAsStringSync('''
loading-units: loading-units:
- id: 2 - id: 2
libraries: libraries:
...@@ -428,7 +416,7 @@ loading-units: ...@@ -428,7 +416,7 @@ loading-units:
- id: 3 - id: 3
libraries: libraries:
''', flush: true, mode: FileMode.append); ''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden( validator.checkAgainstLoadingUnitsCache(
<LoadingUnit>[ <LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']), LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
], ],
...@@ -439,231 +427,13 @@ loading-units: ...@@ -439,231 +427,13 @@ loading-units:
expect(logger.statusText.contains('Errors checking the following files:'), false); expect(logger.statusText.contains('Errors checking the following files:'), false);
}); });
testUsingContext('androidComponentSetup build.gradle does not exist', () async {
final Directory templatesDir = env.flutterRootDir.childDirectory('templates').childDirectory('deferred_component');
final File buildGradleTemplate = templatesDir.childFile('build.gradle.tmpl');
final File androidManifestTemplate = templatesDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml.tmpl');
if (templatesDir.existsSync()) {
templatesDir.deleteSync(recursive: true);
}
buildGradleTemplate.createSync(recursive: true);
androidManifestTemplate.createSync(recursive: true);
buildGradleTemplate.writeAsStringSync('fake build.gradle template {{componentName}}', flush: true, mode: FileMode.append);
androidManifestTemplate.writeAsStringSync('fake AndroidManigest.xml template {{componentName}}', flush: true, mode: FileMode.append);
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
templatesDir: templatesDir,
);
final Directory componentDir = env.projectDir.childDirectory('android').childDirectory('component1');
final File file = componentDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
if (file.existsSync()) {
file.deleteSync();
}
file.createSync(recursive: true);
await validator.checkAndroidDynamicFeature(
<DeferredComponent>[
DeferredComponent(name: 'component1'),
],
);
validator.displayResults();
validator.attemptToolExit();
file.deleteSync();
expect(logger.statusText.contains('Newly generated android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/component1/build.gradle\n'), true);
});
testUsingContext('androidComponentSetup AndroidManifest.xml does not exist', () async {
final Directory templatesDir = env.flutterRootDir.childDirectory('templates').childDirectory('deferred_component');
final File buildGradleTemplate = templatesDir.childFile('build.gradle.tmpl');
final File androidManifestTemplate = templatesDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml.tmpl');
if (templatesDir.existsSync()) {
templatesDir.deleteSync(recursive: true);
}
buildGradleTemplate.createSync(recursive: true);
androidManifestTemplate.createSync(recursive: true);
buildGradleTemplate.writeAsStringSync('fake build.gradle template {{componentName}}', flush: true, mode: FileMode.append);
androidManifestTemplate.writeAsStringSync('fake AndroidManigest.xml template {{componentName}}', flush: true, mode: FileMode.append);
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
templatesDir: templatesDir,
);
final Directory componentDir = env.projectDir.childDirectory('android').childDirectory('component1');
final File file = componentDir.childFile('build.gradle');
if (file.existsSync()) {
file.deleteSync();
}
file.createSync(recursive: true);
await validator.checkAndroidDynamicFeature(
<DeferredComponent>[
DeferredComponent(name: 'component1'),
],
);
validator.displayResults();
validator.attemptToolExit();
file.deleteSync();
expect(logger.statusText.contains('Newly generated android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/component1/src/main/AndroidManifest.xml\n'), true);
});
testUsingContext('androidComponentSetup all files exist passes', () async {
final Directory templatesDir = env.flutterRootDir.childDirectory('templates').childDirectory('deferred_component');
final File buildGradleTemplate = templatesDir.childFile('build.gradle.tmpl');
final File androidManifestTemplate = templatesDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml.tmpl');
if (templatesDir.existsSync()) {
templatesDir.deleteSync(recursive: true);
}
buildGradleTemplate.createSync(recursive: true);
androidManifestTemplate.createSync(recursive: true);
buildGradleTemplate.writeAsStringSync('fake build.gradle template {{componentName}}', flush: true, mode: FileMode.append);
androidManifestTemplate.writeAsStringSync('fake AndroidManigest.xml template {{componentName}}', flush: true, mode: FileMode.append);
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
templatesDir: templatesDir,
);
final Directory componentDir = env.projectDir.childDirectory('android').childDirectory('component1');
final File buildGradle = componentDir.childFile('build.gradle');
if (buildGradle.existsSync()) {
buildGradle.deleteSync();
}
buildGradle.createSync(recursive: true);
final File manifest = componentDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
if (manifest.existsSync()) {
manifest.deleteSync();
}
manifest.createSync(recursive: true);
await validator.checkAndroidDynamicFeature(
<DeferredComponent>[
DeferredComponent(name: 'component1'),
],
);
validator.displayResults();
validator.attemptToolExit();
manifest.deleteSync();
buildGradle.deleteSync();
expect(logger.statusText, 'test check passed.\n');
});
testWithoutContext('androidStringMapping creates new file', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app');
final File stringRes = baseModuleDir.childDirectory('src').childDirectory('main').childDirectory('res').childDirectory('values').childFile('strings.xml');
if (stringRes.existsSync()) {
stringRes.deleteSync();
}
final File manifest = baseModuleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
if (manifest.existsSync()) {
manifest.deleteSync();
}
manifest.createSync(recursive: true);
manifest.writeAsStringSync('''
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.splitaot">
<application
android:name="io.flutter.app.FlutterPlayStoreSplitApplication"
android:label="splitaot"
android:extractNativeLibs="false">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize">
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping"
android:value="invalidmapping" />
</application>
</manifest>
''', flush: true, mode: FileMode.append);
validator.checkAppAndroidManifestComponentLoadingUnitMapping(
<DeferredComponent>[
DeferredComponent(name: 'component1', libraries: <String>['lib2']),
DeferredComponent(name: 'component2', libraries: <String>['lib1', 'lib4']),
],
<LoadingUnit>[
LoadingUnit(id: 2, libraries: <String>['lib1']),
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
LoadingUnit(id: 4, libraries: <String>['lib4', 'lib5']),
],
);
validator.checkAndroidResourcesStrings(
<DeferredComponent>[
DeferredComponent(name: 'component1', libraries: <String>['lib2']),
DeferredComponent(name: 'component2', libraries: <String>['lib1', 'lib4']),
],
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText.contains('Modified android files:\n'), true);
expect(logger.statusText.contains('Newly generated android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/app/src/main/res/values/strings.xml\n'), true);
final File stringsOutput = env.projectDir
.childDirectory('build')
.childDirectory(DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory)
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
expect(stringsOutput.existsSync(), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component1Name">component1</string>'), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component2Name">component2</string>'), true);
final File manifestOutput = env.projectDir
.childDirectory('build')
.childDirectory(DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory)
.childDirectory('app')
.childDirectory('src')
.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"/>'), true);
expect(manifestOutput.readAsStringSync().contains('android:value="invalidmapping"'), false);
expect(manifestOutput.readAsStringSync().contains('<!-- Don\'t delete the meta-data below.'), true);
});
testWithoutContext('androidStringMapping modifies strings file', () async { testWithoutContext('androidStringMapping modifies strings file', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app'); final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app');
final File stringRes = baseModuleDir.childDirectory('src').childDirectory('main').childDirectory('res').childDirectory('values').childFile('strings.xml');
if (stringRes.existsSync()) {
stringRes.deleteSync();
}
stringRes.createSync(recursive: true);
stringRes.writeAsStringSync('''
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="component1Name">component1</string>
</resources>
''', flush: true, mode: FileMode.append);
final File manifest = baseModuleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml'); final File manifest = baseModuleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
if (manifest.existsSync()) { if (manifest.existsSync()) {
manifest.deleteSync(); manifest.deleteSync();
...@@ -706,35 +476,15 @@ loading-units: ...@@ -706,35 +476,15 @@ loading-units:
LoadingUnit(id: 4, libraries: <String>['lib4', 'lib5']), LoadingUnit(id: 4, libraries: <String>['lib4', 'lib5']),
], ],
); );
validator.checkAndroidResourcesStrings(
<DeferredComponent>[
DeferredComponent(name: 'component1', libraries: <String>['lib2']),
DeferredComponent(name: 'component2', libraries: <String>['lib1', 'lib4']),
],
);
validator.displayResults(); validator.displayResults();
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Modified android files:\n'), true); expect(logger.statusText.contains('Modified android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true); expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/app/src/main/res/values/strings.xml\n'), true);
final File stringsOutput = env.projectDir
.childDirectory('build')
.childDirectory(DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory)
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
expect(stringsOutput.existsSync(), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component1Name">component1</string>'), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component2Name">component2</string>'), true);
final File manifestOutput = env.projectDir final File manifestOutput = env.projectDir
.childDirectory('build') .childDirectory('build')
.childDirectory(DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory) .childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory)
.childDirectory('app') .childDirectory('app')
.childDirectory('src') .childDirectory('src')
.childDirectory('main') .childDirectory('main')
...@@ -746,24 +496,12 @@ loading-units: ...@@ -746,24 +496,12 @@ loading-units:
}); });
testWithoutContext('androidStringMapping adds mapping when no existing mapping', () async { testWithoutContext('androidStringMapping adds mapping when no existing mapping', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app'); final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app');
final File stringRes = baseModuleDir.childDirectory('src').childDirectory('main').childDirectory('res').childDirectory('values').childFile('strings.xml');
if (stringRes.existsSync()) {
stringRes.deleteSync();
}
stringRes.createSync(recursive: true);
stringRes.writeAsStringSync('''
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="component1Name">component1</string>
</resources>
''', flush: true, mode: FileMode.append);
final File manifest = baseModuleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml'); final File manifest = baseModuleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
if (manifest.existsSync()) { if (manifest.existsSync()) {
manifest.deleteSync(); manifest.deleteSync();
...@@ -800,35 +538,15 @@ loading-units: ...@@ -800,35 +538,15 @@ loading-units:
LoadingUnit(id: 4, libraries: <String>['lib4', 'lib5']), LoadingUnit(id: 4, libraries: <String>['lib4', 'lib5']),
], ],
); );
validator.checkAndroidResourcesStrings(
<DeferredComponent>[
DeferredComponent(name: 'component1', libraries: <String>['lib2']),
DeferredComponent(name: 'component2', libraries: <String>['lib1', 'lib4']),
],
);
validator.displayResults(); validator.displayResults();
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Modified android files:\n'), true); expect(logger.statusText.contains('Modified android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true); expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/app/src/main/res/values/strings.xml\n'), true);
final File stringsOutput = env.projectDir
.childDirectory('build')
.childDirectory(DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory)
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
expect(stringsOutput.existsSync(), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component1Name">component1</string>'), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component2Name">component2</string>'), true);
final File manifestOutput = env.projectDir final File manifestOutput = env.projectDir
.childDirectory('build') .childDirectory('build')
.childDirectory(DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory) .childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory)
.childDirectory('app') .childDirectory('app')
.childDirectory('src') .childDirectory('src')
.childDirectory('main') .childDirectory('main')
...@@ -840,24 +558,12 @@ loading-units: ...@@ -840,24 +558,12 @@ loading-units:
// Tests if all of the regexp whitespace detection is working. // Tests if all of the regexp whitespace detection is working.
testWithoutContext('androidStringMapping handles whitespace within entry', () async { testWithoutContext('androidStringMapping handles whitespace within entry', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator( final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator(
env, env,
exitOnFail: false, exitOnFail: false,
title: 'test check', title: 'test check',
); );
final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app'); final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app');
final File stringRes = baseModuleDir.childDirectory('src').childDirectory('main').childDirectory('res').childDirectory('values').childFile('strings.xml');
if (stringRes.existsSync()) {
stringRes.deleteSync();
}
stringRes.createSync(recursive: true);
stringRes.writeAsStringSync('''
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="component1Name">component1</string>
</resources>
''', flush: true, mode: FileMode.append);
final File manifest = baseModuleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml'); final File manifest = baseModuleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
if (manifest.existsSync()) { if (manifest.existsSync()) {
manifest.deleteSync(); manifest.deleteSync();
...@@ -901,35 +607,15 @@ loading-units: ...@@ -901,35 +607,15 @@ loading-units:
LoadingUnit(id: 4, libraries: <String>['lib4', 'lib5']), LoadingUnit(id: 4, libraries: <String>['lib4', 'lib5']),
], ],
); );
validator.checkAndroidResourcesStrings(
<DeferredComponent>[
DeferredComponent(name: 'component1', libraries: <String>['lib2']),
DeferredComponent(name: 'component2', libraries: <String>['lib1', 'lib4']),
],
);
validator.displayResults(); validator.displayResults();
validator.attemptToolExit(); validator.attemptToolExit();
expect(logger.statusText.contains('Modified android files:\n'), true); expect(logger.statusText.contains('Modified android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true); expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory}/app/src/main/res/values/strings.xml\n'), true);
final File stringsOutput = env.projectDir
.childDirectory('build')
.childDirectory(DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory)
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
expect(stringsOutput.existsSync(), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component1Name">component1</string>'), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component2Name">component2</string>'), true);
final File manifestOutput = env.projectDir final File manifestOutput = env.projectDir
.childDirectory('build') .childDirectory('build')
.childDirectory(DeferredComponentsSetupValidator.kDeferredComponentsTempDirectory) .childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory)
.childDirectory('app') .childDirectory('app')
.childDirectory('src') .childDirectory('src')
.childDirectory('main') .childDirectory('main')
......
// 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.
// @dart = 2.8
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/deferred_components_prebuild_validator.dart';
import 'package:flutter_tools/src/android/deferred_components_validator.dart';
import 'package:flutter_tools/src/base/deferred_component.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/common.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
FileSystem fileSystem;
BufferLogger logger;
Environment env;
Environment createEnvironment() {
final Map<String, String> defines = <String, String>{ kSplitAot: 'true' };
final Environment result = Environment(
outputDir: fileSystem.directory('/output'),
buildDir: fileSystem.directory('/build'),
projectDir: fileSystem.directory('/project'),
defines: defines,
inputs: <String, String>{},
cacheDir: fileSystem.directory('/cache'),
flutterRootDir: fileSystem.directory('/flutter_root'),
artifacts: globals.artifacts,
fileSystem: fileSystem,
logger: logger,
processManager: globals.processManager,
engineVersion: 'invalidEngineVersion',
generateDartPluginRegistry: false,
);
return result;
}
setUp(() {
fileSystem = MemoryFileSystem.test();
logger = BufferLogger.test();
env = createEnvironment();
});
testWithoutContext('No checks passes', () async {
final DeferredComponentsPrebuildValidator validator = DeferredComponentsPrebuildValidator(
env,
exitOnFail: false,
title: 'test check',
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText, 'test check passed.\n');
});
testWithoutContext('clearTempDir passes', () async {
final DeferredComponentsPrebuildValidator validator = DeferredComponentsPrebuildValidator(
env,
exitOnFail: false,
title: 'test check',
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText, 'test check passed.\n');
});
testUsingContext('androidComponentSetup build.gradle does not exist', () async {
final Directory templatesDir = env.flutterRootDir.childDirectory('templates').childDirectory('deferred_component');
final File buildGradleTemplate = templatesDir.childFile('build.gradle.tmpl');
final File androidManifestTemplate = templatesDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml.tmpl');
if (templatesDir.existsSync()) {
templatesDir.deleteSync(recursive: true);
}
buildGradleTemplate.createSync(recursive: true);
androidManifestTemplate.createSync(recursive: true);
buildGradleTemplate.writeAsStringSync('fake build.gradle template {{componentName}}', flush: true, mode: FileMode.append);
androidManifestTemplate.writeAsStringSync('fake AndroidManigest.xml template {{componentName}}', flush: true, mode: FileMode.append);
final DeferredComponentsPrebuildValidator validator = DeferredComponentsPrebuildValidator(
env,
exitOnFail: false,
title: 'test check',
templatesDir: templatesDir,
);
final Directory componentDir = env.projectDir.childDirectory('android').childDirectory('component1');
final File file = componentDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
if (file.existsSync()) {
file.deleteSync();
}
file.createSync(recursive: true);
await validator.checkAndroidDynamicFeature(
<DeferredComponent>[
DeferredComponent(name: 'component1'),
],
);
validator.displayResults();
validator.attemptToolExit();
file.deleteSync();
expect(logger.statusText.contains('Newly generated android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/component1/build.gradle\n'), true);
});
testUsingContext('androidComponentSetup AndroidManifest.xml does not exist', () async {
final Directory templatesDir = env.flutterRootDir.childDirectory('templates').childDirectory('deferred_component');
final File buildGradleTemplate = templatesDir.childFile('build.gradle.tmpl');
final File androidManifestTemplate = templatesDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml.tmpl');
if (templatesDir.existsSync()) {
templatesDir.deleteSync(recursive: true);
}
buildGradleTemplate.createSync(recursive: true);
androidManifestTemplate.createSync(recursive: true);
buildGradleTemplate.writeAsStringSync('fake build.gradle template {{componentName}}', flush: true, mode: FileMode.append);
androidManifestTemplate.writeAsStringSync('fake AndroidManigest.xml template {{componentName}}', flush: true, mode: FileMode.append);
final DeferredComponentsPrebuildValidator validator = DeferredComponentsPrebuildValidator(
env,
exitOnFail: false,
title: 'test check',
templatesDir: templatesDir,
);
final Directory componentDir = env.projectDir.childDirectory('android').childDirectory('component1');
final File file = componentDir.childFile('build.gradle');
if (file.existsSync()) {
file.deleteSync();
}
file.createSync(recursive: true);
await validator.checkAndroidDynamicFeature(
<DeferredComponent>[
DeferredComponent(name: 'component1'),
],
);
validator.displayResults();
validator.attemptToolExit();
file.deleteSync();
expect(logger.statusText.contains('Newly generated android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/component1/src/main/AndroidManifest.xml\n'), true);
});
testUsingContext('androidComponentSetup all files exist passes', () async {
final Directory templatesDir = env.flutterRootDir.childDirectory('templates').childDirectory('deferred_component');
final File buildGradleTemplate = templatesDir.childFile('build.gradle.tmpl');
final File androidManifestTemplate = templatesDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml.tmpl');
if (templatesDir.existsSync()) {
templatesDir.deleteSync(recursive: true);
}
buildGradleTemplate.createSync(recursive: true);
androidManifestTemplate.createSync(recursive: true);
buildGradleTemplate.writeAsStringSync('fake build.gradle template {{componentName}}', flush: true, mode: FileMode.append);
androidManifestTemplate.writeAsStringSync('fake AndroidManigest.xml template {{componentName}}', flush: true, mode: FileMode.append);
final DeferredComponentsPrebuildValidator validator = DeferredComponentsPrebuildValidator(
env,
exitOnFail: false,
title: 'test check',
templatesDir: templatesDir,
);
final Directory componentDir = env.projectDir.childDirectory('android').childDirectory('component1');
final File buildGradle = componentDir.childFile('build.gradle');
if (buildGradle.existsSync()) {
buildGradle.deleteSync();
}
buildGradle.createSync(recursive: true);
final File manifest = componentDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
if (manifest.existsSync()) {
manifest.deleteSync();
}
manifest.createSync(recursive: true);
await validator.checkAndroidDynamicFeature(
<DeferredComponent>[
DeferredComponent(name: 'component1'),
],
);
validator.displayResults();
validator.attemptToolExit();
manifest.deleteSync();
buildGradle.deleteSync();
expect(logger.statusText, 'test check passed.\n');
});
testWithoutContext('androidStringMapping creates new file', () async {
final DeferredComponentsPrebuildValidator validator = DeferredComponentsPrebuildValidator(
env,
exitOnFail: false,
title: 'test check',
);
final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app');
final File stringRes = baseModuleDir.childDirectory('src').childDirectory('main').childDirectory('res').childDirectory('values').childFile('strings.xml');
if (stringRes.existsSync()) {
stringRes.deleteSync();
}
final File manifest = baseModuleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
if (manifest.existsSync()) {
manifest.deleteSync();
}
manifest.createSync(recursive: true);
manifest.writeAsStringSync('''
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.splitaot">
<application
android:name="io.flutter.app.FlutterPlayStoreSplitApplication"
android:label="splitaot"
android:extractNativeLibs="false">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize">
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping"
android:value="invalidmapping" />
</application>
</manifest>
''', flush: true, mode: FileMode.append);
validator.checkAndroidResourcesStrings(
<DeferredComponent>[
DeferredComponent(name: 'component1', libraries: <String>['lib2']),
DeferredComponent(name: 'component2', libraries: <String>['lib1', 'lib4']),
],
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText.contains('Modified android files:\n'), false);
expect(logger.statusText.contains('Newly generated android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/app/src/main/res/values/strings.xml\n'), true);
final File stringsOutput = env.projectDir
.childDirectory('build')
.childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory)
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
expect(stringsOutput.existsSync(), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component1Name">component1</string>'), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component2Name">component2</string>'), true);
});
testWithoutContext('androidStringMapping modifies strings file', () async {
final DeferredComponentsPrebuildValidator validator = DeferredComponentsPrebuildValidator(
env,
exitOnFail: false,
title: 'test check',
);
final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app');
final File stringRes = baseModuleDir.childDirectory('src').childDirectory('main').childDirectory('res').childDirectory('values').childFile('strings.xml');
if (stringRes.existsSync()) {
stringRes.deleteSync();
}
stringRes.createSync(recursive: true);
stringRes.writeAsStringSync('''
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="component1Name">component1</string>
</resources>
''', flush: true, mode: FileMode.append);
validator.checkAndroidResourcesStrings(
<DeferredComponent>[
DeferredComponent(name: 'component1', libraries: <String>['lib2']),
DeferredComponent(name: 'component2', libraries: <String>['lib1', 'lib4']),
],
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText.contains('Newly generated android files:\n'), false);
expect(logger.statusText.contains('Modified android files:\n'), true);
expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/app/src/main/res/values/strings.xml\n'), true);
final File stringsOutput = env.projectDir
.childDirectory('build')
.childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory)
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
expect(stringsOutput.existsSync(), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component1Name">component1</string>'), true);
expect(stringsOutput.readAsStringSync().contains('<string name="component2Name">component2</string>'), true);
});
}
// 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.
// @dart = 2.8
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/deferred_components.dart';
import 'package:flutter_tools/src/build_system/targets/android.dart';
import 'package:flutter_tools/src/build_system/targets/common.dart';
import '../../../src/common.dart';
import '../../../src/context.dart';
// These tests perform a simple check to verify if the check/task was executed at all.
// Detailed per-check tests are in android/deferred_components_setup_validator_test.dart.
void main() {
FileSystem fileSystem;
BufferLogger logger;
setUp(() {
logger = BufferLogger.test();
fileSystem = MemoryFileSystem.test();
});
testUsingContext('checkAppAndroidManifestComponentLoadingUnitMapping checks runs', () async {
final Environment environment = Environment.test(
fileSystem.currentDirectory,
outputDir: fileSystem.directory('out')..createSync(),
buildDir: fileSystem.directory('build')..createSync(),
projectDir: fileSystem.directory('project')..createSync(),
defines: <String, String>{
kSplitAot: 'true',
},
artifacts: null,
processManager: null,
fileSystem: fileSystem,
logger: logger,
);
environment.buildDir.createSync(recursive: true);
const AndroidAot androidAot = AndroidAot(TargetPlatform.android_arm64, BuildMode.release);
const AndroidAotBundle androidAotBundle = AndroidAotBundle(androidAot);
final CompositeTarget androidDefBundle = CompositeTarget(<Target>[androidAotBundle]);
final CompositeTarget compositeTarget = CompositeTarget(<Target>[androidDefBundle]);
final DeferredComponentsGenSnapshotValidatorTarget validatorTarget = DeferredComponentsGenSnapshotValidatorTarget(
dependency: compositeTarget,
abis: <String>['arm64-v8a'],
title: 'test checks',
exitOnFail: false,
);
await validatorTarget.build(environment);
// We check the inputs to determine if the task was executed.
expect(validatorTarget.validator.inputs.length, 3);
expect(validatorTarget.validator.inputs[0].path, 'project/pubspec.yaml');
expect(validatorTarget.validator.inputs[1].path, 'project/android/app/src/main/AndroidManifest.xml');
});
testUsingContext('checkAgainstLoadingUnitsCache checks runs', () async {
final Environment environment = Environment.test(
fileSystem.currentDirectory,
outputDir: fileSystem.directory('out')..createSync(),
buildDir: fileSystem.directory('build')..createSync(),
projectDir: fileSystem.directory('project')..createSync(),
defines: <String, String>{
kSplitAot: 'true',
},
artifacts: null,
processManager: null,
fileSystem: fileSystem,
logger: logger,
);
environment.buildDir.createSync(recursive: true);
const AndroidAot androidAot = AndroidAot(TargetPlatform.android_arm64, BuildMode.release);
const AndroidAotBundle androidAotBundle = AndroidAotBundle(androidAot);
final CompositeTarget androidDefBundle = CompositeTarget(<Target>[androidAotBundle]);
final CompositeTarget compositeTarget = CompositeTarget(<Target>[androidDefBundle]);
final DeferredComponentsGenSnapshotValidatorTarget validatorTarget = DeferredComponentsGenSnapshotValidatorTarget(
dependency: compositeTarget,
abis: <String>['arm64-v8a'],
title: 'test checks',
exitOnFail: false,
);
await validatorTarget.build(environment);
// We check the inputs to determine if the task was executed.
expect(validatorTarget.validator.inputs.length, 3);
expect(validatorTarget.validator.inputs[2].path, 'project/deferred_components_loading_units.yaml');
});
testUsingContext('writeLoadingUnitsCache task runs', () async {
final Environment environment = Environment.test(
fileSystem.currentDirectory,
outputDir: fileSystem.directory('out')..createSync(),
buildDir: fileSystem.directory('build')..createSync(),
projectDir: fileSystem.directory('project')..createSync(),
defines: <String, String>{
kSplitAot: 'true',
},
artifacts: null,
processManager: null,
fileSystem: fileSystem,
logger: logger,
);
environment.buildDir.createSync(recursive: true);
const AndroidAot androidAot = AndroidAot(TargetPlatform.android_arm64, BuildMode.release);
const AndroidAotBundle androidAotBundle = AndroidAotBundle(androidAot);
final CompositeTarget androidDefBundle = CompositeTarget(<Target>[androidAotBundle]);
final CompositeTarget compositeTarget = CompositeTarget(<Target>[androidDefBundle]);
final DeferredComponentsGenSnapshotValidatorTarget validatorTarget = DeferredComponentsGenSnapshotValidatorTarget(
dependency: compositeTarget,
abis: <String>['arm64-v8a'],
title: 'test checks',
exitOnFail: false,
);
await validatorTarget.build(environment);
// We check the inputs to determine if the task was executed.
expect(validatorTarget.validator.outputs.length, 1);
expect(validatorTarget.validator.outputs[0].path, 'project/deferred_components_loading_units.yaml');
});
}
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