From 9df362a73fe75362eb3bb41fa97e0287cf9c6be5 Mon Sep 17 00:00:00 2001 From: Gary Qian Date: Wed, 24 Feb 2021 20:51:03 -0500 Subject: [PATCH] [flutter_tools] Refactor DeferredComponents validator and add build target (#76400) --- ...red_components_gen_snapshot_validator.dart | 328 ++++++ ...eferred_components_prebuild_validator.dart | 294 ++++++ .../deferred_components_setup_validator.dart | 773 -------------- .../deferred_components_validator.dart | 223 +++++ .../lib/src/build_system/build_system.dart | 23 + .../targets/deferred_components.dart | 96 ++ .../lib/src/commands/assemble.dart | 21 +- ...omponents_gen_snapshot_validator_test.dart | 628 ++++++++++++ ...ed_components_prebuild_validator_test.dart | 301 ++++++ ...erred_components_setup_validator_test.dart | 942 ------------------ .../targets/deferred_components_test.dart | 129 +++ 11 files changed, 2023 insertions(+), 1735 deletions(-) create mode 100644 packages/flutter_tools/lib/src/android/deferred_components_gen_snapshot_validator.dart create mode 100644 packages/flutter_tools/lib/src/android/deferred_components_prebuild_validator.dart delete mode 100644 packages/flutter_tools/lib/src/android/deferred_components_setup_validator.dart create mode 100644 packages/flutter_tools/lib/src/android/deferred_components_validator.dart create mode 100644 packages/flutter_tools/lib/src/build_system/targets/deferred_components.dart create mode 100644 packages/flutter_tools/test/general.shard/android/deferred_components_gen_snapshot_validator_test.dart create mode 100644 packages/flutter_tools/test/general.shard/android/deferred_components_prebuild_validator_test.dart delete mode 100644 packages/flutter_tools/test/general.shard/android/deferred_components_setup_validator_test.dart create mode 100644 packages/flutter_tools/test/general.shard/build_system/targets/deferred_components_test.dart diff --git a/packages/flutter_tools/lib/src/android/deferred_components_gen_snapshot_validator.dart b/packages/flutter_tools/lib/src/android/deferred_components_gen_snapshot_validator.dart new file mode 100644 index 0000000000..16df5637f3 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/deferred_components_gen_snapshot_validator.dart @@ -0,0 +1,328 @@ +// 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 components, List 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 mapping = {}; + 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(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 generatedLoadingUnits) { + final List cachedLoadingUnits = _parseLodingUnitsCache(env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName)); + loadingUnitComparisonResults = {}; + final Set unmatchedLoadingUnits = {}; + final List newLoadingUnits = []; + if (generatedLoadingUnits == null || cachedLoadingUnits == null) { + loadingUnitComparisonResults['new'] = newLoadingUnits; + loadingUnitComparisonResults['missing'] = unmatchedLoadingUnits; + loadingUnitComparisonResults['match'] = false; + return false; + } + unmatchedLoadingUnits.addAll(cachedLoadingUnits); + final Set addedNewIds = {}; + 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 _parseLodingUnitsCache(File cacheFile) { + final List loadingUnits = []; + 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 libraries = []; + 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 generatedLoadingUnits) { + generatedLoadingUnits ??= []; + 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 usedIds = {}; + 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); + } +} diff --git a/packages/flutter_tools/lib/src/android/deferred_components_prebuild_validator.dart b/packages/flutter_tools/lib/src/android/deferred_components_prebuild_validator.dart new file mode 100644 index 0000000000..9bac5c5830 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/deferred_components_prebuild_validator.dart @@ -0,0 +1,294 @@ +// 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 '../base/deferred_component.dart'; +import '../base/error_handling_io.dart'; +import '../base/file_system.dart'; +import '../build_system/build_system.dart'; +import '../globals.dart' as globals; +import '../project.dart'; +import '../template.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 DeferredComponentsPrebuildValidator 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. + DeferredComponentsPrebuildValidator(Environment env, { + bool exitOnFail = true, + String title, + Directory templatesDir, + }) : _templatesDir = templatesDir, + super(env, exitOnFail: exitOnFail, title: title); + + final Directory _templatesDir; + + /// 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//build.gradle` + /// and `android//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 checkAndroidDynamicFeature(List components) async { + inputs.add(env.projectDir.childFile('pubspec.yaml')); + if (components == null || components.isEmpty) { + return false; + } + 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> 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; + } + + /// 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: + /// + /// component1 + /// + /// 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 components) { + final Directory androidDir = env.projectDir.childDirectory('android'); + inputs.add(env.projectDir.childFile('pubspec.yaml')); + + // 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); + if (components == null || components.isEmpty) { + return true; + } + final Map requiredEntriesMap = {}; + 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(XmlName.fromString('name'), key), + ], + [ + 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(''' + + +'''); + for (final String key in requiredEntriesMap.keys) { + buffer.write(' ${requiredEntriesMap[key]}\n'); + } + buffer.write( +''' + + +'''); + stringResOutput.writeAsStringSync(buffer.toString(), flush: true, mode: FileMode.append); + generatedFiles.add(stringResOutput.path); + return false; + } + + /// Deletes all files inside of the validator's output directory. + void clearOutputDir() { + final Directory dir = env.projectDir.childDirectory('build').childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory); + ErrorHandlingFileSystem.deleteIfExists(dir, recursive: true); + } +} + +// 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>> generateFiles({Directory alternateAndroidDir, bool clearAlternateOutputDir = false}) async { + final Directory outputDir = alternateAndroidDir?.childDirectory(name) ?? componentDir; + if (clearAlternateOutputDir && alternateAndroidDir != null) { + ErrorHandlingFileSystem.deleteIfExists(outputDir); + } + final List inputs = []; + inputs.add(androidManifestFile); + inputs.add(buildGradleFile); + final Map> results = >{'inputs': inputs}; + results['outputs'] = await _setupComponentFiles(outputDir); + return results; + } + + // generates default build.gradle and AndroidManifest.xml for the deferred component. + Future> _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 context = { + 'androidIdentifier': FlutterProject.current().manifest.androidPackage ?? 'com.example.${FlutterProject.current().manifest.appName}', + 'componentName': name, + }; + + template.render(outputDir, context); + + final List generatedFiles = []; + + 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; + } +} diff --git a/packages/flutter_tools/lib/src/android/deferred_components_setup_validator.dart b/packages/flutter_tools/lib/src/android/deferred_components_setup_validator.dart deleted file mode 100644 index dffc85559f..0000000000 --- a/packages/flutter_tools/lib/src/android/deferred_components_setup_validator.dart +++ /dev/null @@ -1,773 +0,0 @@ -// 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 = [], - _outputs = [], - _title = title ?? 'Deferred components setup verification', - _templatesDir = templatesDir, - _generatedFiles = [], - _modifiedFiles = [], - _invalidFiles = {}, - _diffLines = []; - - /// 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 _generatedFiles; - // Existing files that were modified by this validator. - final List _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 _invalidFiles; - // Output of the diff task. - // TODO(garyq): implement the diff task. - final List _diffLines; - // Tracks the new and missing loading units. - Map _goldenComparisonResults; - - /// All files read by the validator. - List get inputs => _inputs; - final List _inputs; - - /// All files output by the validator. - List get outputs => _outputs; - final List _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//build.gradle` - /// and `android//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 checkAndroidDynamicFeature(List 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> 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 components, List 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 mapping = {}; - 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(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: - /// - /// component1 - /// - /// 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 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 requiredEntriesMap = {}; - 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(XmlName.fromString('name'), key), - ], - [ - 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(''' - - -'''); - for (final String key in requiredEntriesMap.keys) { - buffer.write(' ${requiredEntriesMap[key]}\n'); - } - buffer.write( -''' - - -'''); - 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 generatedLoadingUnits) { - final List goldenLoadingUnits = _parseGolden(env.projectDir.childFile(kDeferredComponentsGoldenFileName)); - _goldenComparisonResults = {}; - final Set unmatchedLoadingUnits = {}; - final List newLoadingUnits = []; - 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 addedNewIds = {}; - 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 _parseGolden(File goldenFile) { - final List loadingUnits = []; - _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 libraries = []; - 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 generatedLoadingUnits) { - generatedLoadingUnits ??= []; - 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 usedIds = {}; - 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).isNotEmpty) { - env.logger.printStatus('New loading units were found:', emphasis: true); - for (final LoadingUnit unit in _goldenComparisonResults['new'] as List) { - env.logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2); - } - env.logger.printStatus(''); - } - if ((_goldenComparisonResults['missing'] as Set).isNotEmpty) { - env.logger.printStatus('Previously existing loading units no longer exist:', emphasis: true); - for (final LoadingUnit unit in _goldenComparisonResults['missing'] as Set) { - 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>> generateFiles({Directory alternateAndroidDir, bool clearAlternateOutputDir = false}) async { - final Directory outputDir = alternateAndroidDir?.childDirectory(name) ?? componentDir; - if (clearAlternateOutputDir && alternateAndroidDir != null) { - ErrorHandlingFileSystem.deleteIfExists(outputDir); - } - final List inputs = []; - inputs.add(androidManifestFile); - inputs.add(buildGradleFile); - final Map> results = >{'inputs': inputs}; - results['outputs'] = await _setupComponentFiles(outputDir); - return results; - } - - // generates default build.gradle and AndroidManifest.xml for the deferred component. - Future> _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 context = { - 'androidIdentifier': FlutterProject.current().manifest.androidPackage ?? 'com.example.${FlutterProject.current().manifest.appName}', - 'componentName': name, - }; - - template.render(outputDir, context); - - final List generatedFiles = []; - - 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; - } -} diff --git a/packages/flutter_tools/lib/src/android/deferred_components_validator.dart b/packages/flutter_tools/lib/src/android/deferred_components_validator.dart new file mode 100644 index 0000000000..f0c4386693 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/deferred_components_validator.dart @@ -0,0 +1,223 @@ +// 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 = [], + outputs = [], + title = title ?? 'Deferred components setup verification', + generatedFiles = [], + modifiedFiles = [], + invalidFiles = {}, + diffLines = []; + /// 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 generatedFiles; + + /// Existing files that were modified by this validator. + final List 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 invalidFiles; + + // TODO(garyq): implement the diff task. + /// Output of the diff task. + final List diffLines; + + /// Tracks the new and missing loading units. + Map loadingUnitComparisonResults; + + /// All files read by the validator. + final List inputs; + /// All files output by the validator. + final List 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).isNotEmpty) { + env.logger.printStatus('New loading units were found:', emphasis: true); + for (final LoadingUnit unit in loadingUnitComparisonResults['new'] as List) { + env.logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2); + } + env.logger.printStatus(''); + } + if ((loadingUnitComparisonResults['missing'] as Set).isNotEmpty) { + env.logger.printStatus('Previously existing loading units no longer exist:', emphasis: true); + for (final LoadingUnit unit in loadingUnitComparisonResults['missing'] as Set) { + 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); + } + } +} diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart index 9abddf1bac..6abfb88020 100644 --- a/packages/flutter_tools/lib/src/build_system/build_system.dart +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -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 dependencies; + + @override + String get name => '_composite'; + + @override + Future build(Environment environment) async { } + + @override + List get inputs => []; + + @override + List get outputs => []; +} + /// The [Environment] defines several constants for use during the build. /// /// The environment contains configuration and file paths that are safe to diff --git a/packages/flutter_tools/lib/src/build_system/targets/deferred_components.dart b/packages/flutter_tools/lib/src/build_system/targets/deferred_components.dart new file mode 100644 index 0000000000..5eff1b2b0c --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/deferred_components.dart @@ -0,0 +1,96 @@ +// 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 abis; + + @override + String get name => _name; + final String _name; + + @override + List get inputs => const []; + + @override + List get outputs => const []; + + @override + List get depfiles => [ + 'flutter_$name.d', + ]; + + @override + List get dependencies => dependency == null ? [] : [dependency]; + + @visibleForTesting + DeferredComponentsGenSnapshotValidator validator; + + @override + Future build(Environment environment) async { + final DepfileService depfileService = DepfileService( + fileSystem: environment.fileSystem, + logger: environment.logger, + ); + validator = DeferredComponentsGenSnapshotValidator( + environment, + title: title, + exitOnFail: exitOnFail, + ); + + final List 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'), + ); + } +} diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart index 322b8606a0..5471387bee 100644 --- a/packages/flutter_tools/lib/src/commands/assemble.dart +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -227,7 +227,7 @@ class AssembleCommand extends FlutterCommand { @override Future runCommand() async { final List 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( target, createEnvironment(), @@ -308,22 +308,3 @@ void writePerformanceData(Iterable measurements, File ou } outFile.writeAsStringSync(json.encode(jsonData)); } - -class _CompositeTarget extends Target { - _CompositeTarget(this.dependencies); - - @override - final List dependencies; - - @override - String get name => '_composite'; - - @override - Future build(Environment environment) async { } - - @override - List get inputs => []; - - @override - List get outputs => []; -} diff --git a/packages/flutter_tools/test/general.shard/android/deferred_components_gen_snapshot_validator_test.dart b/packages/flutter_tools/test/general.shard/android/deferred_components_gen_snapshot_validator_test.dart new file mode 100644 index 0000000000..08605de5df --- /dev/null +++ b/packages/flutter_tools/test/general.shard/android/deferred_components_gen_snapshot_validator_test.dart @@ -0,0 +1,628 @@ +// 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_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/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'; + +void main() { + FileSystem fileSystem; + BufferLogger logger; + Environment env; + + Environment createEnvironment() { + final Map defines = { kSplitAot: 'true' }; + final Environment result = Environment( + outputDir: fileSystem.directory('/output'), + buildDir: fileSystem.directory('/build'), + projectDir: fileSystem.directory('/project'), + defines: defines, + inputs: {}, + 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 DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + validator.displayResults(); + validator.attemptToolExit(); + expect(logger.statusText, 'test check passed.\n'); + }); + + testWithoutContext('writeCache passes', () async { + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + validator.writeLoadingUnitsCache( + [ + LoadingUnit(id: 2, libraries: ['lib1']), + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText, 'test check passed.\n'); + + final File expectedFile = env.projectDir.childFile('deferred_components_loading_units.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('loadingUnitCache identical passes', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units: + - id: 2 + libraries: + - lib1 + - id: 3 + libraries: + - lib2 + - lib3 +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 2, libraries: ['lib1']), + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + ] + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText, 'test check passed.\n'); + }); + + testWithoutContext('loadingUnitCache finds new loading units', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units: + - id: 3 + libraries: + - lib2 + - lib3 +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 2, libraries: ['lib1']), + LoadingUnit(id: 3, libraries: ['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('loadingUnitCache finds missing loading units', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units: + - id: 2 + libraries: + - lib1 + - id: 3 + libraries: + - lib2 + - lib3 +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 3, libraries: ['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 cache file counts as all new loading units', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 2, libraries: ['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('loadingUnitCache validator detects malformed file: missing main entry', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units-spelled-wrong: + - id: 2 + libraries: + - lib1 + - id: 3 + libraries: + - lib2 + - lib3 +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Errors checking the following files:'), 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); + }); + + testWithoutContext('loadingUnitCache validator detects malformed file: not a list', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units: hello +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Errors checking the following files:'), true); + expect(logger.statusText.contains('Invalid loading units yaml file, \'loading-units\' is not a list.'), true); + }); + + testWithoutContext('loadingUnitCache validator detects malformed file: not a list', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units: + - 2 + - 3 +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Errors checking the following files:'), true); + expect(logger.statusText.contains('Invalid loading units yaml file, \'loading-units\' is not a list of maps.'), true); + }); + + testWithoutContext('loadingUnitCache validator detects malformed file: missing id', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units: + - id: 2 + libraries: + - lib1 + - libraries: + - lib2 + - lib3 +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Errors checking the following files:'), true); + expect(logger.statusText.contains('Invalid loading units yaml file, all loading units must have an \'id\''), true); + }); + + testWithoutContext('loadingUnitCache validator detects malformed file: libraries is list', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units: + - id: 2 + libraries: + - lib1 + - id: 3 + libraries: hello +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Errors checking the following files:'), true); + expect(logger.statusText.contains('Invalid loading units yaml file, \'libraries\' is not a list.'), true); + }); + + testWithoutContext('loadingUnitCache validator detects malformed file: libraries is list of strings', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units: + - id: 2 + libraries: + - lib1 + - id: 3 + libraries: + - blah: hello + blah2: hello2 +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Errors checking the following files:'), true); + expect(logger.statusText.contains('Invalid loading units yaml file, \'libraries\' is not a list of strings.'), true); + }); + + testWithoutContext('loadingUnitCache validator detects malformed file: empty libraries allowed', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final File cacheFile = env.projectDir.childFile(DeferredComponentsValidator.kLoadingUnitsCacheFileName); + if (cacheFile.existsSync()) { + cacheFile.deleteSync(); + } + cacheFile.createSync(recursive: true); + cacheFile.writeAsStringSync(''' +loading-units: + - id: 2 + libraries: + - lib1 + - id: 3 + libraries: +''', flush: true, mode: FileMode.append); + validator.checkAgainstLoadingUnitsCache( + [ + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Errors checking the following files:'), false); + }); + + testWithoutContext('androidStringMapping modifies strings file', () async { + final DeferredComponentsGenSnapshotValidator validator = DeferredComponentsGenSnapshotValidator( + env, + exitOnFail: false, + title: 'test check', + ); + final Directory baseModuleDir = env.projectDir.childDirectory('android').childDirectory('app'); + final File manifest = baseModuleDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml'); + if (manifest.existsSync()) { + manifest.deleteSync(); + } + manifest.createSync(recursive: true); + manifest.writeAsStringSync(''' + + + + + + + + + +''', flush: true, mode: FileMode.append); + validator.checkAppAndroidManifestComponentLoadingUnitMapping( + [ + DeferredComponent(name: 'component1', libraries: ['lib2']), + DeferredComponent(name: 'component2', libraries: ['lib1', 'lib4']), + ], + [ + LoadingUnit(id: 2, libraries: ['lib1']), + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + LoadingUnit(id: 4, libraries: ['lib4', 'lib5']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Modified android files:\n'), true); + expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true); + + final File manifestOutput = env.projectDir + .childDirectory('build') + .childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory) + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childFile('AndroidManifest.xml'); + expect(manifestOutput.existsSync(), true); + expect(manifestOutput.readAsStringSync().contains(''), true); + expect(manifestOutput.readAsStringSync().contains('android:value="invalidmapping"'), false); + expect(manifestOutput.readAsStringSync().contains(' + + + +''', flush: true, mode: FileMode.append); + validator.checkAppAndroidManifestComponentLoadingUnitMapping( + [ + DeferredComponent(name: 'component1', libraries: ['lib2']), + DeferredComponent(name: 'component2', libraries: ['lib1', 'lib4']), + ], + [ + LoadingUnit(id: 2, libraries: ['lib1']), + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + LoadingUnit(id: 4, libraries: ['lib4', 'lib5']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Modified android files:\n'), true); + expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true); + + final File manifestOutput = env.projectDir + .childDirectory('build') + .childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory) + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childFile('AndroidManifest.xml'); + expect(manifestOutput.existsSync(), true); + expect(manifestOutput.readAsStringSync().contains(''), true); + expect(manifestOutput.readAsStringSync().contains(' + + + + +''', flush: true, mode: FileMode.append); + validator.checkAppAndroidManifestComponentLoadingUnitMapping( + [ + DeferredComponent(name: 'component1', libraries: ['lib2']), + DeferredComponent(name: 'component2', libraries: ['lib1', 'lib4']), + ], + [ + LoadingUnit(id: 2, libraries: ['lib1']), + LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), + LoadingUnit(id: 4, libraries: ['lib4', 'lib5']), + ], + ); + validator.displayResults(); + validator.attemptToolExit(); + + expect(logger.statusText.contains('Modified android files:\n'), true); + expect(logger.statusText.contains('build/${DeferredComponentsValidator.kDeferredComponentsTempDirectory}/app/src/main/AndroidManifest.xml\n'), true); + + final File manifestOutput = env.projectDir + .childDirectory('build') + .childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory) + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childFile('AndroidManifest.xml'); + expect(manifestOutput.existsSync(), true); + expect(manifestOutput.readAsStringSync().contains(''), true); + expect(manifestOutput.readAsStringSync().contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"')), false); + expect(manifestOutput.readAsStringSync().contains(' + + + + +''', flush: true, mode: FileMode.append); + validator.checkAndroidResourcesStrings( + [ + DeferredComponent(name: 'component1', libraries: ['lib2']), + DeferredComponent(name: 'component2', libraries: ['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('component1'), true); + expect(stringsOutput.readAsStringSync().contains('component2'), 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(''' + + + component1 + + +''', flush: true, mode: FileMode.append); + validator.checkAndroidResourcesStrings( + [ + DeferredComponent(name: 'component1', libraries: ['lib2']), + DeferredComponent(name: 'component2', libraries: ['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('component1'), true); + expect(stringsOutput.readAsStringSync().contains('component2'), true); + }); +} diff --git a/packages/flutter_tools/test/general.shard/android/deferred_components_setup_validator_test.dart b/packages/flutter_tools/test/general.shard/android/deferred_components_setup_validator_test.dart deleted file mode 100644 index eaf39140a6..0000000000 --- a/packages/flutter_tools/test/general.shard/android/deferred_components_setup_validator_test.dart +++ /dev/null @@ -1,942 +0,0 @@ -// 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 defines = { kSplitAot: 'true' }; - final Environment result = Environment( - outputDir: fileSystem.directory('/output'), - buildDir: fileSystem.directory('/build'), - projectDir: fileSystem.directory('/project'), - defines: defines, - inputs: {}, - 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 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(id: 2, libraries: ['lib1']), - LoadingUnit(id: 3, libraries: ['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(id: 2, libraries: ['lib1']), - LoadingUnit(id: 3, libraries: ['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(id: 2, libraries: ['lib1']), - LoadingUnit(id: 3, libraries: ['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(id: 3, libraries: ['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(id: 2, libraries: ['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(id: 3, libraries: ['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(id: 3, libraries: ['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(id: 3, libraries: ['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(id: 3, libraries: ['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(id: 3, libraries: ['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(id: 3, libraries: ['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(id: 3, libraries: ['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(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(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(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(''' - - - - - - - - - -''', flush: true, mode: FileMode.append); - validator.checkAppAndroidManifestComponentLoadingUnitMapping( - [ - DeferredComponent(name: 'component1', libraries: ['lib2']), - DeferredComponent(name: 'component2', libraries: ['lib1', 'lib4']), - ], - [ - LoadingUnit(id: 2, libraries: ['lib1']), - LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), - LoadingUnit(id: 4, libraries: ['lib4', 'lib5']), - ], - ); - validator.checkAndroidResourcesStrings( - [ - DeferredComponent(name: 'component1', libraries: ['lib2']), - DeferredComponent(name: 'component2', libraries: ['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('component1'), true); - expect(stringsOutput.readAsStringSync().contains('component2'), 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(''), true); - expect(manifestOutput.readAsStringSync().contains('android:value="invalidmapping"'), false); - expect(manifestOutput.readAsStringSync().contains(' - - - - -''', flush: true, mode: FileMode.append); - validator.checkAppAndroidManifestComponentLoadingUnitMapping( - [ - DeferredComponent(name: 'component1', libraries: ['lib2']), - DeferredComponent(name: 'component2', libraries: ['lib1', 'lib4']), - ], - [ - LoadingUnit(id: 2, libraries: ['lib1']), - LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), - LoadingUnit(id: 4, libraries: ['lib4', 'lib5']), - ], - ); - validator.checkAndroidResourcesStrings( - [ - DeferredComponent(name: 'component1', libraries: ['lib2']), - DeferredComponent(name: 'component2', libraries: ['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('component1'), true); - expect(stringsOutput.readAsStringSync().contains('component2'), 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(''), true); - expect(manifestOutput.readAsStringSync().contains('android:value="invalidmapping"'), false); - expect(manifestOutput.readAsStringSync().contains(' - - - -''', flush: true, mode: FileMode.append); - validator.checkAppAndroidManifestComponentLoadingUnitMapping( - [ - DeferredComponent(name: 'component1', libraries: ['lib2']), - DeferredComponent(name: 'component2', libraries: ['lib1', 'lib4']), - ], - [ - LoadingUnit(id: 2, libraries: ['lib1']), - LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), - LoadingUnit(id: 4, libraries: ['lib4', 'lib5']), - ], - ); - validator.checkAndroidResourcesStrings( - [ - DeferredComponent(name: 'component1', libraries: ['lib2']), - DeferredComponent(name: 'component2', libraries: ['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('component1'), true); - expect(stringsOutput.readAsStringSync().contains('component2'), 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(''), true); - expect(manifestOutput.readAsStringSync().contains(' - - - - -''', flush: true, mode: FileMode.append); - validator.checkAppAndroidManifestComponentLoadingUnitMapping( - [ - DeferredComponent(name: 'component1', libraries: ['lib2']), - DeferredComponent(name: 'component2', libraries: ['lib1', 'lib4']), - ], - [ - LoadingUnit(id: 2, libraries: ['lib1']), - LoadingUnit(id: 3, libraries: ['lib2', 'lib3']), - LoadingUnit(id: 4, libraries: ['lib4', 'lib5']), - ], - ); - validator.checkAndroidResourcesStrings( - [ - DeferredComponent(name: 'component1', libraries: ['lib2']), - DeferredComponent(name: 'component2', libraries: ['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('component1'), true); - expect(stringsOutput.readAsStringSync().contains('component2'), 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(''), true); - expect(manifestOutput.readAsStringSync().contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"')), false); - expect(manifestOutput.readAsStringSync().contains('