Unverified Commit ba5cc2cb authored by Gary Qian's avatar Gary Qian Committed by GitHub

[flutter_tools] Deferred components setup validator (#75739)

parent bfcb43d2
// 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 'package:xml/xml.dart';
import 'package:yaml/yaml.dart';
import '../base/common.dart';
import '../base/deferred_component.dart';
import '../base/error_handling_io.dart';
import '../base/file_system.dart';
import '../base/terminal.dart';
import '../build_system/build_system.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../template.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 DeferredComponentsSetupValidator {
/// 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.
DeferredComponentsSetupValidator(this.env, {
this.exitOnFail = true,
String title,
Directory templatesDir,
}) : _outputDir = env.projectDir
.childDirectory('build')
.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 _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
/// component.
///
/// Returns true if the check passed with no recommended changes, and false
/// otherwise.
///
/// This method looks for the existence of `android/<componentname>/build.gradle`
/// and `android/<componentname>/src/main/AndroidManifest.xml`. If either of
/// these files does not exist, it will generate it in the validator output
/// directory based off of a template.
///
/// 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.
Future<bool> checkAndroidDynamicFeature(List<DeferredComponent> components) async {
bool changesMade = false;
for (final DeferredComponent component in components) {
final _DeferredComponentAndroidFiles androidFiles = _DeferredComponentAndroidFiles(
name: component.name,
env: env,
templatesDir: _templatesDir
);
if (!androidFiles.verifyFilesExist()) {
// generate into temp directory
final Map<String, List<File>> results =
await androidFiles.generateFiles(
alternateAndroidDir: _outputDir,
clearAlternateOutputDir: true,
);
for (final File file in results['outputs']) {
_generatedFiles.add(file.path);
changesMade = true;
}
_outputs.addAll(results['outputs']);
_inputs.addAll(results['inputs']);
}
}
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
/// resources for each component's name.
///
/// Returns true if the check passed with no recommended changes, and false
/// otherwise.
///
/// In each dynamic feature module's AndroidManifest.xml, the
/// name of the module is a string resource. This checks if
/// the needed string resources are in the base module `strings.xml`.
/// If not, this method will generate a modified `strings.xml` (or a
/// completely new one if the original file did not exist) in the
/// validator's output directory.
///
/// For example, if there is a deferred component named `component1`,
/// there should be the following string resource:
///
/// <string name="component1Name">component1</string>
///
/// The string element's name attribute should be the component name with
/// `Name` as a suffix, and the text contents should be the component name.
bool checkAndroidResourcesStrings(List<DeferredComponent> components) {
final Directory androidDir = env.projectDir.childDirectory('android');
// Add component name mapping to strings.xml
final File stringRes = androidDir
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
_inputs.add(stringRes);
final File stringResOutput = _outputDir
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
ErrorHandlingFileSystem.deleteIfExists(stringResOutput);
final Map<String, String> requiredEntriesMap = <String, String>{};
for (final DeferredComponent component in components) {
requiredEntriesMap['${component.name}Name'] = component.name;
}
if (stringRes.existsSync()) {
bool modified = false;
XmlDocument document;
try {
document = XmlDocument.parse(stringRes.readAsStringSync());
} on XmlParserException {
_invalidFiles[stringRes.path] = 'Error parsing $stringRes '
'Please ensure that the strings.xml is a valid XML document and '
'try again.';
return false;
}
// Check if all required lines are present, and fix if name exists, but
// wrong string stored.
for (final XmlElement resources in document.findAllElements('resources')) {
for (final XmlElement element in resources.findElements('string')) {
final String name = element.getAttribute('name');
if (requiredEntriesMap.containsKey(name)) {
if (element.text != null && element.text != requiredEntriesMap[name]) {
element.innerText = requiredEntriesMap[name];
modified = true;
}
requiredEntriesMap.remove(name);
}
}
for (final String key in requiredEntriesMap.keys) {
modified = true;
final XmlElement newStringElement = XmlElement(
XmlName.fromString('string'),
<XmlAttribute>[
XmlAttribute(XmlName.fromString('name'), key),
],
<XmlNode>[
XmlText(requiredEntriesMap[key]),
],
);
resources.children.add(newStringElement);
}
break;
}
if (modified) {
stringResOutput.createSync(recursive: true);
stringResOutput.writeAsStringSync(document.toXmlString(pretty: true));
_modifiedFiles.add(stringResOutput.path);
return false;
}
return true;
}
// strings.xml does not exist, generate completely new file.
stringResOutput.createSync(recursive: true);
final StringBuffer buffer = StringBuffer();
buffer.writeln('''
<?xml version="1.0" encoding="utf-8"?>
<resources>
''');
for (final String key in requiredEntriesMap.keys) {
buffer.write(' <string name="$key">${requiredEntriesMap[key]}</string>\n');
}
buffer.write(
'''
</resources>
''');
stringResOutput.writeAsStringSync(buffer.toString(), flush: true, mode: FileMode.append);
_generatedFiles.add(stringResOutput.path);
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.
void clearOutputDir() {
final Directory dir = env.projectDir.childDirectory('build').childDirectory(kDeferredComponentsTempDirectory);
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
// directory.
class _DeferredComponentAndroidFiles {
_DeferredComponentAndroidFiles({
@required this.name,
@required this.env,
Directory templatesDir,
}) : _templatesDir = templatesDir;
// The name of the deferred component.
final String name;
final Environment env;
final Directory _templatesDir;
Directory get androidDir => env.projectDir.childDirectory('android');
Directory get componentDir => androidDir.childDirectory(name);
File get androidManifestFile => componentDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
File get buildGradleFile => componentDir.childFile('build.gradle');
// True when AndroidManifest.xml and build.gradle exist for the android dynamic feature.
bool verifyFilesExist() {
return androidManifestFile.existsSync() && buildGradleFile.existsSync();
}
// Generates any missing basic files for the dynamic feature into a temporary directory.
Future<Map<String, List<File>>> generateFiles({Directory alternateAndroidDir, bool clearAlternateOutputDir = false}) async {
final Directory outputDir = alternateAndroidDir?.childDirectory(name) ?? componentDir;
if (clearAlternateOutputDir && alternateAndroidDir != null) {
ErrorHandlingFileSystem.deleteIfExists(outputDir);
}
final List<File> inputs = <File>[];
inputs.add(androidManifestFile);
inputs.add(buildGradleFile);
final Map<String, List<File>> results = <String, List<File>>{'inputs': inputs};
results['outputs'] = await _setupComponentFiles(outputDir);
return results;
}
// generates default build.gradle and AndroidManifest.xml for the deferred component.
Future<List<File>> _setupComponentFiles(Directory outputDir) async {
Template template;
if (_templatesDir != null) {
final Directory templateComponentDir = _templatesDir.childDirectory('module${env.fileSystem.path.separator}android${env.fileSystem.path.separator}deferred_component');
template = Template(templateComponentDir, templateComponentDir, _templatesDir,
fileSystem: env.fileSystem,
templateManifest: null,
logger: env.logger,
templateRenderer: globals.templateRenderer,
);
} else {
template = await Template.fromName('module${env.fileSystem.path.separator}android${env.fileSystem.path.separator}deferred_component',
fileSystem: env.fileSystem,
templateManifest: null,
logger: env.logger,
templateRenderer: globals.templateRenderer,
);
}
final Map<String, dynamic> context = <String, dynamic>{
'androidIdentifier': FlutterProject.current().manifest.androidPackage ?? 'com.example.${FlutterProject.current().manifest.appName}',
'componentName': name,
};
template.render(outputDir, context);
final List<File> generatedFiles = <File>[];
final File tempBuildGradle = outputDir.childFile('build.gradle');
if (!buildGradleFile.existsSync()) {
generatedFiles.add(tempBuildGradle);
} else {
ErrorHandlingFileSystem.deleteIfExists(tempBuildGradle);
}
final File tempAndroidManifest = outputDir
.childDirectory('src')
.childDirectory('main')
.childFile('AndroidManifest.xml');
if (!androidManifestFile.existsSync()) {
generatedFiles.add(tempAndroidManifest);
} else {
ErrorHandlingFileSystem.deleteIfExists(tempAndroidManifest);
}
return generatedFiles;
}
}
...@@ -45,6 +45,10 @@ const String kExtraFrontEndOptions = 'ExtraFrontEndOptions'; ...@@ -45,6 +45,10 @@ const String kExtraFrontEndOptions = 'ExtraFrontEndOptions';
/// This is expected to be a comma separated list of strings. /// This is expected to be a comma separated list of strings.
const String kExtraGenSnapshotOptions = 'ExtraGenSnapshotOptions'; const String kExtraGenSnapshotOptions = 'ExtraGenSnapshotOptions';
/// Whether the app should run gen_snapshot as a split aot build for deferred
/// components.
const String kSplitAot = 'SplitAot';
/// Whether to strip source code information out of release builds and where to save it. /// Whether to strip source code information out of release builds and where to save it.
const String kSplitDebugInfo = 'SplitDebugInfo'; const String kSplitDebugInfo = 'SplitDebugInfo';
......
// 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_setup_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',
);
return result;
}
setUp(() {
fileSystem = MemoryFileSystem.test();
logger = BufferLogger.test();
env = createEnvironment();
});
testWithoutContext('No checks passes', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText, 'test check passed.\n');
});
testWithoutContext('clearTempDir passes', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
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(
env,
exitOnFail: false,
title: 'test check',
);
validator.writeGolden(
<LoadingUnit>[
LoadingUnit(id: 2, libraries: <String>['lib1']),
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText, 'test check passed.\n');
final File expectedFile = env.projectDir.childFile('deferred_components_golden.yaml');
expect(expectedFile.existsSync(), true);
const String expectedContents =
'''
loading-units:
- id: 2
libraries:
- lib1
- id: 3
libraries:
- lib2
- lib3
''';
expect(expectedFile.readAsStringSync().contains(expectedContents), true);
});
testWithoutContext('loadingUnitGolden identical passes', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units:
- id: 2
libraries:
- lib1
- id: 3
libraries:
- lib2
- lib3
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 2, libraries: <String>['lib1']),
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
]
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText, 'test check passed.\n');
});
testWithoutContext('loadingUnitGolden finds new loading units', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units:
- id: 3
libraries:
- lib2
- lib3
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 2, libraries: <String>['lib1']),
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
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 {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units:
- id: 2
libraries:
- lib1
- id: 3
libraries:
- lib2
- lib3
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
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 {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 2, libraries: <String>['lib1']),
],
);
validator.displayResults();
validator.attemptToolExit();
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 {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units-spelled-wrong:
- id: 2
libraries:
- lib1
- id: 3
libraries:
- lib2
- lib3
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
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('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 {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units: hello
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
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);
});
testWithoutContext('loadingUnitGolden validator detects malformed file: not a list', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units:
- 2
- 3
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
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);
});
testWithoutContext('loadingUnitGolden validator detects malformed file: missing id', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units:
- id: 2
libraries:
- lib1
- libraries:
- lib2
- lib3
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
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);
});
testWithoutContext('loadingUnitGolden validator detects malformed file: libraries is list', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units:
- id: 2
libraries:
- lib1
- id: 3
libraries: hello
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText.contains('Errors checking the following files:'), true);
expect(logger.statusText.contains('Invalid golden yaml file, \'libraries\' is not a list.'), true);
});
testWithoutContext('loadingUnitGolden validator detects malformed file: libraries is list of strings', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units:
- id: 2
libraries:
- lib1
- id: 3
libraries:
- blah: hello
blah2: hello2
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
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);
});
testWithoutContext('loadingUnitGolden validator detects malformed file: empty libraries allowed', () async {
final DeferredComponentsSetupValidator validator = DeferredComponentsSetupValidator(
env,
exitOnFail: false,
title: 'test check',
);
final File goldenFile = env.projectDir.childFile(DeferredComponentsSetupValidator.kDeferredComponentsGoldenFileName);
if (goldenFile.existsSync()) {
goldenFile.deleteSync();
}
goldenFile.createSync(recursive: true);
goldenFile.writeAsStringSync('''
loading-units:
- id: 2
libraries:
- lib1
- id: 3
libraries:
''', flush: true, mode: FileMode.append);
validator.checkAgainstLoadingUnitGolden(
<LoadingUnit>[
LoadingUnit(id: 3, libraries: <String>['lib2', 'lib3']),
],
);
validator.displayResults();
validator.attemptToolExit();
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 {
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();
}
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');
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('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 adds mapping when no existing mapping', () 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();
}
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');
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" />
</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('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('<!-- Don\'t delete the meta-data below.'), true);
});
// Tests if all of the regexp whitespace detection is working.
testWithoutContext('androidStringMapping handles whitespace within entry', () 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();
}
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');
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('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(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"')), false);
expect(manifestOutput.readAsStringSync().contains('<!-- Don\'t delete the meta-data below.'), true);
});
}
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