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 |