From e425eea6c7a9776ec382c8343a113b9fdebd28dc Mon Sep 17 00:00:00 2001 From: Gary Qian Date: Tue, 27 Dec 2022 13:45:05 -0500 Subject: [PATCH] [flutter_migrate] Start command and executables (#2735) * [flutter_migrate] Start command * Sync with upstream changes * Integrate with compute and add E2E tests * E2E tests passing * Resolve hangs, add help messages * Use CommandRunner * Cleanup hooks to prevent hang * Formatting * Softer requirements on test matching to ignore order * Null safety, address minor comments * Address comments * Cleanup and logging improvements * format, normalize * Kick tests * Fix tests * Improve win test robustness * Windows perms copyall * Fix directory typo bug * takeown command in CI * Add logging for windows * windows command debug logging * Skip running on unsupported flutter versions * Add main.dart * Bots memory, dynamic flutter executable * Different temp dir * Takeown of main.dart * test adjustments * Analyzer * Increase resources to dart_unit_tests * Remove logging --- .cirrus.yml | 28 +- packages/flutter_migrate/README.md | 6 +- .../flutter_migrate/bin/flutter_migrate.dart | 9 + packages/flutter_migrate/lib/executable.dart | 95 ++++ .../flutter_migrate/lib/src/base/command.dart | 15 + .../lib/src/base_dependencies.dart | 87 ++++ .../lib/src/commands/abandon.dart | 10 +- .../lib/src/commands/apply.dart | 11 +- .../lib/src/commands/start.dart | 345 ++++++++++++++ .../lib/src/commands/status.dart | 11 +- packages/flutter_migrate/lib/src/compute.dart | 23 +- packages/flutter_migrate/lib/src/utils.dart | 28 +- .../flutter_migrate/test/migrate_test.dart | 440 ++++++++++++++++++ .../test/test_data/migrate_project.dart | 39 +- packages/flutter_migrate/test/utils_test.dart | 13 +- 15 files changed, 1110 insertions(+), 50 deletions(-) create mode 100644 packages/flutter_migrate/bin/flutter_migrate.dart create mode 100644 packages/flutter_migrate/lib/executable.dart create mode 100644 packages/flutter_migrate/lib/src/base_dependencies.dart create mode 100644 packages/flutter_migrate/lib/src/commands/start.dart create mode 100644 packages/flutter_migrate/test/migrate_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index 85726fb182..7210d19ec5 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -128,19 +128,6 @@ task: - else - echo "Only run in presubmit" - fi - - name: dart_unit_tests - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - unit_test_script: - - ./script/tool_runner.sh test --exclude=script/configs/dart_unit_tests_exceptions.yaml - pathified_unit_test_script: - # Run tests with path-based dependencies to ensure that publishing - # the changes won't break tests of other packages in the repository - # that depend on it. - - ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates - - $PLUGIN_TOOL_COMMAND test --run-on-dirty-packages --exclude=script/configs/dart_unit_tests_exceptions.yaml - name: analyze env: matrix: @@ -256,7 +243,7 @@ task: zone: us-central1-a namespace: default cpu: 4 - memory: 12G + memory: 16G matrix: ### Android tasks ### - name: android-platform_tests @@ -320,6 +307,19 @@ task: - cd ../.. - flutter packages get - dart testing/web_benchmarks_test.dart + - name: dart_unit_tests + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + unit_test_script: + - ./script/tool_runner.sh test --exclude=script/configs/dart_unit_tests_exceptions.yaml + pathified_unit_test_script: + # Run tests with path-based dependencies to ensure that publishing + # the changes won't break tests of other packages in the repository + # that depend on it. + - ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates + - $PLUGIN_TOOL_COMMAND test --run-on-dirty-packages --exclude=script/configs/dart_unit_tests_exceptions.yaml # ARM macOS tasks. task: diff --git a/packages/flutter_migrate/README.md b/packages/flutter_migrate/README.md index 3a52d2bcaa..2d33273d94 100644 --- a/packages/flutter_migrate/README.md +++ b/packages/flutter_migrate/README.md @@ -24,7 +24,11 @@ any migrations that are broken. ## Usage -The core command sequence to use is `start`, `apply`. +To run the tool enter the root directory of your flutter project and run: + + `dart run /bin/flutter_migrate.dart [parameters]` + +The core subcommand sequence to use is `start`, `apply`. * `start` will generate a migration that will be staged in the `migration_staging_directory` in your project home. This command may take some time to complete depending on network speed. diff --git a/packages/flutter_migrate/bin/flutter_migrate.dart b/packages/flutter_migrate/bin/flutter_migrate.dart new file mode 100644 index 0000000000..5e49696c40 --- /dev/null +++ b/packages/flutter_migrate/bin/flutter_migrate.dart @@ -0,0 +1,9 @@ +// 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/executable.dart' as executable; + +void main(List args) { + executable.main(args); +} diff --git a/packages/flutter_migrate/lib/executable.dart b/packages/flutter_migrate/lib/executable.dart new file mode 100644 index 0000000000..a8b91e07ae --- /dev/null +++ b/packages/flutter_migrate/lib/executable.dart @@ -0,0 +1,95 @@ +// 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 'dart:async'; +import 'dart:io' as io; + +import 'package:args/args.dart'; +import 'package:args/command_runner.dart'; + +import 'src/base/command.dart'; +import 'src/base/file_system.dart'; +import 'src/base_dependencies.dart'; + +import 'src/commands/abandon.dart'; +import 'src/commands/apply.dart'; +import 'src/commands/start.dart'; +import 'src/commands/status.dart'; + +Future main(List args) async { + final bool veryVerbose = args.contains('-vv'); + final bool verbose = + args.contains('-v') || args.contains('--verbose') || veryVerbose; + + final MigrateBaseDependencies baseDependencies = MigrateBaseDependencies(); + + final List commands = [ + MigrateStartCommand( + verbose: verbose, + logger: baseDependencies.logger, + fileSystem: baseDependencies.fileSystem, + processManager: baseDependencies.processManager, + ), + MigrateStatusCommand( + verbose: verbose, + logger: baseDependencies.logger, + fileSystem: baseDependencies.fileSystem, + processManager: baseDependencies.processManager, + ), + MigrateAbandonCommand( + logger: baseDependencies.logger, + fileSystem: baseDependencies.fileSystem, + terminal: baseDependencies.terminal, + processManager: baseDependencies.processManager), + MigrateApplyCommand( + verbose: verbose, + logger: baseDependencies.logger, + fileSystem: baseDependencies.fileSystem, + terminal: baseDependencies.terminal, + processManager: baseDependencies.processManager), + ]; + + final MigrateCommandRunner runner = MigrateCommandRunner(); + + commands.forEach(runner.addCommand); + await runner.run(args); + + await _exit(0, baseDependencies, + shutdownHooks: baseDependencies.fileSystem.shutdownHooks); + await baseDependencies.fileSystem.dispose(); +} + +/// Simple extension of a CommandRunner to provide migrate specific global flags. +class MigrateCommandRunner extends CommandRunner { + MigrateCommandRunner() + : super( + 'flutter', + 'Migrates legacy flutter projects to modern versions.', + ) { + argParser.addFlag('verbose', + abbr: 'v', + negatable: false, + help: 'Noisy logging, including all shell commands executed.'); + } + + @override + ArgParser get argParser => _argParser; + final ArgParser _argParser = ArgParser(); +} + +Future _exit(int code, MigrateBaseDependencies baseDependencies, + {required ShutdownHooks shutdownHooks}) async { + // Run shutdown hooks before flushing logs + await shutdownHooks.runShutdownHooks(baseDependencies.logger); + + final Completer completer = Completer(); + + // Give the task / timer queue one cycle through before we hard exit. + Timer.run(() { + io.exit(code); + }); + + await completer.future; + return code; +} diff --git a/packages/flutter_migrate/lib/src/base/command.dart b/packages/flutter_migrate/lib/src/base/command.dart index 18604d73a6..129cc45449 100644 --- a/packages/flutter_migrate/lib/src/base/command.dart +++ b/packages/flutter_migrate/lib/src/base/command.dart @@ -4,6 +4,9 @@ import 'package:args/command_runner.dart'; +import 'logger.dart'; +import 'project.dart'; + enum ExitStatus { success, warning, @@ -11,6 +14,9 @@ enum ExitStatus { killed, } +const String flutterNoPubspecMessage = 'Error: No pubspec.yaml file found.\n' + 'This command should be run from the root of your Flutter project.'; + class CommandResult { const CommandResult(this.exitStatus); @@ -50,6 +56,7 @@ abstract class MigrateCommand extends Command { @override Future run() async { await runCommand(); + return; } Future runCommand(); @@ -71,4 +78,12 @@ abstract class MigrateCommand extends Command { /// Gets the parsed command-line option named [name] as an `int`. int? intArg(String name) => argResults?[name] as int?; + + bool validateWorkingDirectory(FlutterProject project, Logger logger) { + if (!project.pubspecFile.existsSync()) { + logger.printError(flutterNoPubspecMessage); + return false; + } + return true; + } } diff --git a/packages/flutter_migrate/lib/src/base_dependencies.dart b/packages/flutter_migrate/lib/src/base_dependencies.dart new file mode 100644 index 0000000000..640f264de1 --- /dev/null +++ b/packages/flutter_migrate/lib/src/base_dependencies.dart @@ -0,0 +1,87 @@ +// 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 'dart:io'; + +import 'package:process/process.dart'; + +import 'base/common.dart'; +import 'base/file_system.dart'; +import 'base/io.dart'; +import 'base/logger.dart'; +import 'base/signals.dart'; +import 'base/terminal.dart'; + +/// Initializes the boilerplate dependencies needed by the migrate tool. +class MigrateBaseDependencies { + MigrateBaseDependencies() { + processManager = const LocalProcessManager(); + fileSystem = LocalFileSystem( + LocalSignals.instance, Signals.defaultExitSignals, ShutdownHooks()); + + stdio = Stdio(); + terminal = AnsiTerminal(stdio: stdio); + + final LoggerFactory loggerFactory = LoggerFactory( + outputPreferences: OutputPreferences( + wrapText: stdio.hasTerminal, + showColor: stdout.supportsAnsiEscapes, + stdio: stdio, + ), + terminal: terminal, + stdio: stdio, + ); + logger = loggerFactory.createLogger( + windows: isWindows, + ); + } + + late final ProcessManager processManager; + late final LocalFileSystem fileSystem; + late final Stdio stdio; + late final Terminal terminal; + late final Logger logger; +} + +/// An abstraction for instantiation of the correct logger type. +/// +/// Our logger class hierarchy and runtime requirements are overly complicated. +class LoggerFactory { + LoggerFactory({ + required Terminal terminal, + required Stdio stdio, + required OutputPreferences outputPreferences, + StopwatchFactory stopwatchFactory = const StopwatchFactory(), + }) : _terminal = terminal, + _stdio = stdio, + _stopwatchFactory = stopwatchFactory, + _outputPreferences = outputPreferences; + + final Terminal _terminal; + final Stdio _stdio; + final StopwatchFactory _stopwatchFactory; + final OutputPreferences _outputPreferences; + + /// Create the appropriate logger for the current platform and configuration. + Logger createLogger({ + required bool windows, + }) { + Logger logger; + if (windows) { + logger = WindowsStdoutLogger( + terminal: _terminal, + stdio: _stdio, + outputPreferences: _outputPreferences, + stopwatchFactory: _stopwatchFactory, + ); + } else { + logger = StdoutLogger( + terminal: _terminal, + stdio: _stdio, + outputPreferences: _outputPreferences, + stopwatchFactory: _stopwatchFactory); + } + return logger; + } +} diff --git a/packages/flutter_migrate/lib/src/commands/abandon.dart b/packages/flutter_migrate/lib/src/commands/abandon.dart index b2a69866d4..9f1f3bf6d0 100644 --- a/packages/flutter_migrate/lib/src/commands/abandon.dart +++ b/packages/flutter_migrate/lib/src/commands/abandon.dart @@ -19,6 +19,7 @@ class MigrateAbandonCommand extends MigrateCommand { required this.fileSystem, required this.terminal, required ProcessManager processManager, + this.standalone = false, }) : migrateUtils = MigrateUtils( logger: logger, fileSystem: fileSystem, @@ -60,6 +61,8 @@ class MigrateAbandonCommand extends MigrateCommand { final MigrateUtils migrateUtils; + final bool standalone; + @override final String name = 'abandon'; @@ -75,7 +78,12 @@ class MigrateAbandonCommand extends MigrateCommand { ? FlutterProject.current(fileSystem) : flutterProjectFactory .fromDirectory(fileSystem.directory(projectDirectory)); - final bool isSubcommand = boolArg('flutter-subcommand') ?? false; + final bool isSubcommand = boolArg('flutter-subcommand') ?? !standalone; + + if (!validateWorkingDirectory(project, logger)) { + return const CommandResult(ExitStatus.fail); + } + Directory stagingDirectory = project.directory.childDirectory(kDefaultMigrateStagingDirectoryName); final String? customStagingDirectoryPath = stringArg('staging-directory'); diff --git a/packages/flutter_migrate/lib/src/commands/apply.dart b/packages/flutter_migrate/lib/src/commands/apply.dart index a9b9781d98..8b55b54eb4 100644 --- a/packages/flutter_migrate/lib/src/commands/apply.dart +++ b/packages/flutter_migrate/lib/src/commands/apply.dart @@ -25,6 +25,7 @@ class MigrateApplyCommand extends MigrateCommand { required this.fileSystem, required this.terminal, required ProcessManager processManager, + this.standalone = false, }) : _verbose = verbose, _processManager = processManager, migrateUtils = MigrateUtils( @@ -76,6 +77,8 @@ class MigrateApplyCommand extends MigrateCommand { final MigrateUtils migrateUtils; + final bool standalone; + @override final String name = 'apply'; @@ -98,12 +101,16 @@ class MigrateApplyCommand extends MigrateCommand { final FlutterToolsEnvironment environment = await FlutterToolsEnvironment.initializeFlutterToolsEnvironment( _processManager, logger); - final bool isSubcommand = boolArg('flutter-subcommand') ?? false; + final bool isSubcommand = boolArg('flutter-subcommand') ?? !standalone; + + if (!validateWorkingDirectory(project, logger)) { + return CommandResult.fail(); + } if (!await gitRepoExists(project.directory.path, logger, migrateUtils)) { logger.printStatus('No git repo found. Please run in a project with an ' 'initialized git repo or initialize one with:'); - printCommandText('git init', logger, standalone: null); + printCommand('git init', logger); return const CommandResult(ExitStatus.fail); } diff --git a/packages/flutter_migrate/lib/src/commands/start.dart b/packages/flutter_migrate/lib/src/commands/start.dart new file mode 100644 index 0000000000..34c4b5dc64 --- /dev/null +++ b/packages/flutter_migrate/lib/src/commands/start.dart @@ -0,0 +1,345 @@ +// 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:process/process.dart'; + +import '../base/command.dart'; +import '../base/file_system.dart'; +import '../base/logger.dart'; +import '../base/project.dart'; + +import '../compute.dart'; +import '../environment.dart'; +import '../manifest.dart'; +import '../result.dart'; +import '../utils.dart'; + +class MigrateStartCommand extends MigrateCommand { + MigrateStartCommand({ + bool verbose = false, + required this.logger, + required this.fileSystem, + required this.processManager, + this.standalone = false, + }) : _verbose = verbose, + migrateUtils = MigrateUtils( + logger: logger, + fileSystem: fileSystem, + processManager: processManager, + ) { + argParser.addOption( + 'staging-directory', + help: + 'Specifies the custom migration staging directory used to stage and edit proposed changes. ' + 'This path can be absolute or relative to the flutter project root.', + valueHelp: 'path', + ); + argParser.addOption( + 'project-directory', + help: 'The root directory of the flutter project.', + valueHelp: 'path', + ); + argParser.addOption( + 'platforms', + help: + 'Restrict the tool to only migrate the listed platforms. By default all platforms generated by ' + 'flutter create will be migrated. To indicate the project root, use the `root` platform', + valueHelp: 'root,android,ios,windows...', + ); + argParser.addFlag( + 'delete-temp-directories', + help: + 'Indicates if the temporary directories created by the migrate tool will be deleted.', + ); + argParser.addOption( + 'base-app-directory', + help: + 'The directory containing the base reference app. This is used as the common ancestor in a 3 way merge. ' + 'Providing this directory will prevent the tool from generating its own. This is primarily used ' + 'in testing and CI.', + valueHelp: 'path', + hide: !verbose, + ); + argParser.addOption( + 'target-app-directory', + help: + 'The directory containing the target reference app. This is used as the target app in 3 way merge. ' + 'Providing this directory will prevent the tool from generating its own. This is primarily used ' + 'in testing and CI.', + valueHelp: 'path', + hide: !verbose, + ); + argParser.addFlag( + 'allow-fallback-base-revision', + help: + 'If a base revision cannot be determined, this flag enables using flutter 1.0.0 as a fallback base revision. ' + 'Using this fallback will typically produce worse quality migrations and possibly more conflicts.', + ); + argParser.addOption( + 'base-revision', + help: + 'Manually sets the base revision to generate the base ancestor reference app with. This may be used ' + 'if the tool is unable to determine an appropriate base revision.', + valueHelp: 'git revision hash', + ); + argParser.addOption( + 'target-revision', + help: + 'Manually sets the target revision to generate the target reference app with. Passing this indicates ' + 'that the current flutter sdk version is not the version that should be migrated to.', + valueHelp: 'git revision hash', + ); + argParser.addFlag( + 'prefer-two-way-merge', + negatable: false, + help: + 'Avoid three way merges when possible. Enabling this effectively ignores the base ancestor reference ' + 'files when a merge is required, opting for a simpler two way merge instead. In some edge cases typically ' + 'involving using a fallback or incorrect base revision, the default three way merge algorithm may produce ' + 'incorrect merges. Two way merges are more conflict prone, but less likely to produce incorrect results ' + 'silently.', + ); + argParser.addFlag( + 'flutter-subcommand', + help: + 'Enable when using the flutter tool as a subcommand. This changes the ' + 'wording of log messages to indicate the correct suggested commands to use.', + ); + } + + final bool _verbose; + + final Logger logger; + + final FileSystem fileSystem; + + final MigrateUtils migrateUtils; + + final ProcessManager processManager; + + final bool standalone; + + @override + final String name = 'start'; + + @override + final String description = + r'Begins a new migration. Computes the changes needed to migrate the project from the base revision of Flutter to the current revision of Flutter and outputs the results in a working directory. Use `$ flutter migrate apply` accept and apply the changes.'; + + @override + Future runCommand() async { + final FlutterToolsEnvironment environment = + await FlutterToolsEnvironment.initializeFlutterToolsEnvironment( + processManager, logger); + if (!_validateEnvironment(environment)) { + return const CommandResult(ExitStatus.fail); + } + final String? projectRootDirPath = stringArg('project-directory') ?? + environment.getString('FlutterProject.directory'); + final Directory projectRootDir = fileSystem.directory(projectRootDirPath); + final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory(); + final FlutterProject project = projectRootDirPath == null + ? FlutterProject.current(fileSystem) + : flutterProjectFactory + .fromDirectory(fileSystem.directory(projectRootDirPath)); + + if (!validateWorkingDirectory(project, logger)) { + return CommandResult.fail(); + } + + final bool isModule = + environment.getBool('FlutterProject.isModule') ?? false; + final bool isPlugin = + environment.getBool('FlutterProject.isPlugin') ?? false; + if (isModule || isPlugin) { + logger.printError( + 'Migrate tool only supports app projects. This project is a ${isModule ? 'module' : 'plugin'}'); + return const CommandResult(ExitStatus.fail); + } + final bool isSubcommand = boolArg('flutter-subcommand') ?? !standalone; + + if (!await gitRepoExists(project.directory.path, logger, migrateUtils)) { + return const CommandResult(ExitStatus.fail); + } + + Directory stagingDirectory = + project.directory.childDirectory(kDefaultMigrateStagingDirectoryName); + final String? customStagingDirectoryPath = stringArg('staging-directory'); + if (customStagingDirectoryPath != null) { + if (fileSystem.path.isAbsolute(customStagingDirectoryPath)) { + stagingDirectory = fileSystem.directory(customStagingDirectoryPath); + } else { + stagingDirectory = + project.directory.childDirectory(customStagingDirectoryPath); + } + } + if (stagingDirectory.existsSync()) { + logger.printStatus('Old migration already in progress.', emphasis: true); + logger.printStatus( + 'Pending migration files exist in `${stagingDirectory.path}/$kDefaultMigrateStagingDirectoryName`'); + logger.printStatus( + 'Resolve merge conflicts and accept changes with by running:'); + printCommandText('apply', logger, standalone: !isSubcommand); + logger.printStatus( + 'You may also abandon the existing migration and start a new one with:'); + printCommandText('abandon', logger, standalone: !isSubcommand); + return const CommandResult(ExitStatus.fail); + } + + if (await hasUncommittedChanges( + project.directory.path, logger, migrateUtils)) { + return const CommandResult(ExitStatus.fail); + } + + List? platforms; + if (stringArg('platforms') != null) { + platforms = []; + for (String platformString in stringArg('platforms')!.split(',')) { + platformString = platformString.trim(); + platforms.add(SupportedPlatform.values.firstWhere( + (SupportedPlatform val) => + val.toString() == 'SupportedPlatform.$platformString')); + } + } + + final MigrateCommandParameters commandParameters = MigrateCommandParameters( + verbose: _verbose, + baseAppPath: stringArg('base-app-directory'), + targetAppPath: stringArg('target-app-directory'), + baseRevision: stringArg('base-revision'), + targetRevision: stringArg('target-revision'), + deleteTempDirectories: boolArg('delete-temp-directories') ?? true, + platforms: platforms, + preferTwoWayMerge: boolArg('prefer-two-way-merge') ?? false, + allowFallbackBaseRevision: + boolArg('allow-fallback-base-revision') ?? false, + ); + + final MigrateResult? migrateResult = await computeMigration( + flutterProject: project, + commandParameters: commandParameters, + fileSystem: fileSystem, + logger: logger, + migrateUtils: migrateUtils, + environment: environment, + ); + if (migrateResult == null) { + return const CommandResult(ExitStatus.fail); + } + + await writeStagingDir(migrateResult, logger, + verbose: _verbose, projectRootDir: projectRootDir); + + _deleteTempDirectories( + paths: [], + directories: migrateResult.tempDirectories, + ); + + logger.printStatus( + 'The migrate tool has staged proposed changes in the migrate staging directory.\n'); + logger.printStatus('Guided conflict resolution wizard:'); + printCommandText('resolve-conflicts', logger, standalone: !isSubcommand); + logger.printStatus('Check the status and diffs of the migration with:'); + printCommandText('status', logger, standalone: !isSubcommand); + logger.printStatus('Abandon the proposed migration with:'); + printCommandText('abandon', logger, standalone: !isSubcommand); + logger.printStatus( + 'Accept staged changes after resolving any merge conflicts with:'); + printCommandText('apply', logger, standalone: !isSubcommand); + + return const CommandResult(ExitStatus.success); + } + + /// Deletes the files or directories at the provided paths. + void _deleteTempDirectories( + {List paths = const [], + List directories = const []}) { + for (final Directory d in directories) { + try { + d.deleteSync(recursive: true); + } on FileSystemException catch (e) { + logger.printError( + 'Unabled to delete ${d.path} due to ${e.message}, please clean up manually.'); + } + } + for (final String p in paths) { + try { + fileSystem.directory(p).deleteSync(recursive: true); + } on FileSystemException catch (e) { + logger.printError( + 'Unabled to delete $p due to ${e.message}, please clean up manually.'); + } + } + } + + bool _validateEnvironment(FlutterToolsEnvironment environment) { + if (environment.getString('FlutterProject.directory') == null) { + logger.printError( + 'No valid flutter project found. This command must be run from a flutter project directory'); + return false; + } + if (environment.getString('FlutterProject.manifest.appname') == null) { + logger.printError('No app name found in project pubspec.yaml'); + return false; + } + if (!(environment.getBool('FlutterProject.android.exists') ?? false) && + environment['FlutterProject.android.isKotlin'] == null) { + logger.printError( + 'Could not detect if android project uses kotlin or java'); + return false; + } + if (!(environment.getBool('FlutterProject.ios.exists') ?? false) && + environment['FlutterProject.ios.isSwift'] == null) { + logger.printError( + 'Could not detect if iosProject uses swift or objective-c'); + return false; + } + return true; + } + + /// Writes the files into the working directory for the developer to review and resolve any conflicts. + Future writeStagingDir(MigrateResult migrateResult, Logger logger, + {bool verbose = false, required Directory projectRootDir}) async { + final Directory stagingDir = + projectRootDir.childDirectory(kDefaultMigrateStagingDirectoryName); + if (verbose) { + logger.printStatus( + 'Writing migrate staging directory at `${stagingDir.path}`'); + } + // Write files in working dir + for (final MergeResult result in migrateResult.mergeResults) { + final File file = stagingDir.childFile(result.localPath); + file.createSync(recursive: true); + if (result is StringMergeResult) { + file.writeAsStringSync(result.mergedString, flush: true); + } else { + file.writeAsBytesSync((result as BinaryMergeResult).mergedBytes, + flush: true); + } + } + + // Write all files that are newly added in target + for (final FilePendingMigration addedFile in migrateResult.addedFiles) { + final File file = stagingDir.childFile(addedFile.localPath); + file.createSync(recursive: true); + try { + file.writeAsStringSync(addedFile.file.readAsStringSync(), flush: true); + } on FileSystemException { + file.writeAsBytesSync(addedFile.file.readAsBytesSync(), flush: true); + } + } + + // Write the MigrateManifest. + final MigrateManifest manifest = MigrateManifest( + migrateRootDir: stagingDir, + migrateResult: migrateResult, + ); + manifest.writeFile(); + + // output the manifest contents. + checkAndPrintMigrateStatus(manifest, stagingDir, logger: logger); + + logger.printBox('Staging directory created at `${stagingDir.path}`'); + } +} diff --git a/packages/flutter_migrate/lib/src/commands/status.dart b/packages/flutter_migrate/lib/src/commands/status.dart index 3dc6fca01f..9228832ab2 100644 --- a/packages/flutter_migrate/lib/src/commands/status.dart +++ b/packages/flutter_migrate/lib/src/commands/status.dart @@ -20,6 +20,7 @@ class MigrateStatusCommand extends MigrateCommand { required this.logger, required this.fileSystem, required ProcessManager processManager, + this.standalone = false, }) : _verbose = verbose, migrateUtils = MigrateUtils( logger: logger, @@ -65,6 +66,8 @@ class MigrateStatusCommand extends MigrateCommand { final MigrateUtils migrateUtils; + final bool standalone; + @override final String name = 'status'; @@ -86,7 +89,11 @@ class MigrateStatusCommand extends MigrateCommand { ? FlutterProject.current(fileSystem) : flutterProjectFactory .fromDirectory(fileSystem.directory(projectDirectory)); - final bool isSubcommand = boolArg('flutter-subcommand') ?? false; + final bool isSubcommand = boolArg('flutter-subcommand') ?? !standalone; + + if (!validateWorkingDirectory(project, logger)) { + return CommandResult.fail(); + } Directory stagingDirectory = project.directory.childDirectory(kDefaultMigrateStagingDirectoryName); @@ -166,7 +173,7 @@ class MigrateStatusCommand extends MigrateCommand { } } - logger.printBox('Working directory at `${stagingDirectory.path}`'); + logger.printBox('Staging directory at `${stagingDirectory.path}`'); checkAndPrintMigrateStatus(manifest, stagingDirectory, logger: logger); diff --git a/packages/flutter_migrate/lib/src/compute.dart b/packages/flutter_migrate/lib/src/compute.dart index d3c8d81dd4..1f37b3d7a7 100644 --- a/packages/flutter_migrate/lib/src/compute.dart +++ b/packages/flutter_migrate/lib/src/compute.dart @@ -55,11 +55,8 @@ bool _skipped(String localPath, FileSystem fileSystem, } } 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 skippedPrefixes.any((String prefix) => localPath.startsWith( + '${normalize(prefix.replaceAll(r'\', fileSystem.path.separator))}${fileSystem.path.separator}')); } return false; } @@ -114,7 +111,6 @@ Set _getSkippedPrefixes(List platforms) { for (final SupportedPlatform platform in platforms) { skippedPrefixes.remove(platformToSubdirectoryPrefix(platform)); } - skippedPrefixes.remove(null); return skippedPrefixes; } @@ -275,10 +271,6 @@ Future computeMigration({ 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 @@ -377,8 +369,10 @@ Future _generateBaseAndTargetReferenceProjects({ // 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; + Directory baseProjectDir = + context.fileSystem.systemTempDirectory.createTempSync('baseProject'); + Directory targetProjectDir = + context.fileSystem.systemTempDirectory.createTempSync('targetProject'); if (customBaseProjectDir) { baseProjectDir = context.fileSystem.directory(commandParameters.baseAppPath); @@ -402,6 +396,9 @@ Future _generateBaseAndTargetReferenceProjects({ await context.migrateUtils.gitInit(baseProjectDir.absolute.path); await context.migrateUtils.gitInit(targetProjectDir.absolute.path); + result.generatedBaseTemplateDirectory = baseProjectDir; + result.generatedTargetTemplateDirectory = targetProjectDir; + final String name = context.environment['FlutterProject.manifest.appname']! as String; final String androidLanguage = @@ -821,7 +818,7 @@ class MigrateBaseFlutterProject extends MigrateFlutterProject { final List platforms = []; for (final MigratePlatformConfig config in revisionToConfigs[revision]!) { - if (config.component == null) { + if (config.component == FlutterProjectComponent.root) { continue; } platforms.add(config.component.toString().split('.').last); diff --git a/packages/flutter_migrate/lib/src/utils.dart b/packages/flutter_migrate/lib/src/utils.dart index 45f1415783..6b46d73b55 100644 --- a/packages/flutter_migrate/lib/src/utils.dart +++ b/packages/flutter_migrate/lib/src/utils.dart @@ -220,6 +220,7 @@ class MigrateUtils { '--deleted', '--modified', '--others', + '--exclude-standard', '--exclude=${migrateStagingDir ?? kDefaultMigrateStagingDirectoryName}' ]; final ProcessResult result = @@ -332,7 +333,7 @@ Future gitRepoExists( } logger.printStatus( 'Project is not a git repo. Please initialize a git repo and try again.'); - printCommandText('git init', logger); + printCommand('git init', logger); return false; } @@ -341,27 +342,34 @@ Future hasUncommittedChanges( if (await migrateUtils.hasUncommittedChanges(projectDirectory)) { logger.printStatus( 'There are uncommitted changes in your project. Please git commit, abandon, or stash your changes before trying again.'); - logger.printStatus( - 'You may commit your changes using \'git add .\' and \'git commit -m ""', logger); return true; } return false; } -/// Prints a command to logger with appropriate formatting. -void printCommandText(String command, Logger logger, - {bool? standalone = true}) { - final String prefix = standalone == null - ? '' - : (standalone ? './bin/flutter_migrate ' : 'flutter migrate '); +void printCommand(String command, Logger logger, {bool newlineAfter = true}) { logger.printStatus( - '\n\$ $prefix$command\n', + '\n\$ $command${newlineAfter ? '\n' : ''}', color: TerminalColor.grey, indent: 4, newline: false, ); } +/// Prints a command to logger with appropriate formatting. +void printCommandText(String command, Logger logger, + {bool? standalone = true, bool newlineAfter = true}) { + final String prefix = standalone == null + ? '' + : (standalone + ? 'dart run ${Platform.pathSeparator}bin${Platform.pathSeparator}flutter_migrate.dart ' + : 'flutter migrate '); + printCommand('$prefix$command', logger, newlineAfter: newlineAfter); +} + /// Defines the classification of difference between files. enum DiffType { command, diff --git a/packages/flutter_migrate/test/migrate_test.dart b/packages/flutter_migrate/test/migrate_test.dart new file mode 100644 index 0000000000..0f4376a960 --- /dev/null +++ b/packages/flutter_migrate/test/migrate_test.dart @@ -0,0 +1,440 @@ +// 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/io.dart'; +import 'package:flutter_migrate/src/base/logger.dart'; +import 'package:flutter_migrate/src/base/signals.dart'; +import 'package:flutter_migrate/src/base/terminal.dart'; +import 'package:process/process.dart'; + +import 'src/common.dart'; +import 'src/context.dart'; +import 'test_data/migrate_project.dart'; + +// This file contains E2E test that execute the core migrate commands +// and simulates manual conflict resolution and other manipulations of +// the project files. +void main() { + late Directory tempDir; + late BufferLogger logger; + late ProcessManager processManager; + late FileSystem fileSystem; + + setUp(() async { + logger = BufferLogger.test(); + processManager = const LocalProcessManager(); + fileSystem = LocalFileSystem.test(signals: LocalSignals.instance); + tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_run_test'); + }); + + tearDown(() async { + tryToDelete(tempDir); + }); + + Future hasFlutterEnvironment() async { + final String flutterRoot = getFlutterRoot(); + final String flutterExecutable = fileSystem.path + .join(flutterRoot, 'bin', 'flutter${isWindows ? '.bat' : ''}'); + final ProcessResult result = await Process.run( + flutterExecutable, ['analyze', '--suggestions', '--machine']); + if (result.exitCode != 0) { + return false; + } + return true; + } + + // Migrates a clean untouched app generated with flutter create + testUsingContext('vanilla migrate process succeeds', () async { + // This tool does not support old versions of flutter that dont include + // `flutter analyze --suggestions --machine` command + if (!await hasFlutterEnvironment()) { + return; + } + // Flutter Stable 1.22.6 hash: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + await MigrateProject.installProject('version:1.22.6_stable', tempDir); + + ProcessResult result = await runMigrateCommand([ + 'start', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.stdout.toString(), contains('Staging directory created at')); + const String linesToMatch = ''' +Added files: + - android/app/src/main/res/values-night/styles.xml + - android/app/src/main/res/drawable-v21/launch_background.xml + - analysis_options.yaml +Modified files: + - .metadata + - ios/Runner/Info.plist + - ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata + - ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme + - ios/Flutter/AppFrameworkInfo.plist + - ios/.gitignore + - pubspec.yaml + - .gitignore + - android/app/build.gradle + - android/app/src/profile/AndroidManifest.xml + - android/app/src/main/res/values/styles.xml + - android/app/src/main/AndroidManifest.xml + - android/app/src/debug/AndroidManifest.xml + - android/gradle/wrapper/gradle-wrapper.properties + - android/.gitignore + - android/build.gradle'''; + for (final String line in linesToMatch.split('\n')) { + expect(result.stdout.toString(), contains(line)); + } + + result = await runMigrateCommand([ + 'apply', + '--verbose', + ], workingDirectory: tempDir.path); + logger.printStatus('${result.exitCode}', color: TerminalColor.blue); + logger.printStatus(result.stdout, color: TerminalColor.green); + logger.printStatus(result.stderr, color: TerminalColor.red); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('Migration complete')); + + expect(tempDir.childFile('.metadata').readAsStringSync(), + contains('migration:\n platforms:\n - platform: root\n')); + + expect( + tempDir + .childFile('android/app/src/main/res/values-night/styles.xml') + .existsSync(), + true); + expect(tempDir.childFile('analysis_options.yaml').existsSync(), true); + }, timeout: const Timeout(Duration(seconds: 500)), skip: isWindows); + + // Migrates a clean untouched app generated with flutter create + testUsingContext('vanilla migrate builds', () async { + // This tool does not support old versions of flutter that dont include + // `flutter analyze --suggestions --machine` command + if (!await hasFlutterEnvironment()) { + return; + } + // Flutter Stable 2.0.0 hash: 60bd88df915880d23877bfc1602e8ddcf4c4dd2a + await MigrateProject.installProject('version:2.0.0_stable', tempDir, + main: ''' +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: Container(), + ); + } +} +'''); + ProcessResult result = await runMigrateCommand([ + 'start', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.stdout.toString(), contains('Staging directory created at')); + + result = await runMigrateCommand([ + 'apply', + '--verbose', + ], workingDirectory: tempDir.path); + logger.printStatus('${result.exitCode}', color: TerminalColor.blue); + logger.printStatus(result.stdout, color: TerminalColor.green); + logger.printStatus(result.stderr, color: TerminalColor.red); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('Migration complete')); + + result = await processManager.run([ + 'flutter', + 'build', + 'apk', + '--debug', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('app-debug.apk')); + // Skipped due to being flaky, the build completes successfully, but sometimes + // Gradle crashes due to resources on the bot. We should fine tune this to + // make it stable. + }, timeout: const Timeout(Duration(seconds: 900)), skip: true); + + testUsingContext('migrate abandon', () async { + // Abandon in an empty dir fails. + ProcessResult result = await runMigrateCommand([ + 'abandon', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stderr.toString(), + contains('Error: No pubspec.yaml file found')); + expect( + result.stderr.toString(), + contains( + 'This command should be run from the root of your Flutter project')); + + final File manifestFile = + tempDir.childFile('migrate_staging_dir/.migrate_manifest'); + expect(manifestFile.existsSync(), false); + + // Flutter Stable 1.22.6 hash: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + await MigrateProject.installProject('version:1.22.6_stable', tempDir); + + // Initialized repo fails. + result = await runMigrateCommand([ + 'abandon', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('No migration in progress')); + + // Create migration. + manifestFile.createSync(recursive: true); + + // Directory with manifest_staging_dir succeeds. + result = await runMigrateCommand([ + 'abandon', + '--verbose', + '--force', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('Abandon complete')); + }, timeout: const Timeout(Duration(seconds: 300))); + + // Migrates a user-modified app + testUsingContext('modified migrate process succeeds', () async { + // This tool does not support old versions of flutter that dont include + // `flutter analyze --suggestions --machine` command + if (!await hasFlutterEnvironment()) { + return; + } + // Flutter Stable 1.22.6 hash: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + await MigrateProject.installProject('version:1.22.6_stable', tempDir, + vanilla: false); + + ProcessResult result = await runMigrateCommand([ + 'apply', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('No migration')); + + result = await runMigrateCommand([ + 'status', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('No migration')); + + result = await runMigrateCommand([ + 'start', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('Staging directory created at')); + const String linesToMatch = ''' +Modified files: + - .metadata + - ios/Runner/Info.plist + - ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata + - ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme + - ios/Flutter/AppFrameworkInfo.plist + - ios/.gitignore + - .gitignore + - android/app/build.gradle + - android/app/src/profile/AndroidManifest.xml + - android/app/src/main/res/values/styles.xml + - android/app/src/main/AndroidManifest.xml + - android/app/src/debug/AndroidManifest.xml + - android/gradle/wrapper/gradle-wrapper.properties + - android/.gitignore + - android/build.gradle +Merge conflicted files: + - pubspec.yaml'''; + for (final String line in linesToMatch.split('\n')) { + expect(result.stdout.toString(), contains(line)); + } + + // Call apply with conflicts remaining. Should fail. + result = await runMigrateCommand([ + 'apply', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect( + result.stdout.toString(), + contains( + 'Conflicting files found. Resolve these conflicts and try again.')); + expect(result.stdout.toString(), contains('- pubspec.yaml')); + + result = await runMigrateCommand([ + 'status', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('Modified files')); + expect(result.stdout.toString(), contains('Merge conflicted files')); + + // Manually resolve conflics. The correct contents for resolution may change over time, + // but it shouldnt matter for this test. + final File metadataFile = + tempDir.childFile('migrate_staging_dir/.metadata'); + 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: e96a72392696df66755ca246ff291dfc6ca6c4ad + channel: unknown + +project_type: app + +''', flush: true); + final File pubspecYamlFile = + tempDir.childFile('migrate_staging_dir/pubspec.yaml'); + pubspecYamlFile.writeAsStringSync(''' +name: vanilla_app_1_22_6_stable +description: This is a modified description from the default. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.17.0-79.0.dev <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - images/a_dot_burr.jpeg + - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages + +''', flush: true); + + result = await runMigrateCommand([ + 'status', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('Modified files')); + expect(result.stdout.toString(), contains('diff --git')); + expect(result.stdout.toString(), contains('@@')); + expect(result.stdout.toString(), isNot(contains('Merge conflicted files'))); + + result = await runMigrateCommand([ + 'apply', + '--verbose', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + expect(result.stdout.toString(), contains('Migration complete')); + + expect(tempDir.childFile('.metadata').readAsStringSync(), + contains('e96a72392696df66755ca246ff291dfc6ca6c4ad')); + expect(tempDir.childFile('pubspec.yaml').readAsStringSync(), + isNot(contains('">=2.6.0 <3.0.0"'))); + expect(tempDir.childFile('pubspec.yaml').readAsStringSync(), + contains('">=2.17.0-79.0.dev <3.0.0"')); + expect( + tempDir.childFile('pubspec.yaml').readAsStringSync(), + contains( + 'description: This is a modified description from the default.')); + expect(tempDir.childFile('lib/main.dart').readAsStringSync(), + contains('OtherWidget()')); + expect(tempDir.childFile('lib/other.dart').existsSync(), true); + expect(tempDir.childFile('lib/other.dart').readAsStringSync(), + contains('class OtherWidget')); + + expect( + tempDir + .childFile('android/app/src/main/res/values-night/styles.xml') + .existsSync(), + true); + expect(tempDir.childFile('analysis_options.yaml').existsSync(), true); + }, timeout: const Timeout(Duration(seconds: 500)), skip: isWindows); +} diff --git a/packages/flutter_migrate/test/test_data/migrate_project.dart b/packages/flutter_migrate/test/test_data/migrate_project.dart index bd4b50b259..90d68890bc 100644 --- a/packages/flutter_migrate/test/test_data/migrate_project.dart +++ b/packages/flutter_migrate/test/test_data/migrate_project.dart @@ -56,7 +56,7 @@ class MigrateProject extends Project { 'git', 'commit', '-m', - '"Initial commit"', + '"All changes"', ], workingDirectory: dir.path); } @@ -101,18 +101,37 @@ class MigrateProject extends Project { ], workingDirectory: dir.path); if (Platform.isWindows) { - await processManager.run([ + ProcessResult res = await processManager.run([ 'robocopy', tempDir.path, dir.path, '*', '/E', + '/V', '/mov', ]); - // Add full access permissions to Users - await processManager.run([ + // Robocopy exit code 1 means some files were copied. 0 means no files were copied. + assert(res.exitCode == 1); + res = await processManager.run([ + 'takeown', + '/f', + dir.path, + '/r', + ]); + res = await processManager.run([ + 'takeown', + '/f', + '${dir.path}\\lib\\main.dart', + '/r', + ]); + res = await processManager.run([ 'icacls', - tempDir.path, + dir.path, + ], workingDirectory: dir.path); + // Add full access permissions to Users + res = await processManager.run([ + 'icacls', + dir.path, '/q', '/c', '/t', @@ -138,8 +157,16 @@ class MigrateProject extends Project { await processManager.run([ 'chmod', + '-R', '+w', - '${dir.path}${fileSystem.path.separator}*', + dir.path, + ], workingDirectory: dir.path); + + await processManager.run([ + 'chmod', + '-R', + '+r', + dir.path, ], workingDirectory: dir.path); } diff --git a/packages/flutter_migrate/test/utils_test.dart b/packages/flutter_migrate/test/utils_test.dart index debb303b4b..35a0bf50e9 100644 --- a/packages/flutter_migrate/test/utils_test.dart +++ b/packages/flutter_migrate/test/utils_test.dart @@ -2,6 +2,7 @@ // 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/io.dart'; import 'package:flutter_migrate/src/base/logger.dart'; @@ -238,7 +239,17 @@ void main() { logger.clear(); printCommandText('fullstandalone', logger); - expect(logger.statusText, contains('./bin/flutter_migrate fullstandalone')); + if (isWindows) { + expect( + logger.statusText, + contains( + r'dart run \bin\flutter_migrate.dart fullstandalone')); + } else { + expect( + logger.statusText, + contains( + 'dart run /bin/flutter_migrate.dart fullstandalone')); + } logger.clear(); });