| Index: tools/migration/bin/migrate_batch.dart
|
| diff --git a/tools/migration/bin/migrate_batch.dart b/tools/migration/bin/migrate_batch.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..b0ae2b1f2722a5cf9940da0314a11dc39df77ed4
|
| --- /dev/null
|
| +++ b/tools/migration/bin/migrate_batch.dart
|
| @@ -0,0 +1,290 @@
|
| +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
|
| +// for details. All rights reserved. Use of this source code is governed by a
|
| +// BSD-style license that can be found in the LICENSE file.
|
| +
|
| +/// Given the beginning and ending file names in a batch, does as much automated
|
| +/// migration and possible and prints out the remaining manual steps required.
|
| +///
|
| +/// This should be safe to run, and safe to re-run on an in-progress chunk.
|
| +/// However, it has not been thoroughly tested, so run at your own risk.
|
| +
|
| +import 'dart:io';
|
| +
|
| +import 'package:path/path.dart' as p;
|
| +
|
| +import 'package:migration/src/log.dart';
|
| +
|
| +const simpleDirs = const ["corelib", "language", "lib"];
|
| +
|
| +final String sdkRoot =
|
| + p.normalize(p.join(p.dirname(p.fromUri(Platform.script)), '../../../'));
|
| +
|
| +final String testRoot = p.join(sdkRoot, "tests");
|
| +
|
| +bool dryRun = false;
|
| +
|
| +void main(List<String> arguments) {
|
| + if (arguments.contains("--dry-run")) {
|
| + dryRun = true;
|
| + arguments = arguments.where((argument) => argument != "--dry-run").toList();
|
| + }
|
| +
|
| + if (arguments.length != 2) {
|
| + stderr.writeln(
|
| + "Usage: dart migrate_batch.dart [--dry-run] <first file> <last file>");
|
| + stderr.writeln();
|
| + stderr.writeln("Example:");
|
| + stderr.writeln();
|
| + stderr.writeln(
|
| + " \$ dart migrate_batch.dart corelib/map_to_string corelib/queue");
|
| + exit(1);
|
| + }
|
| +
|
| + var first = toTwoPath(arguments[0]);
|
| + var last = toTwoPath(arguments[1]);
|
| +
|
| + var tests = scanTests();
|
| +
|
| + // Find the range of files in the chunk. We use comparisons here instead of
|
| + // equality to try to compensate for files that may only appear in one fork
|
| + // and should be part of the chunk but aren't officially listed as the begin
|
| + // or end point.
|
| + var startIndex = -1;
|
| + var endIndex = 0;
|
| + for (var i = 0; i < tests.length; i++) {
|
| + if (startIndex == -1 && tests[i].twoPath.compareTo(first) >= 0) {
|
| + startIndex = i;
|
| + }
|
| +
|
| + if (tests[i].twoPath.compareTo(last) > 0) {
|
| + endIndex = i;
|
| + break;
|
| + }
|
| + }
|
| +
|
| + print("Migrating ${bold(endIndex - startIndex)} tests from ${bold(first)} "
|
| + "to ${bold(last)}...");
|
| + print("");
|
| +
|
| + var todos = <String>[];
|
| + var migratedTests = 0;
|
| + var unmigratedTests = 0;
|
| + for (var i = startIndex; i < endIndex; i++) {
|
| + if (tests[i].migrate(todos)) {
|
| + migratedTests++;
|
| + } else {
|
| + unmigratedTests++;
|
| + }
|
| + }
|
| +
|
| + print("");
|
| +
|
| + var summary = "";
|
| +
|
| + if (migratedTests > 0) {
|
| + var s = migratedTests == 1 ? "" : "s";
|
| + summary += "Successfully migrated ${green(migratedTests)} test$s. ";
|
| + }
|
| +
|
| + if (unmigratedTests > 0) {
|
| + var s = migratedTests == 1 ? "" : "s";
|
| + summary += "Need manual work on ${red(unmigratedTests)} test$s:";
|
| + }
|
| +
|
| + print(summary);
|
| + todos.forEach(todo);
|
| +}
|
| +
|
| +String toTwoPath(String path) {
|
| + // Allow eliding "_test" and/or ".dart" to make things more command-line
|
| + // friendly.
|
| + if (!path.endsWith("_test.dart")) path += "_test.dart";
|
| + if (!path.endsWith(".dart")) path += ".dart";
|
| +
|
| + for (var dir in simpleDirs) {
|
| + if (p.isWithin(dir, path)) {
|
| + return p.join("${dir}_2", p.relative(path, from: dir));
|
| + }
|
| +
|
| + if (p.isWithin("${dir}_strong", path)) {
|
| + return p.join("${dir}_2", p.relative(path, from: dir));
|
| + }
|
| + }
|
| +
|
| + if (p.isWithin("html", path)) {
|
| + return p.join("lib_2/html", p.relative(path, from: "html"));
|
| + }
|
| +
|
| + if (p.isWithin("isolate", path)) {
|
| + return p.join("lib_2/isolate", p.relative(path, from: "isolate"));
|
| + }
|
| +
|
| + // Guess it's already a two path.
|
| + return path;
|
| +}
|
| +
|
| +/// Loads all of the unforked test files.
|
| +///
|
| +/// Creates an list of [Fork]s, ordered by their destination paths. Handles
|
| +/// tests that only appear in one fork or the other, or both.
|
| +List<Fork> scanTests() {
|
| + var tests = <String, Fork>{};
|
| +
|
| + addTestDirectory(String fromDir, String twoDir) {
|
| + for (var entry
|
| + in new Directory(p.join(testRoot, fromDir)).listSync(recursive: true)) {
|
| + if (!entry.path.endsWith("_test.dart")) continue;
|
| +
|
| + var fromPath = p.relative(entry.path, from: testRoot);
|
| + var twoPath = p.join(twoDir, p.relative(fromPath, from: fromDir));
|
| +
|
| + var fork = tests.putIfAbsent(twoPath, () => new Fork(twoPath));
|
| + if (fromDir.contains("_strong")) {
|
| + fork.strongPath = fromPath;
|
| + } else {
|
| + fork.onePath = fromPath;
|
| + }
|
| + }
|
| + }
|
| +
|
| + addTestDirectory("corelib", "corelib_2");
|
| + addTestDirectory("corelib_strong", "corelib_2");
|
| + addTestDirectory("html", "lib_2/html");
|
| + addTestDirectory("isolate", "lib_2/isolate");
|
| + addTestDirectory("language", "language_2");
|
| + addTestDirectory("language_strong", "language_2");
|
| + addTestDirectory("lib", "lib_2");
|
| + addTestDirectory("lib_strong", "lib_2");
|
| +
|
| + var sorted = tests.values.toList();
|
| + sorted.sort((a, b) => a.twoPath.compareTo(b.twoPath));
|
| + return sorted;
|
| +}
|
| +
|
| +/// Moves the file from [from] to [to], which are both assumed to be relative
|
| +/// paths inside "tests".
|
| +void moveFile(String from, String to) {
|
| + if (dryRun) {
|
| + print(" Dry run: move $from to $to");
|
| + return;
|
| + }
|
| +
|
| + new File(p.join(testRoot, from)).renameSync(p.join(testRoot, to));
|
| +}
|
| +
|
| +/// Reads the contents of the file at [path], which is assumed to be relative
|
| +/// within "tests".
|
| +String readFile(String path) {
|
| + return new File(p.join(testRoot, path)).readAsStringSync();
|
| +}
|
| +
|
| +/// Deletes the file at [path], which is assumed to be relative within "tests".
|
| +void deleteFile(String path) {
|
| + if (dryRun) {
|
| + print(" Dry run: delete $path");
|
| + return;
|
| + }
|
| +
|
| + new File(p.join(testRoot, path)).deleteSync();
|
| +}
|
| +
|
| +bool checkForUnitTest(String path, String source) {
|
| + if (!source.contains("package:unittest")) return false;
|
| +
|
| + note("${bold(path)} uses unittest package.");
|
| + return true;
|
| +}
|
| +
|
| +class Fork {
|
| + final String twoPath;
|
| + String onePath;
|
| + String strongPath;
|
| +
|
| + String get twoSource {
|
| + if (twoPath == null) return null;
|
| + if (_twoSource == null) _twoSource = readFile(twoPath);
|
| + return _twoSource;
|
| + }
|
| +
|
| + String _twoSource;
|
| +
|
| + String get oneSource {
|
| + if (onePath == null) return null;
|
| + if (_oneSource == null) _oneSource = readFile(onePath);
|
| + return _oneSource;
|
| + }
|
| +
|
| + String _oneSource;
|
| +
|
| + String get strongSource {
|
| + if (strongPath == null) return null;
|
| + if (_strongSource == null) _strongSource = readFile(strongPath);
|
| + return _strongSource;
|
| + }
|
| +
|
| + String _strongSource;
|
| +
|
| + Fork(this.twoPath);
|
| +
|
| + bool migrate(List<String> todos) {
|
| + print("- ${bold(twoPath)}:");
|
| +
|
| + var todosBefore = todos.length;
|
| + var isMigrated = new File(p.join(testRoot, twoPath)).existsSync();
|
| +
|
| + // If there is a migrated version and it's the same as an unmigrated one,
|
| + // delete the unmigrated one.
|
| + if (isMigrated) {
|
| + if (onePath != null) {
|
| + if (oneSource == twoSource) {
|
| + deleteFile(onePath);
|
| + done("Deleted already-migrated $onePath.");
|
| + } else {
|
| + note("${bold(onePath)} does not match already-migrated "
|
| + "${bold(twoPath)}.");
|
| + todos.add("Merge ${bold(onePath)} into ${bold(twoPath)}.");
|
| + checkForUnitTest(onePath, oneSource);
|
| + }
|
| + }
|
| +
|
| + if (strongPath != null) {
|
| + if (strongSource == twoSource) {
|
| + deleteFile(strongPath);
|
| + done("Deleted already-migrated ${bold(strongPath)}.");
|
| + } else {
|
| + note("${bold(strongPath)} does not match already-migrated "
|
| + "${bold(twoPath)}.");
|
| + todos.add("Merge ${bold(strongPath)} into ${bold(twoPath)}.");
|
| + checkForUnitTest(strongPath, strongSource);
|
| + }
|
| + }
|
| + } else {
|
| + // If it only exists in one place, just move it.
|
| + if (strongPath == null) {
|
| + moveFile(onePath, twoPath);
|
| + done("Moved from ${bold(onePath)} (no strong mode fork).");
|
| + } else if (onePath == null) {
|
| + moveFile(strongPath, twoPath);
|
| + done("Moved from ${bold(strongPath)} (no 1.0 mode fork).");
|
| + } else if (oneSource == strongSource) {
|
| + // The forks are identical, pick one.
|
| + moveFile(onePath, twoPath);
|
| + deleteFile(strongPath);
|
| + done("Merged identical forks.");
|
| + checkForUnitTest(twoPath, oneSource);
|
| + } else {
|
| + // Otherwise, a manual merge is required. Start with the strong one.
|
| + moveFile(strongPath, twoPath);
|
| + done("Moved strong fork, kept 1.0 fork, manual merge required.");
|
| + todos.add("Merge ${bold(onePath)} into ${bold(twoPath)}.");
|
| + checkForUnitTest(onePath, oneSource);
|
| + }
|
| + }
|
| +
|
| + if (checkForUnitTest(twoPath, twoSource)) {
|
| + todos.add("Migrate ${bold(twoPath)} off unittest.");
|
| + }
|
| +
|
| + return todos.length == todosBefore;
|
| + }
|
| +}
|
|
|