| OLD | NEW |
| 1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /// Given the beginning and ending file names in a batch, does as much automated | 5 /// Given the beginning and ending file names in a batch, does as much automated |
| 6 /// migration and possible and prints out the remaining manual steps required. | 6 /// migration and possible and prints out the remaining manual steps required. |
| 7 /// | 7 /// |
| 8 /// This should be safe to run, and safe to re-run on an in-progress chunk. | 8 /// This should be safe to run, and safe to re-run on an in-progress chunk. |
| 9 /// However, it has not been thoroughly tested, so run at your own risk. | 9 /// However, it has not been thoroughly tested, so run at your own risk. |
| 10 | 10 |
| 11 import 'dart:io'; | 11 import 'dart:io'; |
| 12 | 12 |
| 13 import 'package:path/path.dart' as p; | 13 import 'package:path/path.dart' as p; |
| 14 | |
| 15 import 'package:migration/src/log.dart'; | |
| 16 import 'package:migration/src/validate.dart'; | |
| 17 import 'package:status_file/status_file.dart'; | 14 import 'package:status_file/status_file.dart'; |
| 18 | 15 |
| 16 import 'package:migration/src/fork.dart'; |
| 17 import 'package:migration/src/io.dart'; |
| 18 import 'package:migration/src/log.dart'; |
| 19 |
| 19 const simpleDirs = const ["corelib", "language", "lib"]; | 20 const simpleDirs = const ["corelib", "language", "lib"]; |
| 20 | 21 |
| 21 final String sdkRoot = | |
| 22 p.normalize(p.join(p.dirname(p.fromUri(Platform.script)), '../../../')); | |
| 23 | |
| 24 final String testRoot = p.join(sdkRoot, "tests"); | |
| 25 | |
| 26 bool dryRun = false; | |
| 27 | |
| 28 void main(List<String> arguments) { | 22 void main(List<String> arguments) { |
| 29 if (arguments.contains("--dry-run")) { | 23 if (arguments.contains("--dry-run")) { |
| 30 dryRun = true; | 24 dryRun = true; |
| 31 arguments = arguments.where((argument) => argument != "--dry-run").toList(); | 25 arguments = arguments.where((argument) => argument != "--dry-run").toList(); |
| 32 } | 26 } |
| 33 | 27 |
| 34 if (arguments.length != 2) { | 28 if (arguments.length != 2) { |
| 35 stderr.writeln( | 29 stderr.writeln( |
| 36 "Usage: dart migrate_batch.dart [--dry-run] <first file> <last file>"); | 30 "Usage: dart migrate_batch.dart [--dry-run] <first file> <last file>"); |
| 37 stderr.writeln(); | 31 stderr.writeln(); |
| 38 stderr.writeln("Example:"); | 32 stderr.writeln("Example:"); |
| 39 stderr.writeln(); | 33 stderr.writeln(); |
| 40 stderr.writeln( | 34 stderr.writeln( |
| 41 " \$ dart migrate_batch.dart corelib/map_to_string corelib/queue"); | 35 " \$ dart migrate_batch.dart corelib/map_to_string corelib/queue"); |
| 42 exit(1); | 36 exit(1); |
| 43 } | 37 } |
| 44 | 38 |
| 45 var first = toTwoPath(arguments[0]); | |
| 46 var last = toTwoPath(arguments[1]); | |
| 47 | |
| 48 var tests = scanTests(); | 39 var tests = scanTests(); |
| 49 | 40 |
| 50 // Find the range of files in the chunk. We use comparisons here instead of | 41 var startIndex = findFork(tests, arguments[0]); |
| 51 // equality to try to compensate for files that may only appear in one fork | 42 var endIndex = findFork(tests, arguments[1]); |
| 52 // and should be part of the chunk but aren't officially listed as the begin | |
| 53 // or end point. | |
| 54 var startIndex = -1; | |
| 55 var endIndex = 0; | |
| 56 for (var i = 0; i < tests.length; i++) { | |
| 57 if (startIndex == -1 && tests[i].twoPath.compareTo(first) >= 0) { | |
| 58 startIndex = i; | |
| 59 } | |
| 60 | 43 |
| 61 if (tests[i].twoPath.compareTo(last) > 0) { | 44 if (startIndex == null || endIndex == null) exit(1); |
| 62 endIndex = i; | |
| 63 break; | |
| 64 } | |
| 65 } | |
| 66 | 45 |
| 67 if ((endIndex - startIndex) == 0) { | 46 var first = tests[startIndex].twoPath; |
| 47 var last = tests[endIndex].twoPath; |
| 48 |
| 49 // Make the range half-inclusive to simplify the math below. |
| 50 endIndex++; |
| 51 |
| 52 if (endIndex - startIndex == 0) { |
| 68 print(bold("No tests in range.")); | 53 print(bold("No tests in range.")); |
| 69 return; | 54 return; |
| 70 } | 55 } |
| 71 | 56 |
| 72 print("Migrating ${bold(endIndex - startIndex)} tests from ${bold(first)} " | 57 print("Migrating ${bold(endIndex - startIndex)} tests from ${bold(first)} " |
| 73 "to ${bold(last)}..."); | 58 "to ${bold(last)}..."); |
| 74 print(""); | 59 print(""); |
| 75 | 60 |
| 76 var allTodos = <String, List<String>>{}; | 61 var allTodos = <String, List<String>>{}; |
| 77 tests = tests.sublist(startIndex, endIndex); | 62 tests = tests.sublist(startIndex, endIndex); |
| (...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 145 if (currentSection != null) { | 130 if (currentSection != null) { |
| 146 filteredStatusFile.sections.add(currentSection); | 131 filteredStatusFile.sections.add(currentSection); |
| 147 } | 132 } |
| 148 } | 133 } |
| 149 if (!filteredStatusFile.isEmpty) { | 134 if (!filteredStatusFile.isEmpty) { |
| 150 statusFileEntries.writeln("Entries for status file ${statusFile.path}:"); | 135 statusFileEntries.writeln("Entries for status file ${statusFile.path}:"); |
| 151 statusFileEntries.writeln(filteredStatusFile); | 136 statusFileEntries.writeln(filteredStatusFile); |
| 152 } | 137 } |
| 153 } | 138 } |
| 154 | 139 |
| 155 String toTwoPath(String path) { | 140 int findFork(List<Fork> forks, String description) { |
| 156 // Allow eliding "_test" and/or ".dart" to make things more command-line | 141 var matches = <int>[]; |
| 157 // friendly. | 142 |
| 158 if (!path.endsWith(".dart") && !path.endsWith("_test.dart")) { | 143 for (var i = 0; i < forks.length; i++) { |
| 159 path += "_test.dart"; | 144 if (forks[i].twoPath.contains(description)) matches.add(i); |
| 160 } | 145 } |
| 161 if (!path.endsWith(".dart")) path += ".dart"; | |
| 162 | 146 |
| 163 for (var dir in simpleDirs) { | 147 if (matches.isEmpty) { |
| 164 if (p.isWithin(dir, path)) { | 148 print('Could not find a test matching "${bold(description)}".'); |
| 165 return p.join("${dir}_2", p.relative(path, from: dir)); | 149 return null; |
| 150 } else if (matches.length == 1) { |
| 151 return matches.first; |
| 152 } else { |
| 153 print('Description "${bold(description)}" is ambiguous. Could be any of:'); |
| 154 for (var i in matches) { |
| 155 print("- ${forks[i].twoPath.replaceAll(description, bold(description))}"); |
| 166 } | 156 } |
| 167 | 157 |
| 168 if (p.isWithin("${dir}_strong", path)) { | 158 print("Please use a more precise description."); |
| 169 return p.join("${dir}_2", p.relative(path, from: dir)); | 159 return null; |
| 170 } | |
| 171 } | 160 } |
| 172 | |
| 173 if (p.isWithin("html", path)) { | |
| 174 return p.join("lib_2/html", p.relative(path, from: "html")); | |
| 175 } | |
| 176 | |
| 177 if (p.isWithin("isolate", path)) { | |
| 178 return p.join("lib_2/isolate", p.relative(path, from: "isolate")); | |
| 179 } | |
| 180 | |
| 181 // Guess it's already a two path. | |
| 182 return path; | |
| 183 } | 161 } |
| 184 | 162 |
| 185 /// Loads all of the unforked test files. | 163 /// Loads all of the unforked test files. |
| 186 /// | 164 /// |
| 187 /// Creates an list of [Fork]s, ordered by their destination paths. Handles | 165 /// Creates an list of [Fork]s, ordered by their destination paths. Handles |
| 188 /// tests that only appear in one fork or the other, or both. | 166 /// tests that only appear in one fork or the other, or both. |
| 189 List<Fork> scanTests() { | 167 List<Fork> scanTests() { |
| 190 var tests = <String, Fork>{}; | 168 var tests = <String, Fork>{}; |
| 191 | 169 |
| 192 addTestDirectory(String fromDir, String twoDir) { | 170 addFromDirectory(String fromDir, String twoDir) { |
| 193 for (var entry | 171 for (var path in listFiles(fromDir)) { |
| 194 in new Directory(p.join(testRoot, fromDir)).listSync(recursive: true)) { | 172 var fromPath = p.relative(path, from: testRoot); |
| 195 if (!entry.path.endsWith(".dart")) continue; | |
| 196 | |
| 197 var fromPath = p.relative(entry.path, from: testRoot); | |
| 198 var twoPath = p.join(twoDir, p.relative(fromPath, from: fromDir)); | 173 var twoPath = p.join(twoDir, p.relative(fromPath, from: fromDir)); |
| 199 | 174 |
| 200 var fork = tests.putIfAbsent(twoPath, () => new Fork(twoPath)); | 175 var fork = tests.putIfAbsent(twoPath, () => new Fork(twoPath)); |
| 201 if (fromDir.contains("_strong")) { | 176 if (fromDir.contains("_strong")) { |
| 202 fork.strongPath = fromPath; | 177 fork.strongPath = fromPath; |
| 203 } else { | 178 } else { |
| 204 fork.onePath = fromPath; | 179 fork.onePath = fromPath; |
| 205 } | 180 } |
| 206 } | 181 } |
| 207 } | 182 } |
| 208 | 183 |
| 209 addTestDirectory("corelib", "corelib_2"); | 184 addFromDirectory("corelib", "corelib_2"); |
| 210 addTestDirectory("corelib_strong", "corelib_2"); | 185 addFromDirectory("corelib_strong", "corelib_2"); |
| 211 addTestDirectory("html", "lib_2/html"); | 186 addFromDirectory("html", "lib_2/html"); |
| 212 addTestDirectory("isolate", "lib_2/isolate"); | 187 addFromDirectory("isolate", "lib_2/isolate"); |
| 213 addTestDirectory("language", "language_2"); | 188 addFromDirectory("language", "language_2"); |
| 214 addTestDirectory("language_strong", "language_2"); | 189 addFromDirectory("language_strong", "language_2"); |
| 215 addTestDirectory("lib", "lib_2"); | 190 addFromDirectory("lib", "lib_2"); |
| 216 addTestDirectory("lib_strong", "lib_2"); | 191 addFromDirectory("lib_strong", "lib_2"); |
| 192 |
| 193 // Include tests that have already been migrated too so we can show what |
| 194 // works remains to be done in them. |
| 195 const twoDirs = const [ |
| 196 "corelib_2", |
| 197 "lib_2", |
| 198 "language_2", |
| 199 ]; |
| 200 |
| 201 for (var dir in twoDirs) { |
| 202 for (var path in listFiles(dir)) { |
| 203 var twoPath = p.relative(path, from: testRoot); |
| 204 tests.putIfAbsent(twoPath, () => new Fork(twoPath)); |
| 205 } |
| 206 } |
| 217 | 207 |
| 218 var sorted = tests.values.toList(); | 208 var sorted = tests.values.toList(); |
| 219 sorted.sort((a, b) => a.twoPath.compareTo(b.twoPath)); | 209 sorted.sort((a, b) => a.twoPath.compareTo(b.twoPath)); |
| 220 return sorted; | 210 return sorted; |
| 221 } | 211 } |
| 222 | 212 |
| 223 List<StatusFile> loadStatusFiles() { | 213 List<StatusFile> loadStatusFiles() { |
| 224 var statusFiles = <StatusFile>[]; | 214 var statusFiles = <StatusFile>[]; |
| 225 | 215 |
| 226 addStatusFile(String fromDir) { | 216 addStatusFile(String fromDir) { |
| 227 for (var entry | 217 for (var path in listFiles(fromDir, extension: ".status")) { |
| 228 in new Directory(p.join(testRoot, fromDir)).listSync(recursive: true)) { | 218 statusFiles.add(new StatusFile.read(path)); |
| 229 if (!entry.path.endsWith(".status")) continue; | |
| 230 | |
| 231 statusFiles.add(new StatusFile.read(entry.path)); | |
| 232 } | 219 } |
| 233 } | 220 } |
| 234 | 221 |
| 235 addStatusFile("corelib"); | 222 addStatusFile("corelib"); |
| 236 addStatusFile("corelib_strong"); | 223 addStatusFile("corelib_strong"); |
| 237 addStatusFile("html"); | 224 addStatusFile("html"); |
| 238 addStatusFile("isolate"); | 225 addStatusFile("isolate"); |
| 239 addStatusFile("language"); | 226 addStatusFile("language"); |
| 240 addStatusFile("language_strong"); | 227 addStatusFile("language_strong"); |
| 241 addStatusFile("lib"); | 228 addStatusFile("lib"); |
| 242 addStatusFile("lib_strong"); | 229 addStatusFile("lib_strong"); |
| 243 return statusFiles; | 230 return statusFiles; |
| 244 } | 231 } |
| 245 | |
| 246 /// Moves the file from [from] to [to], which are both assumed to be relative | |
| 247 /// paths inside "tests". | |
| 248 void moveFile(String from, String to) { | |
| 249 if (dryRun) { | |
| 250 print(" Dry run: move $from to $to"); | |
| 251 return; | |
| 252 } | |
| 253 | |
| 254 // Create the directory if needed. | |
| 255 new Directory(p.dirname(p.join(testRoot, to))).createSync(recursive: true); | |
| 256 | |
| 257 new File(p.join(testRoot, from)).renameSync(p.join(testRoot, to)); | |
| 258 } | |
| 259 | |
| 260 /// Reads the contents of the file at [path], which is assumed to be relative | |
| 261 /// within "tests". | |
| 262 String readFile(String path) { | |
| 263 return new File(p.join(testRoot, path)).readAsStringSync(); | |
| 264 } | |
| 265 | |
| 266 /// Deletes the file at [path], which is assumed to be relative within "tests". | |
| 267 void deleteFile(String path) { | |
| 268 if (dryRun) { | |
| 269 print(" Dry run: delete $path"); | |
| 270 return; | |
| 271 } | |
| 272 | |
| 273 new File(p.join(testRoot, path)).deleteSync(); | |
| 274 } | |
| 275 | |
| 276 class Fork { | |
| 277 final String twoPath; | |
| 278 String onePath; | |
| 279 String strongPath; | |
| 280 | |
| 281 String get twoSource { | |
| 282 if (twoPath == null) return null; | |
| 283 if (_twoSource == null) _twoSource = readFile(twoPath); | |
| 284 return _twoSource; | |
| 285 } | |
| 286 | |
| 287 String _twoSource; | |
| 288 | |
| 289 String get oneSource { | |
| 290 if (onePath == null) return null; | |
| 291 if (_oneSource == null) _oneSource = readFile(onePath); | |
| 292 return _oneSource; | |
| 293 } | |
| 294 | |
| 295 String _oneSource; | |
| 296 | |
| 297 String get strongSource { | |
| 298 if (strongPath == null) return null; | |
| 299 if (_strongSource == null) _strongSource = readFile(strongPath); | |
| 300 return _strongSource; | |
| 301 } | |
| 302 | |
| 303 String _strongSource; | |
| 304 | |
| 305 Fork(this.twoPath); | |
| 306 | |
| 307 List<String> migrate() { | |
| 308 print("- ${bold(twoPath)}:"); | |
| 309 | |
| 310 var todos = <String>[]; | |
| 311 var isMigrated = new File(p.join(testRoot, twoPath)).existsSync(); | |
| 312 | |
| 313 // If there is a migrated version and it's the same as an unmigrated one, | |
| 314 // delete the unmigrated one. | |
| 315 if (isMigrated) { | |
| 316 if (onePath != null) { | |
| 317 if (oneSource == twoSource) { | |
| 318 deleteFile(onePath); | |
| 319 done("Deleted already-migrated $onePath."); | |
| 320 } else { | |
| 321 note("${bold(onePath)} does not match already-migrated " | |
| 322 "${bold(twoPath)}."); | |
| 323 todos.add("Merge from ${bold(onePath)} into this file."); | |
| 324 validateFile(onePath, oneSource); | |
| 325 } | |
| 326 } | |
| 327 | |
| 328 if (strongPath != null) { | |
| 329 if (strongSource == twoSource) { | |
| 330 deleteFile(strongPath); | |
| 331 done("Deleted already-migrated ${bold(strongPath)}."); | |
| 332 } else { | |
| 333 note("${bold(strongPath)} does not match already-migrated " | |
| 334 "${bold(twoPath)}."); | |
| 335 todos.add("Merge from ${bold(strongPath)} into this file."); | |
| 336 validateFile(strongPath, strongSource); | |
| 337 } | |
| 338 } | |
| 339 } else { | |
| 340 // If it only exists in one place, just move it. | |
| 341 if (strongPath == null) { | |
| 342 moveFile(onePath, twoPath); | |
| 343 done("Moved from ${bold(onePath)} (no strong mode fork)."); | |
| 344 } else if (onePath == null) { | |
| 345 moveFile(strongPath, twoPath); | |
| 346 done("Moved from ${bold(strongPath)} (no 1.0 mode fork)."); | |
| 347 } else if (oneSource == strongSource) { | |
| 348 // The forks are identical, pick one. | |
| 349 moveFile(onePath, twoPath); | |
| 350 deleteFile(strongPath); | |
| 351 done("Merged identical forks."); | |
| 352 validateFile(twoPath, oneSource); | |
| 353 } else { | |
| 354 // Otherwise, a manual merge is required. Start with the strong one. | |
| 355 print(new File(strongPath).existsSync()); | |
| 356 moveFile(strongPath, twoPath); | |
| 357 done("Moved strong fork, kept 1.0 fork, manual merge required."); | |
| 358 todos.add("Merge from ${bold(onePath)} into this file."); | |
| 359 validateFile(onePath, oneSource); | |
| 360 } | |
| 361 } | |
| 362 | |
| 363 validateFile(twoPath, twoSource, todos); | |
| 364 | |
| 365 return todos; | |
| 366 } | |
| 367 } | |
| OLD | NEW |