From 57e388d9973b647524faccd7d76b5f942b6a3d40 Mon Sep 17 00:00:00 2001 From: Gary Qian Date: Tue, 6 Dec 2022 00:49:16 -0800 Subject: [PATCH] [flutter_migrate] Compute (#2734) * Compute * Licenses: * Licenses for test * Merge with depenencies * Address comments p1 * Separate out logging from logic * Refactor into smaller methods * Improve logging and verbose usages * Diferentiate merge skip vs total skip * More docs * Remove additional skip * Fix custom merge tests * Mocked environment * Formatting * TImeouts * Use separate enum to address project directories including root * tests passing * Fix analyzer * address comments, formatting * Test robustness * Fix update locks test * formatting * logging for CI test failures * Canonicalize test paths * Canonizalize both sides of tests * Address comments, fix tests * Formatting * Gradle locks test --- .../flutter_migrate/lib/src/base/project.dart | 7 +- packages/flutter_migrate/lib/src/compute.dart | 1048 +++++++++++++++++ .../flutter_migrate/lib/src/custom_merge.dart | 22 +- .../lib/src/flutter_project_metadata.dart | 78 +- .../lib/src/migrate_logger.dart | 73 ++ .../flutter_migrate/test/compute_test.dart | 869 ++++++++++++++ .../test/flutter_project_metadata_test.dart | 35 +- packages/flutter_migrate/test/src/common.dart | 4 +- .../flutter_migrate/test/src/context.dart | 6 +- .../test/update_locks_test.dart | 17 +- 10 files changed, 2098 insertions(+), 61 deletions(-) create mode 100644 packages/flutter_migrate/lib/src/compute.dart create mode 100644 packages/flutter_migrate/lib/src/migrate_logger.dart create mode 100644 packages/flutter_migrate/test/compute_test.dart diff --git a/packages/flutter_migrate/lib/src/base/project.dart b/packages/flutter_migrate/lib/src/base/project.dart index a8924d744ba8..b8158d6f5aa0 100644 --- a/packages/flutter_migrate/lib/src/base/project.dart +++ b/packages/flutter_migrate/lib/src/base/project.dart @@ -16,7 +16,6 @@ enum SupportedPlatform { web, windows, fuchsia, - root, // Special platform to represent the root project directory } class FlutterProjectFactory { @@ -61,10 +60,8 @@ class FlutterProject { File get metadataFile => directory.childFile('.metadata'); /// Returns a list of platform names that are supported by the project. - List getSupportedPlatforms({bool includeRoot = false}) { - final List platforms = includeRoot - ? [SupportedPlatform.root] - : []; + List getSupportedPlatforms() { + final List platforms = []; if (directory.childDirectory('android').existsSync()) { platforms.add(SupportedPlatform.android); } diff --git a/packages/flutter_migrate/lib/src/compute.dart b/packages/flutter_migrate/lib/src/compute.dart new file mode 100644 index 000000000000..d3c8d81dd412 --- /dev/null +++ b/packages/flutter_migrate/lib/src/compute.dart @@ -0,0 +1,1048 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:path/path.dart'; + +import 'base/common.dart'; +import 'base/file_system.dart'; +import 'base/logger.dart'; +import 'base/project.dart'; +import 'custom_merge.dart'; +import 'environment.dart'; +import 'flutter_project_metadata.dart'; +import 'migrate_logger.dart'; +import 'result.dart'; +import 'utils.dart'; + +// This defines paths of files and directories relative to the project root +// that should be ignored by the migrate tool regardless of .gitignore and +// config settings. +// Paths use `/` as a stand-in for path separator. +const List _skippedFiles = [ + 'ios/Runner.xcodeproj/project.pbxproj', // Xcode managed configs that may not merge cleanly. + 'README.md', // changes to this shouldn't be overwritten since is is user owned. +]; + +const List _skippedDirectories = [ + '.dart_tool', // The .dart_tool generated dir. + '.git', // Git metadata. + 'assets', // Common directory for user assets. + 'build', // Build artifacts. + 'lib', // Always user owned and we don't want to overwrite their apps. + 'test', // Typically user owned and flutter-side changes are not relevant. +]; + +final Iterable canonicalizedSkippedFiles = _skippedFiles.map( + (String path) => canonicalize(path), +); + +// Returns true for paths relative to the project root that should be skipped +// completely by the migrate tool. +bool _skipped(String localPath, FileSystem fileSystem, + {Set? skippedPrefixes}) { + final String canonicalizedLocalPath = canonicalize(localPath); + final Iterable canonicalizedSkippedFiles = + _skippedFiles.map((String path) => canonicalize(path)); + if (canonicalizedSkippedFiles.contains(canonicalizedLocalPath)) { + return true; + } + final Iterable canonicalizedSkippedDirectories = + _skippedDirectories.map((String path) => canonicalize(path)); + for (final String dir in canonicalizedSkippedDirectories) { + if (canonicalizedLocalPath.startsWith('$dir${fileSystem.path.separator}')) { + return true; + } + } + if (skippedPrefixes != null) { + final Iterable canonicalizedSkippedPrefixes = + _skippedFiles.map((String path) => canonicalize(path)); + return canonicalizedSkippedPrefixes.any((String prefix) => + canonicalizedLocalPath + .startsWith('${canonicalize(prefix)}${fileSystem.path.separator}')); + } + return false; +} + +// File extensions that the tool will not attempt to merge. Changes +// in files with these extensions will be accepted wholesale. +// +// The executables and binaries in this list are not meant to be +// comprehensive and need only cover the files that are generated +// in `flutter create` as only files generated by the template +// will be attempted to be merged. +const List _doNotMergeFileExtensions = [ + // Don't merge image files + '.bmp', + '.gif', + '.jpg', + '.jpeg', + '.png', + '.svg', + // Don't merge compiled artifacts and executables + '.dll', + '.exe', + '.jar', + '.so', +]; + +// These files should always go through the migrate process as +// they are either integral to the migrate process or we expect +// new versions of this file to always be desired. +const Set _alwaysMigrateFiles = { + '.metadata', // .metadata tracks key migration information. + 'android/gradle/wrapper/gradle-wrapper.jar', + // Always add .gitignore back in even if user-deleted as it makes it + // difficult to migrate in the future and the migrate tool enforces git + // usage. + '.gitignore', +}; + +/// False for files that should not be merged. Typically, images and binary files. +bool _mergable(String localPath) { + return _alwaysMigrateFiles.contains(localPath) || + !_doNotMergeFileExtensions.any((String ext) => localPath.endsWith(ext)); +} + +// Compile the set of path prefixes that should be ignored as configured +// in the command arguments. +Set _getSkippedPrefixes(List platforms) { + final Set skippedPrefixes = {}; + for (final SupportedPlatform platform in SupportedPlatform.values) { + skippedPrefixes.add(platformToSubdirectoryPrefix(platform)); + } + for (final SupportedPlatform platform in platforms) { + skippedPrefixes.remove(platformToSubdirectoryPrefix(platform)); + } + skippedPrefixes.remove(null); + return skippedPrefixes; +} + +/// Data class holds the common context that is used throughout the steps of a migrate computation. +class MigrateContext { + MigrateContext({ + required this.flutterProject, + required this.skippedPrefixes, + required this.fileSystem, + required this.migrateLogger, + required this.migrateUtils, + required this.environment, + this.baseProject, + this.targetProject, + }); + + final FlutterProject flutterProject; + final Set skippedPrefixes; + final FileSystem fileSystem; + final MigrateLogger migrateLogger; + final MigrateUtils migrateUtils; + final FlutterToolsEnvironment environment; + + MigrateBaseFlutterProject? baseProject; + MigrateTargetFlutterProject? targetProject; +} + +/// Returns the path relative to the flutter project's root. +String getLocalPath(String path, String basePath, FileSystem fileSystem) { + return path.replaceFirst(basePath + fileSystem.path.separator, ''); +} + +String platformToSubdirectoryPrefix(SupportedPlatform platform) { + switch (platform) { + case SupportedPlatform.android: + return 'android'; + case SupportedPlatform.ios: + return 'ios'; + case SupportedPlatform.linux: + return 'linux'; + case SupportedPlatform.macos: + return 'macos'; + case SupportedPlatform.web: + return 'web'; + case SupportedPlatform.windows: + return 'windows'; + case SupportedPlatform.fuchsia: + return 'fuchsia'; + } +} + +/// Data class that contains the command line arguments passed by the user. +class MigrateCommandParameters { + MigrateCommandParameters({ + this.baseAppPath, + this.targetAppPath, + this.baseRevision, + this.targetRevision, + this.preferTwoWayMerge = false, + this.verbose = false, + this.allowFallbackBaseRevision = false, + this.deleteTempDirectories = true, + this.platforms, + }); + final String? baseAppPath; + final String? targetAppPath; + final String? baseRevision; + final String? targetRevision; + final bool preferTwoWayMerge; + final bool verbose; + final bool allowFallbackBaseRevision; + final bool deleteTempDirectories; + final List? platforms; +} + +/// Computes the changes that migrates the current flutter project to the target revision. +/// +/// This is the entry point to the core migration computations and drives the migration process. +/// +/// This method attempts to find a base revision, which is the revision of the Flutter SDK +/// the app was generated with or the last revision the app was migrated to. The base revision +/// typically comes from the .metadata, but for legacy apps, the config may not exist. In +/// this case, we fallback to using the revision in .metadata, and if that does not exist, we +/// use the target revision as the base revision. In the final fallback case, the migration should +/// still work, but will likely generate slightly less accurate merges. +/// +/// Operations the computation performs: +/// +/// - Parse .metadata file +/// - Collect revisions to use for each platform +/// - Download each flutter revision and call `flutter create` for each. +/// - Call `flutter create` with target revision (target is typically current flutter version) +/// - Diff base revision generated app with target revision generated app +/// - Compute all newly added files between base and target revisions +/// - Compute merge of all files that are modified by user and flutter +/// - Track temp dirs to be deleted +/// +/// Structure: This method builds upon a MigrateResult instance +Future computeMigration({ + FlutterProject? flutterProject, + required MigrateCommandParameters commandParameters, + required FileSystem fileSystem, + required Logger logger, + required MigrateUtils migrateUtils, + required FlutterToolsEnvironment environment, +}) async { + flutterProject ??= FlutterProject.current(fileSystem); + + final MigrateLogger migrateLogger = + MigrateLogger(logger: logger, verbose: commandParameters.verbose); + migrateLogger.logStep('start'); + // Find the path prefixes to ignore. This allows subdirectories of platforms + // not part of the migration to be skipped. + final List platforms = + commandParameters.platforms ?? flutterProject.getSupportedPlatforms(); + final Set skippedPrefixes = _getSkippedPrefixes(platforms); + + final MigrateResult result = MigrateResult.empty(); + final MigrateContext context = MigrateContext( + flutterProject: flutterProject, + skippedPrefixes: skippedPrefixes, + migrateLogger: migrateLogger, + fileSystem: fileSystem, + migrateUtils: migrateUtils, + environment: environment, + ); + + migrateLogger.logStep('revisions'); + final MigrateRevisions revisionConfig = MigrateRevisions( + context: context, + baseRevision: commandParameters.baseRevision, + allowFallbackBaseRevision: commandParameters.allowFallbackBaseRevision, + platforms: platforms, + environment: environment, + ); + + // Extract the unamanged files/paths that should be ignored by the migrate tool. + // These paths are absolute paths. + migrateLogger.logStep('unmanaged'); + final List unmanagedFiles = []; + final List unmanagedDirectories = []; + final String basePath = flutterProject.directory.path; + for (final String localPath in revisionConfig.config.unmanagedFiles) { + if (localPath.endsWith(fileSystem.path.separator)) { + unmanagedDirectories.add(fileSystem.path.join(basePath, localPath)); + } else { + unmanagedFiles.add(fileSystem.path.join(basePath, localPath)); + } + } + + migrateLogger.logStep('generating_base'); + // Generate the base templates + final ReferenceProjects referenceProjects = + await _generateBaseAndTargetReferenceProjects( + context: context, + result: result, + revisionConfig: revisionConfig, + platforms: platforms, + commandParameters: commandParameters, + ); + result.generatedBaseTemplateDirectory = + referenceProjects.baseProject.directory; + result.generatedTargetTemplateDirectory = + referenceProjects.targetProject.directory; + + // Generate diffs. These diffs are used to determine if a file is newly added, needs merging, + // or deleted (rare). Only files with diffs between the base and target revisions need to be + // migrated. If files are unchanged between base and target, then there are no changes to merge. + migrateLogger.logStep('diff'); + result.diffMap.addAll(await referenceProjects.baseProject + .diff(context, referenceProjects.targetProject)); + + // Check for any new files that were added in the target reference app that did not + // exist in the base reference app. + migrateLogger.logStep('new_files'); + result.addedFiles.addAll(await referenceProjects.baseProject + .computeNewlyAddedFiles( + context, result, referenceProjects.targetProject)); + + // Merge any base->target changed files with the version in the developer's project. + // Files that the developer left unchanged are fully updated to match the target reference. + // Files that the developer changed and were changed from base->target are merged. + migrateLogger.logStep('merging'); + await MigrateFlutterProject.merge( + context, + result, + referenceProjects.baseProject, + referenceProjects.targetProject, + unmanagedFiles, + unmanagedDirectories, + commandParameters.preferTwoWayMerge, + ); + + // Clean up any temp directories generated by this tool. + migrateLogger.logStep('cleaning'); + _registerTempDirectoriesForCleaning( + commandParameters: commandParameters, + result: result, + referenceProjects: referenceProjects); + migrateLogger.stop(); + return result; +} + +/// Returns a base revision to fallback to in case a true base revision is unknown. +String _getFallbackBaseRevision( + bool allowFallbackBaseRevision, MigrateLogger migrateLogger) { + if (!allowFallbackBaseRevision) { + migrateLogger.stop(); + migrateLogger.printError( + 'Could not determine base revision this app was created with:'); + migrateLogger.printError( + '.metadata file did not exist or did not contain a valid revision.', + indent: 2); + migrateLogger.printError( + 'Run this command again with the `--allow-fallback-base-revision` flag to use Flutter v1.0.0 as the base revision or manually pass a revision with `--base-revision=`', + indent: 2); + throwToolExit('Failed to resolve base revision'); + } + // Earliest version of flutter with .metadata: c17099f474675d8066fec6984c242d8b409ae985 (2017) + // Flutter 2.0.0: 60bd88df915880d23877bfc1602e8ddcf4c4dd2a + // Flutter v1.0.0: 5391447fae6209bb21a89e6a5a6583cac1af9b4b + // + // TODO(garyq): Use things like dart sdk version and other hints to better fine-tune this fallback. + // + // We fall back on flutter v1.0.0 if .metadata doesn't exist. + migrateLogger.printIfVerbose( + 'Could not determine base revision, falling back on `v1.0.0`, revision 5391447fae6209bb21a89e6a5a6583cac1af9b4b'); + return '5391447fae6209bb21a89e6a5a6583cac1af9b4b'; +} + +/// Simple data class that holds the base and target reference +/// projects. +class ReferenceProjects { + ReferenceProjects({ + required this.baseProject, + required this.targetProject, + required this.customBaseProjectDir, + required this.customTargetProjectDir, + }); + + MigrateBaseFlutterProject baseProject; + MigrateTargetFlutterProject targetProject; + + // Whether a user provided base and target projects were provided. + bool customBaseProjectDir; + bool customTargetProjectDir; +} + +// Generate reference base and target flutter projects. +// +// This function generates reference vaniilla projects by using `flutter create` with +// the base revision Flutter SDK as well as the target revision SDK. +Future _generateBaseAndTargetReferenceProjects({ + required MigrateContext context, + required MigrateResult result, + required MigrateRevisions revisionConfig, + required List platforms, + required MigrateCommandParameters commandParameters, +}) async { + // Use user-provided projects if provided, if not, generate them internally. + final bool customBaseProjectDir = commandParameters.baseAppPath != null; + final bool customTargetProjectDir = commandParameters.targetAppPath != null; + Directory? baseProjectDir; + Directory? targetProjectDir; + if (customBaseProjectDir) { + baseProjectDir = + context.fileSystem.directory(commandParameters.baseAppPath); + } else { + baseProjectDir = + context.fileSystem.systemTempDirectory.createTempSync('baseProject'); + context.migrateLogger + .printIfVerbose('Created temporary directory: ${baseProjectDir.path}'); + } + if (customTargetProjectDir) { + targetProjectDir = + context.fileSystem.directory(commandParameters.targetAppPath); + } else { + targetProjectDir = + context.fileSystem.systemTempDirectory.createTempSync('targetProject'); + context.migrateLogger.printIfVerbose( + 'Created temporary directory: ${targetProjectDir.path}'); + } + + // Git init to enable running further git commands on the reference projects. + await context.migrateUtils.gitInit(baseProjectDir.absolute.path); + await context.migrateUtils.gitInit(targetProjectDir.absolute.path); + + final String name = + context.environment['FlutterProject.manifest.appname']! as String; + final String androidLanguage = + context.environment['FlutterProject.android.isKotlin']! as bool + ? 'kotlin' + : 'java'; + final String iosLanguage = + context.environment['FlutterProject.ios.isSwift']! as bool + ? 'swift' + : 'objc'; + + final Directory targetFlutterDirectory = context.fileSystem + .directory(context.environment.getString('Cache.flutterRoot')); + + // Create the base reference vanilla app. + // + // This step clones the base flutter sdk, and uses it to create a new vanilla app. + // The vanilla base app is used as part of a 3 way merge between the base app, target + // app, and the current user-owned app. + final MigrateBaseFlutterProject baseProject = MigrateBaseFlutterProject( + path: commandParameters.baseAppPath, + directory: baseProjectDir, + name: name, + androidLanguage: androidLanguage, + iosLanguage: iosLanguage, + platformWhitelist: platforms, + ); + context.baseProject = baseProject; + await baseProject.createProject( + context, + result, + revisionConfig.revisionsList, + revisionConfig.revisionToConfigs, + commandParameters.baseRevision ?? + revisionConfig.metadataRevision ?? + _getFallbackBaseRevision( + commandParameters.allowFallbackBaseRevision, context.migrateLogger), + revisionConfig.targetRevision, + targetFlutterDirectory, + ); + + // Create target reference app when not provided. + // + // This step directly calls flutter create with the target (the current installed revision) + // flutter sdk. + final MigrateTargetFlutterProject targetProject = MigrateTargetFlutterProject( + path: commandParameters.targetAppPath, + directory: targetProjectDir, + name: name, + androidLanguage: androidLanguage, + iosLanguage: iosLanguage, + platformWhitelist: platforms, + ); + context.targetProject = targetProject; + await targetProject.createProject( + context, + result, + revisionConfig.targetRevision, + targetFlutterDirectory, + ); + + return ReferenceProjects( + baseProject: baseProject, + targetProject: targetProject, + customBaseProjectDir: customBaseProjectDir, + customTargetProjectDir: customTargetProjectDir, + ); +} + +// Registers any generated temporary directories for optional deletion upon tool exit. +void _registerTempDirectoriesForCleaning({ + required MigrateCommandParameters commandParameters, + required MigrateResult result, + required ReferenceProjects referenceProjects, +}) { + if (commandParameters.deleteTempDirectories) { + // Don't delete user-provided directories + if (!referenceProjects.customBaseProjectDir) { + result.tempDirectories.add(result.generatedBaseTemplateDirectory!); + } + if (!referenceProjects.customTargetProjectDir) { + result.tempDirectories.add(result.generatedTargetTemplateDirectory!); + } + result.tempDirectories.addAll(result.sdkDirs.values); + } +} + +/// A reference flutter project. +/// +/// A MigrateFlutterProject is a project that is generated internally within the tool +/// to see what changes need to be made to the user's project. This class +/// provides methods to merge, diff, and otherwise compare multiple MigrateFlutterProject +/// instances. +abstract class MigrateFlutterProject { + MigrateFlutterProject({ + required this.path, + required this.directory, + required this.name, + required this.androidLanguage, + required this.iosLanguage, + this.platformWhitelist, + }); + + final String? path; + final Directory directory; + final String name; + final String androidLanguage; + final String iosLanguage; + final List? platformWhitelist; + + /// Run git diff over each matching pair of files in the this project and the provided target project. + Future> diff( + MigrateContext context, + MigrateFlutterProject other, + ) async { + final Map diffMap = {}; + final List thisFiles = + directory.listSync(recursive: true); + int modifiedFilesCount = 0; + for (final FileSystemEntity entity in thisFiles) { + if (entity is! File) { + continue; + } + final File thisFile = entity.absolute; + final String localPath = getLocalPath( + thisFile.path, directory.absolute.path, context.fileSystem); + if (_skipped(localPath, context.fileSystem, + skippedPrefixes: context.skippedPrefixes)) { + continue; + } + if (await context.migrateUtils + .isGitIgnored(thisFile.absolute.path, directory.absolute.path)) { + diffMap[localPath] = DiffResult(diffType: DiffType.ignored); + } + final File otherFile = other.directory.childFile(localPath); + if (otherFile.existsSync()) { + final DiffResult diff = + await context.migrateUtils.diffFiles(thisFile, otherFile); + diffMap[localPath] = diff; + if (diff.diff != '') { + context.migrateLogger.printIfVerbose( + 'Found ${diff.exitCode} changes in $localPath', + indent: 4); + modifiedFilesCount++; + } + } else { + // Current file has no new template counterpart, which is equivalent to a deletion. + // This could also indicate a renaming if there is an addition with equivalent contents. + diffMap[localPath] = DiffResult(diffType: DiffType.deletion); + } + } + context.migrateLogger.printIfVerbose( + '$modifiedFilesCount files were modified between base and target apps.'); + return diffMap; + } + + /// Find all files that exist in the target reference app but not in the base reference app. + Future> computeNewlyAddedFiles( + MigrateContext context, + MigrateResult result, + MigrateFlutterProject other, + ) async { + final List addedFiles = []; + final List otherFiles = + other.directory.listSync(recursive: true); + for (final FileSystemEntity entity in otherFiles) { + if (entity is! File) { + continue; + } + final File otherFile = entity.absolute; + final String localPath = getLocalPath( + otherFile.path, other.directory.absolute.path, context.fileSystem); + if (directory.childFile(localPath).existsSync() || + _skipped(localPath, context.fileSystem, + skippedPrefixes: context.skippedPrefixes)) { + continue; + } + if (await context.migrateUtils.isGitIgnored( + otherFile.absolute.path, other.directory.absolute.path)) { + result.diffMap[localPath] = DiffResult(diffType: DiffType.ignored); + } + result.diffMap[localPath] = DiffResult(diffType: DiffType.addition); + if (context.flutterProject.directory.childFile(localPath).existsSync()) { + // Don't store as added file if file already exists in the project. + continue; + } + addedFiles.add(FilePendingMigration(localPath, otherFile)); + } + context.migrateLogger.printIfVerbose( + '${addedFiles.length} files were newly added in the target app.'); + return addedFiles; + } + + /// Loops through each existing file and intelligently merges it with the base->target changes. + static Future merge( + MigrateContext context, + MigrateResult result, + MigrateFlutterProject baseProject, + MigrateFlutterProject targetProject, + List unmanagedFiles, + List unmanagedDirectories, + bool preferTwoWayMerge, + ) async { + final List customMerges = [ + MetadataCustomMerge(logger: context.migrateLogger.logger), + ]; + // For each existing file in the project, we attempt to 3 way merge if it is changed by the user. + final List currentFiles = + context.flutterProject.directory.listSync(recursive: true); + final String projectRootPath = + context.flutterProject.directory.absolute.path; + final Set missingAlwaysMigrateFiles = + Set.of(_alwaysMigrateFiles); + for (final FileSystemEntity entity in currentFiles) { + if (entity is! File) { + continue; + } + // check if the file is unmanaged/ignored by the migration tool. + bool ignored = false; + ignored = unmanagedFiles.contains(entity.absolute.path); + for (final String path in unmanagedDirectories) { + if (entity.absolute.path.startsWith(path)) { + ignored = true; + break; + } + } + if (ignored) { + continue; // Skip if marked as unmanaged + } + + final File currentFile = entity.absolute; + // Diff the current file against the old generated template + final String localPath = + getLocalPath(currentFile.path, projectRootPath, context.fileSystem); + missingAlwaysMigrateFiles.remove(localPath); + if (result.diffMap.containsKey(localPath) && + result.diffMap[localPath]!.diffType == DiffType.ignored || + await context.migrateUtils.isGitIgnored(currentFile.path, + context.flutterProject.directory.absolute.path) || + _skipped(localPath, context.fileSystem, + skippedPrefixes: context.skippedPrefixes) || + !_mergable(localPath)) { + continue; + } + final File baseTemplateFile = baseProject.directory.childFile(localPath); + final File targetTemplateFile = + targetProject.directory.childFile(localPath); + final DiffResult userDiff = + await context.migrateUtils.diffFiles(currentFile, baseTemplateFile); + final DiffResult targetDiff = + await context.migrateUtils.diffFiles(currentFile, targetTemplateFile); + if (targetDiff.exitCode == 0) { + // current file is already the same as the target file. + continue; + } + + final bool alwaysMigrate = _alwaysMigrateFiles.contains(localPath); + + // Current file unchanged by user, thus we consider it owned by the tool. + if (userDiff.exitCode == 0 || alwaysMigrate) { + if ((result.diffMap.containsKey(localPath) || alwaysMigrate) && + result.diffMap[localPath] != null) { + // File changed between base and target + if (result.diffMap[localPath]!.diffType == DiffType.deletion) { + // File is deleted in new template + result.deletedFiles + .add(FilePendingMigration(localPath, currentFile)); + continue; + } + if (result.diffMap[localPath]!.exitCode != 0 || alwaysMigrate) { + // Accept the target version wholesale + MergeResult mergeResult; + try { + mergeResult = StringMergeResult.explicit( + mergedString: targetTemplateFile.readAsStringSync(), + hasConflict: false, + exitCode: 0, + localPath: localPath, + ); + } on FileSystemException { + mergeResult = BinaryMergeResult.explicit( + mergedBytes: targetTemplateFile.readAsBytesSync(), + hasConflict: false, + exitCode: 0, + localPath: localPath, + ); + } + result.mergeResults.add(mergeResult); + continue; + } + } + continue; + } + + // File changed by user + if (result.diffMap.containsKey(localPath)) { + MergeResult? mergeResult; + // Default to two way merge as it does not require the base file to exist. + MergeType mergeType = + result.mergeTypeMap[localPath] ?? MergeType.twoWay; + for (final CustomMerge customMerge in customMerges) { + if (customMerge.localPath == localPath) { + mergeResult = customMerge.merge( + currentFile, baseTemplateFile, targetTemplateFile); + mergeType = MergeType.custom; + break; + } + } + if (mergeResult == null) { + late String basePath; + late String currentPath; + late String targetPath; + + // Use two way merge if diff between base and target are the same. + // This prevents the three way merge re-deleting the base->target changes. + if (preferTwoWayMerge) { + mergeType = MergeType.twoWay; + } + switch (mergeType) { + case MergeType.twoWay: + { + basePath = currentFile.path; + currentPath = currentFile.path; + targetPath = context.fileSystem.path.join( + result.generatedTargetTemplateDirectory!.path, localPath); + break; + } + case MergeType.threeWay: + { + basePath = context.fileSystem.path.join( + result.generatedBaseTemplateDirectory!.path, localPath); + currentPath = currentFile.path; + targetPath = context.fileSystem.path.join( + result.generatedTargetTemplateDirectory!.path, localPath); + break; + } + case MergeType.custom: + { + break; // handled above + } + } + if (mergeType != MergeType.custom) { + mergeResult = await context.migrateUtils.gitMergeFile( + base: basePath, + current: currentPath, + target: targetPath, + localPath: localPath, + ); + } + } + if (mergeResult != null) { + // Don't include if result is identical to the current file. + if (mergeResult is StringMergeResult) { + if (mergeResult.mergedString == currentFile.readAsStringSync()) { + context.migrateLogger + .printIfVerbose('$localPath was merged with a $mergeType.'); + continue; + } + } else { + if ((mergeResult as BinaryMergeResult).mergedBytes == + currentFile.readAsBytesSync()) { + continue; + } + } + result.mergeResults.add(mergeResult); + } + context.migrateLogger + .printStatus('$localPath was merged with a $mergeType.'); + continue; + } + } + + // Add files that are in the target, marked as always migrate, and missing in the current project. + for (final String localPath in missingAlwaysMigrateFiles) { + final File targetTemplateFile = + result.generatedTargetTemplateDirectory!.childFile(localPath); + if (targetTemplateFile.existsSync() && + !_skipped(localPath, context.fileSystem, + skippedPrefixes: context.skippedPrefixes)) { + result.addedFiles + .add(FilePendingMigration(localPath, targetTemplateFile)); + } + } + } +} + +/// The base reference project used in a migration computation. +/// +/// This project is a clean re-generation of the version the user's project +/// was 1. originally generated with, or 2. the last successful migrated to. +class MigrateBaseFlutterProject extends MigrateFlutterProject { + MigrateBaseFlutterProject({ + required super.path, + required super.directory, + required super.name, + required super.androidLanguage, + required super.iosLanguage, + super.platformWhitelist, + }); + + /// Creates the base reference app based off of the migrate config in the .metadata file. + Future createProject( + MigrateContext context, + MigrateResult result, + List revisionsList, + Map> revisionToConfigs, + String fallbackRevision, + String targetRevision, + Directory targetFlutterDirectory, + ) async { + // Create base + // Clone base flutter + if (path == null) { + final Map revisionToFlutterSdkDir = + {}; + for (final String revision in revisionsList) { + final List platforms = []; + for (final MigratePlatformConfig config + in revisionToConfigs[revision]!) { + if (config.component == null) { + continue; + } + platforms.add(config.component.toString().split('.').last); + } + + // In the case of the revision being invalid or not a hash of the master branch, + // we want to fallback in the following order: + // - parsed revision + // - fallback revision + // - target revision (currently installed flutter) + late Directory sdkDir; + final List revisionsToTry = [revision]; + if (revision != fallbackRevision) { + revisionsToTry.add(fallbackRevision); + } + bool sdkAvailable = false; + int index = 0; + do { + if (index < revisionsToTry.length) { + final String activeRevision = revisionsToTry[index++]; + if (activeRevision != revision && + revisionToFlutterSdkDir.containsKey(activeRevision)) { + sdkDir = revisionToFlutterSdkDir[activeRevision]!; + revisionToFlutterSdkDir[revision] = sdkDir; + sdkAvailable = true; + } else { + sdkDir = context.fileSystem.systemTempDirectory + .createTempSync('flutter_$activeRevision'); + result.sdkDirs[activeRevision] = sdkDir; + context.migrateLogger.printStatus('Cloning SDK $activeRevision'); + sdkAvailable = await context.migrateUtils + .cloneFlutter(activeRevision, sdkDir.absolute.path); + revisionToFlutterSdkDir[revision] = sdkDir; + } + } else { + // fallback to just using the modern target version of flutter. + sdkDir = targetFlutterDirectory; + revisionToFlutterSdkDir[revision] = sdkDir; + sdkAvailable = true; + } + } while (!sdkAvailable); + context.migrateLogger.printStatus( + 'Creating base app for $platforms with revision $revision.'); + final String newDirectoryPath = + await context.migrateUtils.createFromTemplates( + sdkDir.childDirectory('bin').absolute.path, + name: name, + androidLanguage: androidLanguage, + iosLanguage: iosLanguage, + outputDirectory: result.generatedBaseTemplateDirectory!.absolute.path, + platforms: platforms, + ); + if (newDirectoryPath != result.generatedBaseTemplateDirectory?.path) { + result.generatedBaseTemplateDirectory = + context.fileSystem.directory(newDirectoryPath); + } + // Determine merge type for each newly generated file. + final List generatedBaseFiles = + result.generatedBaseTemplateDirectory!.listSync(recursive: true); + for (final FileSystemEntity entity in generatedBaseFiles) { + if (entity is! File) { + continue; + } + final File baseTemplateFile = entity.absolute; + final String localPath = getLocalPath( + baseTemplateFile.path, + result.generatedBaseTemplateDirectory!.absolute.path, + context.fileSystem); + if (!result.mergeTypeMap.containsKey(localPath)) { + // Use two way merge when the base revision is the same as the target revision. + result.mergeTypeMap[localPath] = revision == targetRevision + ? MergeType.twoWay + : MergeType.threeWay; + } + } + if (newDirectoryPath != result.generatedBaseTemplateDirectory?.path) { + result.generatedBaseTemplateDirectory = + context.fileSystem.directory(newDirectoryPath); + break; // The create command is old and does not distinguish between platforms so it only needs to be called once. + } + } + } + } +} + +/// Represents a manifested flutter project that is the migration target. +/// +/// The files in this project are the version the migrate tool will try +/// to transform the existing files into. +class MigrateTargetFlutterProject extends MigrateFlutterProject { + MigrateTargetFlutterProject({ + required super.path, + required super.directory, + required super.name, + required super.androidLanguage, + required super.iosLanguage, + super.platformWhitelist, + }); + + /// Creates the base reference app based off of the migrate config in the .metadata file. + Future createProject( + MigrateContext context, + MigrateResult result, + String targetRevision, + Directory targetFlutterDirectory, + ) async { + if (path == null) { + // Create target + context.migrateLogger + .printStatus('Creating target app with revision $targetRevision.'); + context.migrateLogger.printIfVerbose('Creating target app.'); + await context.migrateUtils.createFromTemplates( + targetFlutterDirectory.childDirectory('bin').absolute.path, + name: name, + androidLanguage: androidLanguage, + iosLanguage: iosLanguage, + outputDirectory: result.generatedTargetTemplateDirectory!.absolute.path, + ); + } + } +} + +/// Parses the metadata of the flutter project, extracts, computes, and stores the +/// revisions that the migration should use to migrate between. +class MigrateRevisions { + MigrateRevisions({ + required MigrateContext context, + required String? baseRevision, + required bool allowFallbackBaseRevision, + required List platforms, + required FlutterToolsEnvironment environment, + }) { + _computeRevisions(context, baseRevision, allowFallbackBaseRevision, + platforms, environment); + } + + late List revisionsList; + late Map> revisionToConfigs; + late String fallbackRevision; + late String targetRevision; + late String? metadataRevision; + late MigrateConfig config; + + void _computeRevisions( + MigrateContext context, + String? baseRevision, + bool allowFallbackBaseRevision, + List platforms, + FlutterToolsEnvironment environment, + ) { + final List components = + []; + for (final SupportedPlatform platform in platforms) { + components.add(platform.toFlutterProjectComponent()); + } + components.add(FlutterProjectComponent.root); + final FlutterProjectMetadata metadata = FlutterProjectMetadata( + context.flutterProject.directory.childFile('.metadata'), + context.migrateLogger.logger); + config = metadata.migrateConfig; + + // We call populate in case MigrateConfig is empty. If it is filled, populate should not do anything. + config.populate( + projectDirectory: context.flutterProject.directory, + update: false, + logger: context.migrateLogger.logger, + ); + + metadataRevision = metadata.versionRevision; + if (environment.getString('FlutterVersion.frameworkRevision') == null) { + throwToolExit('Flutter framework revision was null'); + } + targetRevision = environment.getString('FlutterVersion.frameworkRevision')!; + String rootBaseRevision = ''; + revisionToConfigs = >{}; + final Set revisions = {}; + if (baseRevision == null) { + for (final MigratePlatformConfig platform + in config.platformConfigs.values) { + final String effectiveRevision = platform.baseRevision == null + ? metadataRevision ?? + _getFallbackBaseRevision( + allowFallbackBaseRevision, context.migrateLogger) + : platform.baseRevision!; + if (!components.contains(platform.component)) { + continue; + } + if (platform.component == FlutterProjectComponent.root) { + rootBaseRevision = effectiveRevision; + } + revisions.add(effectiveRevision); + if (revisionToConfigs[effectiveRevision] == null) { + revisionToConfigs[effectiveRevision] = []; + } + revisionToConfigs[effectiveRevision]!.add(platform); + } + } else { + rootBaseRevision = baseRevision; + revisionToConfigs[baseRevision] = []; + for (final FlutterProjectComponent component in components) { + revisionToConfigs[baseRevision]!.add(MigratePlatformConfig( + component: component, baseRevision: baseRevision)); + } + // revisionToConfigs[baseRevision]!.add( + // MigratePlatformConfig(platform: null, baseRevision: baseRevision)); + } + // Reorder such that the root revision is created first. + revisions.remove(rootBaseRevision); + revisionsList = List.from(revisions); + if (rootBaseRevision != '') { + revisionsList.insert(0, rootBaseRevision); + } + context.migrateLogger + .printIfVerbose('Potential base revisions: $revisionsList'); + fallbackRevision = _getFallbackBaseRevision(true, context.migrateLogger); + if (revisionsList.contains(fallbackRevision) && + baseRevision != fallbackRevision && + metadataRevision != fallbackRevision) { + context.migrateLogger.printStatus( + 'Using Flutter v1.0.0 ($fallbackRevision) as the base revision since a valid base revision could not be found in the .metadata file. This may result in more merge conflicts than normally expected.', + indent: 4); + } + } +} diff --git a/packages/flutter_migrate/lib/src/custom_merge.dart b/packages/flutter_migrate/lib/src/custom_merge.dart index b65565629abc..d1a3373d8341 100644 --- a/packages/flutter_migrate/lib/src/custom_merge.dart +++ b/packages/flutter_migrate/lib/src/custom_merge.dart @@ -4,7 +4,6 @@ import 'base/file_system.dart'; import 'base/logger.dart'; -import 'base/project.dart'; import 'flutter_project_metadata.dart'; import 'utils.dart'; @@ -82,23 +81,24 @@ class MetadataCustomMerge extends CustomMerge { MigrateConfig mergeMigrateConfig( MigrateConfig current, MigrateConfig target) { // Create the superset of current and target platforms with baseRevision updated to be that of target. - final Map platformConfigs = - {}; - for (final MapEntry entry + final Map + projectComponentConfigs = + {}; + for (final MapEntry entry in current.platformConfigs.entries) { if (target.platformConfigs.containsKey(entry.key)) { - platformConfigs[entry.key] = MigratePlatformConfig( - platform: entry.value.platform, + projectComponentConfigs[entry.key] = MigratePlatformConfig( + component: entry.value.component, createRevision: entry.value.createRevision, baseRevision: target.platformConfigs[entry.key]?.baseRevision); } else { - platformConfigs[entry.key] = entry.value; + projectComponentConfigs[entry.key] = entry.value; } } - for (final MapEntry entry + for (final MapEntry entry in target.platformConfigs.entries) { - if (!platformConfigs.containsKey(entry.key)) { - platformConfigs[entry.key] = entry.value; + if (!projectComponentConfigs.containsKey(entry.key)) { + projectComponentConfigs[entry.key] = entry.value; } } @@ -112,7 +112,7 @@ class MetadataCustomMerge extends CustomMerge { } } return MigrateConfig( - platformConfigs: platformConfigs, + platformConfigs: projectComponentConfigs, unmanagedFiles: unmanagedFiles, ); } diff --git a/packages/flutter_migrate/lib/src/flutter_project_metadata.dart b/packages/flutter_migrate/lib/src/flutter_project_metadata.dart index 6c9e1dfe574e..281ec8ed9b7e 100644 --- a/packages/flutter_migrate/lib/src/flutter_project_metadata.dart +++ b/packages/flutter_migrate/lib/src/flutter_project_metadata.dart @@ -9,6 +9,41 @@ import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/project.dart'; +/// Represents subdirectories of the flutter project that can be independently created. +/// +/// This includes each supported platform as well as a component that represents the +/// root directory of the project. +enum FlutterProjectComponent { + root, + android, + ios, + linux, + macos, + web, + windows, + fuchsia, +} + +extension SupportedPlatformExtension on SupportedPlatform { + FlutterProjectComponent toFlutterProjectComponent() { + final String platformName = toString().split('.').last; + return FlutterProjectComponent.values.firstWhere( + (FlutterProjectComponent e) => + e.toString() == 'FlutterProjectComponent.$platformName'); + } +} + +extension FlutterProjectComponentExtension on FlutterProjectComponent { + SupportedPlatform? toSupportedPlatform() { + final String platformName = toString().split('.').last; + if (platformName == 'root') { + return null; + } + return SupportedPlatform.values.firstWhere((SupportedPlatform e) => + e.toString() == 'SupportedPlatform.$platformName'); + } +} + enum FlutterProjectType { /// This is the default project with the user-managed host code. /// It is different than the "module" template in that it exposes and doesn't @@ -226,10 +261,10 @@ ${migrateConfig.getOutputFileString()}'''; /// used to add support for new platforms, so the base and create revision may not always be the same. class MigrateConfig { MigrateConfig( - {Map? platformConfigs, + {Map? platformConfigs, this.unmanagedFiles = kDefaultUnmanagedFiles}) - : platformConfigs = - platformConfigs ?? {}; + : platformConfigs = platformConfigs ?? + {}; /// A mapping of the files that are unmanaged by defult for each platform. static const List kDefaultUnmanagedFiles = [ @@ -238,7 +273,7 @@ class MigrateConfig { ]; /// The metadata for each platform supported by the project. - final Map platformConfigs; + final Map platformConfigs; /// A list of paths relative to this file the migrate tool should ignore. /// @@ -261,17 +296,23 @@ class MigrateConfig { required Logger logger, }) { final FlutterProject flutterProject = FlutterProject(projectDirectory); - platforms ??= flutterProject.getSupportedPlatforms(includeRoot: true); + platforms ??= flutterProject.getSupportedPlatforms(); + final List components = + []; for (final SupportedPlatform platform in platforms) { - if (platformConfigs.containsKey(platform)) { + components.add(platform.toFlutterProjectComponent()); + } + components.add(FlutterProjectComponent.root); + for (final FlutterProjectComponent component in components) { + if (platformConfigs.containsKey(component)) { if (update) { - platformConfigs[platform]!.baseRevision = currentRevision; + platformConfigs[component]!.baseRevision = currentRevision; } } else { if (create) { - platformConfigs[platform] = MigratePlatformConfig( - platform: platform, + platformConfigs[component] = MigratePlatformConfig( + component: component, createRevision: createRevision, baseRevision: currentRevision); } @@ -287,7 +328,7 @@ class MigrateConfig { } String platformsString = ''; - for (final MapEntry entry + for (final MapEntry entry in platformConfigs.entries) { platformsString += '\n - platform: ${entry.key.toString().split('.').last}\n create_revision: ${entry.value.createRevision == null ? 'null' : "${entry.value.createRevision}"}\n base_revision: ${entry.value.baseRevision == null ? 'null' : "${entry.value.baseRevision}"}'; @@ -327,12 +368,13 @@ migration: 'base_revision': String, }, logger)) { - final SupportedPlatform platformValue = SupportedPlatform.values - .firstWhere((SupportedPlatform val) => + final FlutterProjectComponent component = FlutterProjectComponent + .values + .firstWhere((FlutterProjectComponent val) => val.toString() == - 'SupportedPlatform.${platformYamlMap['platform'] as String}'); - platformConfigs[platformValue] = MigratePlatformConfig( - platform: platformValue, + 'FlutterProjectComponent.${platformYamlMap['platform'] as String}'); + platformConfigs[component] = MigratePlatformConfig( + component: component, createRevision: platformYamlMap['create_revision'] as String?, baseRevision: platformYamlMap['base_revision'] as String?, ); @@ -357,10 +399,10 @@ migration: /// Holds the revisions for a single platform for use by the flutter migrate command. class MigratePlatformConfig { MigratePlatformConfig( - {required this.platform, this.createRevision, this.baseRevision}); + {required this.component, this.createRevision, this.baseRevision}); /// The platform this config describes. - SupportedPlatform platform; + FlutterProjectComponent component; /// The Flutter SDK revision this platform was created by. /// @@ -373,7 +415,7 @@ class MigratePlatformConfig { String? baseRevision; bool equals(MigratePlatformConfig other) { - return platform == other.platform && + return component == other.component && createRevision == other.createRevision && baseRevision == other.baseRevision; } diff --git a/packages/flutter_migrate/lib/src/migrate_logger.dart b/packages/flutter_migrate/lib/src/migrate_logger.dart new file mode 100644 index 000000000000..9b520dbdf13a --- /dev/null +++ b/packages/flutter_migrate/lib/src/migrate_logger.dart @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'base/logger.dart'; +import 'base/terminal.dart'; + +const int kDefaultStatusIndent = 2; + +class MigrateLogger { + MigrateLogger({ + required this.logger, + this.verbose = false, + this.silent = false, + }) : status = logger.startSpinner(); + + final Logger logger; + // We keep a spinner going and print periodic progress messages + // to assure the developer that the command is still working due to + // the long expected runtime. + Status status; + final bool verbose; + final bool silent; + + void start() { + status = logger.startSpinner(); + } + + void stop() { + status.stop(); + } + + static final Map _stepStringsMap = { + 'start': 'Computing migration - this command may take a while to complete.', + 'revisions': 'Obtaining revisions.', + 'unmanaged': 'Parsing unmanagedFiles.', + 'generating_base': 'Generating base reference app.', + 'diff': 'Diffing base and target reference app.', + 'new_files': 'Finding newly added files', + 'merging': 'Merging changes with existing project.', + 'cleaning': 'Cleaning up temp directories.', + 'modified_count': + 'Could not determine base revision, falling back on `v1.0.0`, revision 5391447fae6209bb21a89e6a5a6583cac1af9b4b', + }; + + void printStatus(String message, {int indent = kDefaultStatusIndent}) { + if (silent) { + return; + } + status.pause(); + logger.printStatus(message, indent: indent, color: TerminalColor.grey); + status.resume(); + } + + void printError(String message, {int indent = 0}) { + status.pause(); + logger.printError(message, indent: indent); + status.resume(); + } + + void logStep(String key) { + if (!_stepStringsMap.containsKey(key)) { + return; + } + printStatus(_stepStringsMap[key]!); + } + + void printIfVerbose(String message, {int indent = kDefaultStatusIndent}) { + if (verbose) { + printStatus(message, indent: indent); + } + } +} diff --git a/packages/flutter_migrate/test/compute_test.dart b/packages/flutter_migrate/test/compute_test.dart new file mode 100644 index 000000000000..eea7200a7a98 --- /dev/null +++ b/packages/flutter_migrate/test/compute_test.dart @@ -0,0 +1,869 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_migrate/src/base/common.dart'; +import 'package:flutter_migrate/src/base/file_system.dart'; +import 'package:flutter_migrate/src/base/logger.dart'; +import 'package:flutter_migrate/src/base/project.dart'; +import 'package:flutter_migrate/src/base/signals.dart'; +import 'package:flutter_migrate/src/compute.dart'; +import 'package:flutter_migrate/src/environment.dart'; +import 'package:flutter_migrate/src/flutter_project_metadata.dart'; +import 'package:flutter_migrate/src/migrate_logger.dart'; +import 'package:flutter_migrate/src/result.dart'; +import 'package:flutter_migrate/src/utils.dart'; +import 'package:path/path.dart'; +import 'package:process/process.dart'; + +import 'environment_test.dart'; +import 'src/common.dart'; +import 'src/context.dart'; +import 'src/test_utils.dart'; +import 'test_data/migrate_project.dart'; + +void main() { + late FileSystem fileSystem; + late BufferLogger logger; + late MigrateUtils utils; + late MigrateContext context; + late MigrateResult result; + late Directory targetFlutterDirectory; + late Directory newerTargetFlutterDirectory; + late Directory currentDir; + late FlutterToolsEnvironment environment; + late ProcessManager processManager; + late FakeProcessManager envProcessManager; + late String separator; + + const String oldSdkRevision = '5391447fae6209bb21a89e6a5a6583cac1af9b4b'; + const String newSdkRevision = '85684f9300908116a78138ea4c6036c35c9a1236'; + + Future setUpFullEnv() async { + fileSystem = LocalFileSystem.test(signals: LocalSignals.instance); + currentDir = createResolvedTempDirectorySync('current_app.'); + logger = BufferLogger.test(); + processManager = const LocalProcessManager(); + utils = MigrateUtils( + logger: logger, + fileSystem: fileSystem, + processManager: processManager, + ); + await MigrateProject.installProject('version:1.22.6_stable', currentDir); + final FlutterProjectFactory flutterFactory = FlutterProjectFactory(); + final FlutterProject flutterProject = + flutterFactory.fromDirectory(currentDir); + result = MigrateResult.empty(); + final MigrateLogger migrateLogger = + MigrateLogger(logger: logger, verbose: true); + migrateLogger.start(); + separator = isWindows ? r'\\' : '/'; + envProcessManager = FakeProcessManager(''' +{ + "FlutterProject.directory": "/Users/test/flutter", + "FlutterProject.metadataFile": "/Users/test/flutter/.metadata", + "FlutterProject.android.exists": false, + "FlutterProject.ios.exists": false, + "FlutterProject.web.exists": false, + "FlutterProject.macos.exists": false, + "FlutterProject.linux.exists": false, + "FlutterProject.windows.exists": false, + "FlutterProject.fuchsia.exists": false, + "FlutterProject.android.isKotlin": false, + "FlutterProject.ios.isSwift": false, + "FlutterProject.isModule": false, + "FlutterProject.isPlugin": false, + "FlutterProject.manifest.appname": "test_app_name", + "FlutterVersion.frameworkRevision": "4e181f012c717777681862e4771af5a941774bb9", + "Platform.operatingSystem": "macos", + "Platform.isAndroid": true, + "Platform.isIOS": false, + "Platform.isWindows": ${isWindows ? 'true' : 'false'}, + "Platform.isMacOS": ${isMacOS ? 'true' : 'false'}, + "Platform.isFuchsia": false, + "Platform.pathSeparator": "$separator", + "Cache.flutterRoot": "/Users/test/flutter" +} +'''); + environment = + await FlutterToolsEnvironment.initializeFlutterToolsEnvironment( + envProcessManager, logger); + context = MigrateContext( + flutterProject: flutterProject, + skippedPrefixes: {}, + fileSystem: fileSystem, + migrateLogger: migrateLogger, + migrateUtils: utils, + environment: environment, + ); + targetFlutterDirectory = + createResolvedTempDirectorySync('targetFlutterDir.'); + newerTargetFlutterDirectory = + createResolvedTempDirectorySync('newerTargetFlutterDir.'); + await context.migrateUtils + .cloneFlutter(oldSdkRevision, targetFlutterDirectory.absolute.path); + await context.migrateUtils.cloneFlutter( + newSdkRevision, newerTargetFlutterDirectory.absolute.path); + } + + group('MigrateFlutterProject', () { + setUp(() async { + await setUpFullEnv(); + }); + + tearDown(() async { + tryToDelete(targetFlutterDirectory); + tryToDelete(newerTargetFlutterDirectory); + }); + + testUsingContext('MigrateTargetFlutterProject creates', () async { + final Directory workingDir = + createResolvedTempDirectorySync('migrate_working_dir.'); + final Directory targetDir = + createResolvedTempDirectorySync('target_dir.'); + result.generatedTargetTemplateDirectory = targetDir; + workingDir.createSync(recursive: true); + final MigrateTargetFlutterProject targetProject = + MigrateTargetFlutterProject( + path: null, + directory: targetDir, + name: 'base', + androidLanguage: 'java', + iosLanguage: 'objc', + ); + + await targetProject.createProject( + context, + result, + oldSdkRevision, //targetRevision + targetFlutterDirectory, //targetFlutterDirectory + ); + + expect(targetDir.childFile('pubspec.yaml').existsSync(), true); + expect( + targetDir + .childDirectory('android') + .childFile('build.gradle') + .existsSync(), + true); + }, timeout: const Timeout(Duration(seconds: 500))); + + testUsingContext('MigrateBaseFlutterProject creates', () async { + final Directory workingDir = + createResolvedTempDirectorySync('migrate_working_dir.'); + final Directory baseDir = createResolvedTempDirectorySync('base_dir.'); + result.generatedBaseTemplateDirectory = baseDir; + workingDir.createSync(recursive: true); + final MigrateBaseFlutterProject baseProject = MigrateBaseFlutterProject( + path: null, + directory: baseDir, + name: 'base', + androidLanguage: 'java', + iosLanguage: 'objc', + ); + + await baseProject.createProject( + context, + result, + [oldSdkRevision], //revisionsList + >{ + oldSdkRevision: [ + MigratePlatformConfig(component: FlutterProjectComponent.android), + MigratePlatformConfig(component: FlutterProjectComponent.ios) + ], + }, //revisionToConfigs + oldSdkRevision, //fallbackRevision + oldSdkRevision, //targetRevision + targetFlutterDirectory, //targetFlutterDirectory + ); + + expect(baseDir.childFile('pubspec.yaml').existsSync(), true); + expect( + baseDir + .childDirectory('android') + .childFile('build.gradle') + .existsSync(), + true); + }, timeout: const Timeout(Duration(seconds: 500))); + + testUsingContext('Migrate___FlutterProject skips when path exists', + () async { + final Directory workingDir = + createResolvedTempDirectorySync('migrate_working_dir.'); + final Directory targetDir = + createResolvedTempDirectorySync('target_dir.'); + final Directory baseDir = createResolvedTempDirectorySync('base_dir.'); + result.generatedTargetTemplateDirectory = targetDir; + result.generatedBaseTemplateDirectory = baseDir; + workingDir.createSync(recursive: true); + + final MigrateBaseFlutterProject baseProject = MigrateBaseFlutterProject( + path: 'some_existing_base_path', + directory: baseDir, + name: 'base', + androidLanguage: 'java', + iosLanguage: 'objc', + ); + final MigrateTargetFlutterProject targetProject = + MigrateTargetFlutterProject( + path: 'some_existing_target_path', + directory: targetDir, + name: 'base', + androidLanguage: 'java', + iosLanguage: 'objc', + ); + + await baseProject.createProject( + context, + result, + [oldSdkRevision], //revisionsList + >{ + oldSdkRevision: [ + MigratePlatformConfig(component: FlutterProjectComponent.android), + MigratePlatformConfig(component: FlutterProjectComponent.ios) + ], + }, //revisionToConfigs + oldSdkRevision, //fallbackRevision + oldSdkRevision, //targetRevision + targetFlutterDirectory, //targetFlutterDirectory + ); + + expect(baseDir.childFile('pubspec.yaml').existsSync(), false); + expect( + baseDir + .childDirectory('android') + .childFile('build.gradle') + .existsSync(), + false); + + await targetProject.createProject( + context, + result, + oldSdkRevision, //revisionsList + targetFlutterDirectory, //targetFlutterDirectory + ); + + expect(targetDir.childFile('pubspec.yaml').existsSync(), false); + expect( + targetDir + .childDirectory('android') + .childFile('build.gradle') + .existsSync(), + false); + }, timeout: const Timeout(Duration(seconds: 500))); + }); + + group('MigrateRevisions', () { + setUp(() async { + fileSystem = LocalFileSystem.test(signals: LocalSignals.instance); + currentDir = createResolvedTempDirectorySync('current_app.'); + logger = BufferLogger.test(); + utils = MigrateUtils( + logger: logger, + fileSystem: fileSystem, + processManager: const LocalProcessManager(), + ); + await MigrateProject.installProject('version:1.22.6_stable', currentDir); + final FlutterProjectFactory flutterFactory = FlutterProjectFactory(); + final FlutterProject flutterProject = + flutterFactory.fromDirectory(currentDir); + result = MigrateResult.empty(); + final MigrateLogger migrateLogger = + MigrateLogger(logger: logger, verbose: true); + migrateLogger.start(); + context = MigrateContext( + flutterProject: flutterProject, + skippedPrefixes: {}, + fileSystem: fileSystem, + migrateLogger: migrateLogger, + migrateUtils: utils, + environment: environment, + ); + }); + + testUsingContext('extracts revisions underpopulated metadata', () async { + final MigrateRevisions revisions = MigrateRevisions( + context: context, + baseRevision: oldSdkRevision, + allowFallbackBaseRevision: true, + platforms: [ + SupportedPlatform.android, + SupportedPlatform.ios + ], + environment: environment, + ); + + expect(revisions.revisionsList, [oldSdkRevision]); + expect(revisions.fallbackRevision, oldSdkRevision); + expect(revisions.metadataRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect(revisions.config.unmanagedFiles.isEmpty, false); + expect(revisions.config.platformConfigs.isEmpty, false); + expect(revisions.config.platformConfigs.length, 3); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.root), + true); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.android), + true); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.ios), + true); + }); + + testUsingContext('extracts revisions full metadata', () async { + final File metadataFile = + context.flutterProject.directory.childFile('.metadata'); + if (metadataFile.existsSync()) { + if (!tryToDelete(metadataFile)) { + // TODO(garyq): Inaccessible .metadata on windows. + // Skip test for now. We should reneable. + return; + } + } + metadataFile.createSync(recursive: true); + metadataFile.writeAsStringSync(''' +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + channel: unknown + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + base_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + - platform: android + create_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + base_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + - platform: ios + create_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + base_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + - platform: linux + create_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + base_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + - platform: macos + create_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + base_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + - platform: web + create_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + base_revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + - platform: windows + create_revision: 36427af29421f406ac95ff55ea31d1dc49a45b5f + base_revision: 36427af29421f406ac95ff55ea31d1dc49a45b5f + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'blah.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' +''', flush: true); + + final MigrateRevisions revisions = MigrateRevisions( + context: context, + baseRevision: oldSdkRevision, + allowFallbackBaseRevision: true, + platforms: [ + SupportedPlatform.android, + SupportedPlatform.ios + ], + environment: environment, + ); + + expect(revisions.revisionsList, [oldSdkRevision]); + expect(revisions.fallbackRevision, oldSdkRevision); + expect(revisions.metadataRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect(revisions.config.unmanagedFiles.isEmpty, false); + expect(revisions.config.unmanagedFiles.length, 3); + expect(revisions.config.unmanagedFiles.contains('lib/main.dart'), true); + expect(revisions.config.unmanagedFiles.contains('blah.dart'), true); + expect( + revisions.config.unmanagedFiles + .contains('ios/Runner.xcodeproj/project.pbxproj'), + true); + + expect(revisions.config.platformConfigs.length, 7); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.root), + true); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.android), + true); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.ios), + true); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.linux), + true); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.macos), + true); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.web), + true); + expect( + revisions.config.platformConfigs + .containsKey(FlutterProjectComponent.windows), + true); + + expect( + revisions.config.platformConfigs[FlutterProjectComponent.root]! + .createRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.android]! + .createRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.ios]! + .createRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.linux]! + .createRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.macos]! + .createRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.web]! + .createRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.windows]! + .createRevision, + '36427af29421f406ac95ff55ea31d1dc49a45b5f'); + + expect( + revisions.config.platformConfigs[FlutterProjectComponent.root]! + .baseRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.android]! + .baseRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.ios]! + .baseRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.linux]! + .baseRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.macos]! + .baseRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.web]! + .baseRevision, + '9b2d32b605630f28625709ebd9d78ab3016b2bf6'); + expect( + revisions.config.platformConfigs[FlutterProjectComponent.windows]! + .baseRevision, + '36427af29421f406ac95ff55ea31d1dc49a45b5f'); + }); + }); + + group('project operations', () { + setUp(() async { + await setUpFullEnv(); + }); + + tearDown(() async { + tryToDelete(targetFlutterDirectory); + tryToDelete(newerTargetFlutterDirectory); + }); + + testUsingContext('diff base and target', () async { + final Directory workingDir = + createResolvedTempDirectorySync('migrate_working_dir.'); + final Directory targetDir = + createResolvedTempDirectorySync('target_dir.'); + final Directory baseDir = createResolvedTempDirectorySync('base_dir.'); + result.generatedTargetTemplateDirectory = targetDir; + result.generatedBaseTemplateDirectory = baseDir; + workingDir.createSync(recursive: true); + + final MigrateBaseFlutterProject baseProject = MigrateBaseFlutterProject( + path: null, + directory: baseDir, + name: 'base', + androidLanguage: 'java', + iosLanguage: 'objc', + ); + final MigrateTargetFlutterProject targetProject = + MigrateTargetFlutterProject( + path: null, + directory: targetDir, + name: 'base', + androidLanguage: 'java', + iosLanguage: 'objc', + ); + + await baseProject.createProject( + context, + result, + [oldSdkRevision], //revisionsList + >{ + oldSdkRevision: [ + MigratePlatformConfig(component: FlutterProjectComponent.android), + MigratePlatformConfig(component: FlutterProjectComponent.ios) + ], + }, //revisionToConfigs + oldSdkRevision, //fallbackRevision + oldSdkRevision, //targetRevision + targetFlutterDirectory, //targetFlutterDirectory + ); + + expect(baseDir.childFile('pubspec.yaml').existsSync(), true); + expect( + baseDir + .childDirectory('android') + .childFile('build.gradle') + .existsSync(), + true); + + await targetProject.createProject( + context, + result, + newSdkRevision, //revisionsList + newerTargetFlutterDirectory, //targetFlutterDirectory + ); + + expect(targetDir.childFile('pubspec.yaml').existsSync(), true); + expect( + targetDir + .childDirectory('android') + .childFile('build.gradle') + .existsSync(), + true); + + final Map diffResults = + await baseProject.diff(context, targetProject); + final Map canonicalizedDiffResults = + {}; + for (final MapEntry entry in diffResults.entries) { + canonicalizedDiffResults[canonicalize(entry.key)] = entry.value; + } + result.diffMap.addAll(diffResults); + + List expectedFiles = [ + '.metadata', + 'ios/Runner.xcworkspace/contents.xcworkspacedata', + 'ios/Runner/AppDelegate.h', + 'ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png', + 'ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png', + 'ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md', + 'ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json', + 'ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png', + 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png', + 'ios/Runner/Base.lproj/LaunchScreen.storyboard', + 'ios/Runner/Base.lproj/Main.storyboard', + 'ios/Runner/main.m', + 'ios/Runner/AppDelegate.m', + 'ios/Runner/Info.plist', + 'ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata', + 'ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme', + 'ios/Flutter/Debug.xcconfig', + 'ios/Flutter/Release.xcconfig', + 'ios/Flutter/AppFrameworkInfo.plist', + 'pubspec.yaml', + '.gitignore', + 'android/base_android.iml', + 'android/app/build.gradle', + 'android/app/src/main/res/mipmap-mdpi/ic_launcher.png', + 'android/app/src/main/res/mipmap-hdpi/ic_launcher.png', + 'android/app/src/main/res/drawable/launch_background.xml', + 'android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png', + 'android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png', + 'android/app/src/main/res/values/styles.xml', + 'android/app/src/main/res/mipmap-xhdpi/ic_launcher.png', + 'android/app/src/main/AndroidManifest.xml', + 'android/app/src/main/java/com/example/base/MainActivity.java', + 'android/local.properties', + 'android/gradle/wrapper/gradle-wrapper.jar', + 'android/gradle/wrapper/gradle-wrapper.properties', + 'android/gradlew', + 'android/build.gradle', + 'android/gradle.properties', + 'android/gradlew.bat', + 'android/settings.gradle', + 'base.iml', + '.idea/runConfigurations/main_dart.xml', + '.idea/libraries/Dart_SDK.xml', + '.idea/libraries/KotlinJavaRuntime.xml', + '.idea/libraries/Flutter_for_Android.xml', + '.idea/workspace.xml', + '.idea/modules.xml', + ]; + expectedFiles = + List.from(expectedFiles.map((String e) => canonicalize(e))); + expect(diffResults.length, 62); + expect(expectedFiles.length, 62); + for (final String diffResultPath in canonicalizedDiffResults.keys) { + expect(expectedFiles.contains(diffResultPath), true); + } + // Spot check diffs on key files: + expect( + canonicalizedDiffResults[canonicalize('android/build.gradle')]!.diff, + contains(r''' +@@ -1,18 +1,20 @@ + buildscript { ++ ext.kotlin_version = '1.6.10' + repositories { + google() +- jcenter() ++ mavenCentral() + }''')); + expect( + canonicalizedDiffResults[canonicalize('android/build.gradle')]!.diff, + contains(r''' + dependencies { +- classpath 'com.android.tools.build:gradle:3.2.1' ++ classpath 'com.android.tools.build:gradle:7.1.2' ++ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } + }''')); + expect( + canonicalizedDiffResults[canonicalize('android/build.gradle')]!.diff, + contains(r''' + allprojects { + repositories { + google() +- jcenter() ++ mavenCentral() + } + }''')); + expect( + canonicalizedDiffResults[ + canonicalize('android/app/src/main/AndroidManifest.xml')]! + .diff, + contains(r''' +@@ -1,39 +1,34 @@ + +- +- +- +- +- +- + +- ++ + ++ android:name="io.flutter.embedding.android.NormalTheme" ++ android:resource="@style/NormalTheme" ++ /> + + + + + ++ ++ + + ''')); + }, timeout: const Timeout(Duration(seconds: 500))); + + testUsingContext('Merge succeeds', () async { + final Directory workingDir = + createResolvedTempDirectorySync('migrate_working_dir.'); + final Directory targetDir = + createResolvedTempDirectorySync('target_dir.'); + final Directory baseDir = createResolvedTempDirectorySync('base_dir.'); + result.generatedTargetTemplateDirectory = targetDir; + result.generatedBaseTemplateDirectory = baseDir; + workingDir.createSync(recursive: true); + + final MigrateBaseFlutterProject baseProject = MigrateBaseFlutterProject( + path: null, + directory: baseDir, + name: 'base', + androidLanguage: 'java', + iosLanguage: 'objc', + ); + final MigrateTargetFlutterProject targetProject = + MigrateTargetFlutterProject( + path: null, + directory: targetDir, + name: 'base', + androidLanguage: 'java', + iosLanguage: 'objc', + ); + + await baseProject.createProject( + context, + result, + [oldSdkRevision], //revisionsList + >{ + oldSdkRevision: [ + MigratePlatformConfig(component: FlutterProjectComponent.android), + MigratePlatformConfig(component: FlutterProjectComponent.ios) + ], + }, //revisionToConfigs + oldSdkRevision, //fallbackRevision + oldSdkRevision, //targetRevision + targetFlutterDirectory, //targetFlutterDirectory + ); + + expect(baseDir.childFile('pubspec.yaml').existsSync(), true); + expect(baseDir.childFile('.metadata').existsSync(), true); + expect( + baseDir + .childDirectory('android') + .childFile('build.gradle') + .existsSync(), + true); + + await targetProject.createProject( + context, + result, + newSdkRevision, //revisionsList + newerTargetFlutterDirectory, //targetFlutterDirectory + ); + + expect(targetDir.childFile('pubspec.yaml').existsSync(), true); + expect(targetDir.childFile('.metadata').existsSync(), true); + expect( + targetDir + .childDirectory('android') + .childFile('build.gradle') + .existsSync(), + true); + + result.diffMap.addAll(await baseProject.diff(context, targetProject)); + + await MigrateFlutterProject.merge( + context, + result, + baseProject, + targetProject, + [], // unmanagedFiles + [], // unmanagedDirectories + false, // preferTwoWayMerge + ); + + List expectedMergedPaths = [ + '.metadata', + 'ios/Runner/Info.plist', + 'ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata', + 'ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme', + 'ios/Flutter/AppFrameworkInfo.plist', + 'pubspec.yaml', + '.gitignore', + 'android/app/build.gradle', + 'android/app/src/main/res/values/styles.xml', + 'android/app/src/main/AndroidManifest.xml', + 'android/gradle/wrapper/gradle-wrapper.properties', + 'android/build.gradle', + ]; + expectedMergedPaths = List.from( + expectedMergedPaths.map((String e) => canonicalize(e))); + expect(result.mergeResults.length, 12); + expect(expectedMergedPaths.length, 12); + + for (final MergeResult mergeResult in result.mergeResults) { + expect( + expectedMergedPaths.contains(canonicalize(mergeResult.localPath)), + true); + } + + expect(result.mergeResults[0].exitCode, 0); + expect(result.mergeResults[1].exitCode, 0); + expect(result.mergeResults[2].exitCode, 0); + expect(result.mergeResults[3].exitCode, 0); + expect(result.mergeResults[4].exitCode, 0); + expect(result.mergeResults[5].exitCode, 0); + expect(result.mergeResults[6].exitCode, 0); + expect(result.mergeResults[7].exitCode, 0); + expect(result.mergeResults[8].exitCode, 0); + expect(result.mergeResults[9].exitCode, 0); + expect(result.mergeResults[10].exitCode, 0); + expect(result.mergeResults[11].exitCode, 0); + + expect(result.mergeResults[0].hasConflict, false); + expect(result.mergeResults[1].hasConflict, false); + expect(result.mergeResults[2].hasConflict, false); + expect(result.mergeResults[3].hasConflict, false); + expect(result.mergeResults[4].hasConflict, false); + expect(result.mergeResults[5].hasConflict, false); + expect(result.mergeResults[6].hasConflict, false); + expect(result.mergeResults[7].hasConflict, false); + expect(result.mergeResults[8].hasConflict, false); + expect(result.mergeResults[9].hasConflict, false); + expect(result.mergeResults[10].hasConflict, false); + expect(result.mergeResults[11].hasConflict, false); + }, timeout: const Timeout(Duration(seconds: 500))); + }); +} diff --git a/packages/flutter_migrate/test/flutter_project_metadata_test.dart b/packages/flutter_migrate/test/flutter_project_metadata_test.dart index 8460fb65cfa0..8d4e53280605 100644 --- a/packages/flutter_migrate/test/flutter_project_metadata_test.dart +++ b/packages/flutter_migrate/test/flutter_project_metadata_test.dart @@ -5,7 +5,6 @@ import 'package:file/memory.dart'; import 'package:flutter_migrate/src/base/file_system.dart'; import 'package:flutter_migrate/src/base/logger.dart'; -import 'package:flutter_migrate/src/base/project.dart'; import 'package:flutter_migrate/src/flutter_project_metadata.dart'; import 'src/common.dart'; @@ -139,12 +138,12 @@ migration: FlutterProjectMetadata(metadataFile, logger); expect(projectMetadata.projectType, isNull); expect( - projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root] - ?.createRevision, + projectMetadata.migrateConfig + .platformConfigs[FlutterProjectComponent.root]?.createRevision, 'abcdefg'); expect( - projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root] - ?.baseRevision, + projectMetadata.migrateConfig + .platformConfigs[FlutterProjectComponent.root]?.baseRevision, 'baserevision'); expect(projectMetadata.migrateConfig.unmanagedFiles[0], 'file1'); @@ -180,12 +179,12 @@ migration: FlutterProjectMetadata(metadataFile, logger); expect(projectMetadata.projectType, FlutterProjectType.app); expect( - projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root] - ?.createRevision, + projectMetadata.migrateConfig + .platformConfigs[FlutterProjectComponent.root]?.createRevision, 'abcdefg'); expect( - projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root] - ?.baseRevision, + projectMetadata.migrateConfig + .platformConfigs[FlutterProjectComponent.root]?.baseRevision, 'baserevision'); // Tool uses default unamanged files list when malformed. expect(projectMetadata.migrateConfig.unmanagedFiles[0], 'lib/main.dart'); @@ -223,24 +222,24 @@ migration: FlutterProjectMetadata(metadataFile, logger); expect(projectMetadata.projectType, FlutterProjectType.app); expect( - projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root] - ?.createRevision, + projectMetadata.migrateConfig + .platformConfigs[FlutterProjectComponent.root]?.createRevision, 'abcdefg'); expect( - projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root] - ?.baseRevision, + projectMetadata.migrateConfig + .platformConfigs[FlutterProjectComponent.root]?.baseRevision, 'baserevision'); expect( - projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.ios] - ?.createRevision, + projectMetadata.migrateConfig + .platformConfigs[FlutterProjectComponent.ios]?.createRevision, 'abcdefg'); expect( - projectMetadata - .migrateConfig.platformConfigs[SupportedPlatform.ios]?.baseRevision, + projectMetadata.migrateConfig + .platformConfigs[FlutterProjectComponent.ios]?.baseRevision, 'baserevision'); expect( projectMetadata.migrateConfig.platformConfigs - .containsKey(SupportedPlatform.android), + .containsKey(FlutterProjectComponent.android), false); expect(projectMetadata.migrateConfig.unmanagedFiles[0], 'file1'); diff --git a/packages/flutter_migrate/test/src/common.dart b/packages/flutter_migrate/test/src/common.dart index 20758e22efcd..c8da7d3174b8 100644 --- a/packages/flutter_migrate/test/src/common.dart +++ b/packages/flutter_migrate/test/src/common.dart @@ -23,13 +23,14 @@ export 'package:test_api/test_api.dart' // ignore: deprecated_member_use isInstanceOf, test; -void tryToDelete(FileSystemEntity fileEntity) { +bool tryToDelete(FileSystemEntity fileEntity) { // This should not be necessary, but it turns out that // on Windows it's common for deletions to fail due to // bogus (we think) "access denied" errors. try { if (fileEntity.existsSync()) { fileEntity.deleteSync(recursive: true); + return true; } } on FileSystemException catch (error) { // We print this so that it's visible in the logs, to get an idea of how @@ -37,6 +38,7 @@ void tryToDelete(FileSystemEntity fileEntity) { // ignore: avoid_print print('Failed to delete ${fileEntity.path}: $error'); } + return false; } /// Gets the path to the root of the Flutter repository. diff --git a/packages/flutter_migrate/test/src/context.dart b/packages/flutter_migrate/test/src/context.dart index 4f43b48127ce..961d6d3537f8 100644 --- a/packages/flutter_migrate/test/src/context.dart +++ b/packages/flutter_migrate/test/src/context.dart @@ -24,6 +24,7 @@ void testUsingContext( Map overrides = const {}, bool initializeFlutterRoot = true, String? testOn, + Timeout? timeout, bool? skip, // should default to `false`, but https://github.com/dart-lang/test/issues/545 doesn't allow this }) { @@ -87,10 +88,7 @@ void testUsingContext( }, ); }, overrides: {}); - }, testOn: testOn, skip: skip); - // We don't support "timeout"; see ../../dart_test.yaml which - // configures all tests to have a 15 minute timeout which should - // definitely be enough. + }, testOn: testOn, skip: skip, timeout: timeout); } void _printBufferedErrors(AppContext testContext) { diff --git a/packages/flutter_migrate/test/update_locks_test.dart b/packages/flutter_migrate/test/update_locks_test.dart index 70768bb505b9..5c17021982ba 100644 --- a/packages/flutter_migrate/test/update_locks_test.dart +++ b/packages/flutter_migrate/test/update_locks_test.dart @@ -105,19 +105,25 @@ flutter: }); testWithoutContext('updates gradle locks', () async { - ProcessResult result = await processManager.run([ + final ProcessResult result = await processManager.run([ 'flutter', 'create', currentDir.absolute.path, '--project-name=testproject' ]); - result = await Process.run('dir', [], - workingDirectory: currentDir.path, runInShell: true); expect(result.exitCode, 0); final File projectAppLock = currentDir.childDirectory('android').childFile('project-app.lockfile'); final File buildGradle = currentDir.childDirectory('android').childFile('build.gradle'); + final File gradleProperties = + currentDir.childDirectory('android').childFile('gradle.properties'); + gradleProperties.writeAsStringSync(''' +org.gradle.daemon=false +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +''', flush: true); final File projectAppLockBackup = currentDir .childDirectory('android') .childFile('project-app.lockfile_backup_0'); @@ -180,6 +186,9 @@ subprojects { .childFile('gradlew.bat') .existsSync(), true); + final Directory dotGradle = + currentDir.childDirectory('android').childDirectory('.gradle'); + tryToDelete(dotGradle); await updateGradleDependencyLocking( flutterProject, utils, logger, terminal, true, fileSystem, force: true); @@ -191,7 +200,7 @@ subprojects { contains('# Manual edits can break the build and are not advised.')); expect(projectAppLock.readAsStringSync(), contains('# This file is expected to be part of source control.')); - }, timeout: const Timeout(Duration(seconds: 500))); + }, timeout: const Timeout(Duration(seconds: 500)), skip: true); } class _VersionCode extends Comparable<_VersionCode> {