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; |
+ } |
+} |