Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 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. | |
| 4 | |
| 5 import "dart:async"; | |
| 6 import "dart:convert"; | |
| 7 import "dart:io"; | |
| 8 import "dart:isolate"; | |
| 9 import "dart:mirrors"; | |
| 10 | |
| 11 import "package:args/args.dart"; | |
| 12 import "package:path/path.dart"; | |
| 13 | |
| 14 /// [Environment] stores gathered arguments information. | |
| 15 class Environment { | |
| 16 String sdkRoot; | |
| 17 String pkgRoot; | |
| 18 var input; | |
| 19 var output; | |
| 20 int workers; | |
| 21 bool prettyPrint; | |
| 22 bool lcov; | |
| 23 bool expectMarkers; | |
| 24 bool verbose; | |
| 25 } | |
| 26 | |
| 27 /// [Resolver] resolves imports with respect to a given environment. | |
| 28 class Resolver { | |
| 29 const DART_PREFIX = "dart:"; | |
| 30 const PACKAGE_PREFIX = "package:"; | |
| 31 const FILE_PREFIX = "file://"; | |
| 32 const HTTP_PREFIX = "http://"; | |
| 33 | |
| 34 Map _env; | |
| 35 List failed = []; | |
| 36 | |
| 37 Resolver(this._env); | |
| 38 | |
| 39 /// Returns the absolute path wrt. to the given environment or null, if the | |
| 40 /// import could not be resolved. | |
| 41 resolve(String import) { | |
| 42 if (import.startsWith(DART_PREFIX)) { | |
| 43 var slashPos = import.indexOf("/"); | |
| 44 var filePath; | |
| 45 if (slashPos != -1) { | |
| 46 var path = import.substring(DART_PREFIX.length, slashPos); | |
| 47 // Drop patch files, since we don't have their source in the compiled | |
| 48 // SDK. | |
| 49 if (path.endsWith("-patch")) { | |
| 50 failed.add(import); | |
| 51 return null; | |
| 52 } | |
| 53 // Canonicalize path. For instance: _collection-dev => _collection_dev. | |
| 54 path = path.replaceAll("-", "_"); | |
| 55 filePath = "${_env["sdkRoot"]}" | |
| 56 "/${path}${import.substring(slashPos, import.length)}"; | |
| 57 } else { | |
| 58 // Resolve 'dart:something' to be something/something.dart in the SDK. | |
| 59 var lib = import.substring(DART_PREFIX.length, import.length); | |
| 60 filePath = "${_env["sdkRoot"]}/${lib}/${lib}.dart"; | |
| 61 } | |
| 62 return filePath; | |
| 63 } | |
| 64 if (import.startsWith(PACKAGE_PREFIX)) { | |
| 65 var filePath = | |
| 66 "${_env["pkgRoot"]}" | |
| 67 "/${import.substring(PACKAGE_PREFIX.length, import.length)}"; | |
| 68 return filePath; | |
| 69 } | |
| 70 if (import.startsWith(FILE_PREFIX)) { | |
| 71 var filePath = fromUri(Uri.parse(import)); | |
| 72 return filePath; | |
| 73 } | |
| 74 if (import.startsWith(HTTP_PREFIX)) { | |
| 75 return import; | |
| 76 } | |
| 77 // We cannot deal with anything else. | |
| 78 failed.add(import); | |
| 79 return null; | |
| 80 } | |
| 81 } | |
| 82 | |
| 83 /// Converts the given hitmap to lcov format and appends the result to | |
| 84 /// env.output. | |
| 85 /// | |
| 86 /// Returns a [Future] that completes as soon as all map entries have been | |
| 87 /// emitted. | |
| 88 Future lcov(Map hitmap) { | |
| 89 var emitOne = (key) { | |
| 90 var v = hitmap[key]; | |
| 91 StringBuffer entry = new StringBuffer(); | |
| 92 entry.write("SF:${key}\n"); | |
| 93 v.keys.toList() | |
| 94 ..sort() | |
| 95 ..forEach((k) { | |
| 96 entry.write("DA:${k},${v[k]}\n"); | |
| 97 }); | |
| 98 entry.write("end_of_record\n"); | |
| 99 env.output.write(entry.toString()); | |
| 100 return new Future.value(null); | |
| 101 }; | |
| 102 | |
| 103 return Future.forEach(hitmap.keys, emitOne); | |
| 104 } | |
| 105 | |
| 106 /// Converts the given hitmap to a pretty-print format and appends the result | |
| 107 /// to env.output. | |
| 108 /// | |
| 109 /// Returns a [Future] that completes as soon as all map entries have been | |
| 110 /// emitted. | |
| 111 Future prettyPrint(Map hitMap, List failedLoads) { | |
| 112 var emitOne = (key) { | |
| 113 var v = hitMap[key]; | |
| 114 var c = new Completer(); | |
| 115 loadResource(key).then((lines) { | |
| 116 if (lines == null) { | |
| 117 failedLoads.add(key); | |
| 118 c.complete(); | |
| 119 return; | |
| 120 } | |
| 121 env.output.write("${key}\n"); | |
| 122 for (var line = 1; line <= lines.length; line++) { | |
| 123 String prefix = " "; | |
| 124 if (v.containsKey(line)) { | |
| 125 prefix = v[line].toString(); | |
| 126 StringBuffer b = new StringBuffer(); | |
| 127 for (int i = prefix.length; i < 7; i++) { | |
| 128 b.write(" "); | |
| 129 } | |
| 130 b.write(prefix); | |
| 131 prefix = b.toString(); | |
| 132 } | |
| 133 env.output.write("${prefix}|${lines[line-1]}\n"); | |
| 134 } | |
| 135 c.complete(); | |
| 136 }); | |
| 137 return c.future; | |
| 138 }; | |
| 139 | |
| 140 return Future.forEach(hitMap.keys, emitOne); | |
| 141 } | |
| 142 | |
| 143 /// Load an import resource and return a [Future] with a [List] of its lines. | |
| 144 /// Returns [null] instead of a list if the resource could not be loaded. | |
| 145 Future<List> loadResource(String import) { | |
| 146 if (import.startsWith("http")) { | |
| 147 Completer c = new Completer(); | |
| 148 HttpClient client = new HttpClient(); | |
| 149 client.getUrl(Uri.parse(import)) | |
| 150 .then((HttpClientRequest request) { | |
| 151 return request.close(); | |
| 152 }) | |
| 153 .then((HttpClientResponse response) { | |
| 154 response.transform(new StringDecoder()).toList().then((data) { | |
| 155 c.complete(data); | |
| 156 httpClient.close(); | |
| 157 }); | |
| 158 }) | |
| 159 .catchError((e) { | |
| 160 c.complete(null); | |
| 161 }); | |
| 162 return c.future; | |
| 163 } else { | |
| 164 File f = new File(import); | |
| 165 return f.readAsLines() | |
| 166 .catchError((e) { | |
| 167 return new Future.value(null); | |
| 168 }); | |
| 169 } | |
| 170 } | |
| 171 | |
| 172 /// Creates a single hitmap from a raw json object. Throws away all entries that | |
| 173 /// are not resolvable. | |
| 174 Map createHitmap(String rawJson, Resolver resolver) { | |
| 175 Map<String, Map<int,int>> hitMap = {}; | |
| 176 | |
| 177 addToMap(source, line, count) { | |
| 178 if (!hitMap[source].containsKey(line)) { | |
| 179 hitMap[source][line] = 0; | |
| 180 } | |
| 181 hitMap[source][line] += count; | |
| 182 } | |
| 183 | |
| 184 JSON.decode(rawJson).forEach((Map e) { | |
| 185 String source = resolver.resolve(e["source"]); | |
| 186 if (source == null) { | |
| 187 // Couldnt resolve import, so skip this entry. | |
| 188 return; | |
| 189 } | |
| 190 if (!hitMap.containsKey(source)) { | |
| 191 hitMap[source] = {}; | |
| 192 } | |
| 193 var hits = e["hits"]; | |
| 194 // hits is a flat array of the following format: | |
| 195 // [ <line|linerange>, <hitcount>,...] | |
| 196 // line: number. | |
| 197 // linerange: "<line>-<line>". | |
| 198 for (var i = 0; i < hits.length; i += 2) { | |
| 199 var k = hits[i]; | |
| 200 if (k is num) { | |
| 201 // Single line. | |
| 202 addToMap(source, k, hits[i+1]); | |
| 203 } | |
| 204 if (k is String) { | |
| 205 // Linerange. We expand line ranges to actual lines at this point. | |
| 206 var splitPos = k.indexOf("-"); | |
| 207 int start = int.parse(k.substring(0, splitPos)); | |
| 208 int end = int.parse(k.substring(splitPos + 1, k.length)); | |
| 209 for (var j = start; j <= end; j++) { | |
| 210 addToMap(source, j, hits[i+1]); | |
| 211 } | |
| 212 } | |
| 213 } | |
| 214 }); | |
| 215 return hitMap; | |
| 216 } | |
| 217 | |
| 218 /// Merges [newMap] into [result]. | |
| 219 mergeHitmaps(Map newMap, Map result) { | |
| 220 newMap.forEach((String file, Map v) { | |
| 221 if (result.containsKey(file)) { | |
| 222 v.forEach((int line, int cnt) { | |
| 223 if (result[file][line] == null) { | |
| 224 result[file][line] = cnt; | |
| 225 } else { | |
| 226 result[file][line] += cnt; | |
| 227 } | |
| 228 }); | |
| 229 } else { | |
| 230 result[file] = v; | |
| 231 } | |
| 232 }); | |
| 233 } | |
| 234 | |
| 235 /// Given an absolute path absPath, this function returns a [List] of files | |
| 236 /// are contained by it if it is a directory, or a [List] containing the file if | |
| 237 /// it is a file. | |
| 238 List filesToProcess(String absPath) { | |
| 239 if (FileSystemEntity.isDirectorySync(absPath)) { | |
| 240 Directory d = new Directory(absPath); | |
| 241 List files = []; | |
| 242 d.listSync(recursive: true).forEach((FileSystemEntity entity) { | |
| 243 if (entity is File) { | |
| 244 files.add(entity as File); | |
| 245 } | |
| 246 }); | |
| 247 return files; | |
| 248 } else if (FileSystemEntity.isFileSync(absPath)) { | |
| 249 return [ new File(absPath) ]; | |
| 250 } | |
| 251 } | |
| 252 | |
| 253 worker() { | |
| 254 final start = new DateTime.now().millisecondsSinceEpoch; | |
| 255 String me = currentMirrorSystem().isolate.debugName; | |
| 256 | |
| 257 port.receive((Message message, reply) { | |
| 258 if (message.type == Message.SHUTDOWN) { | |
| 259 port.close(); | |
| 260 } | |
| 261 | |
| 262 if (message.type == Message.WORK) { | |
| 263 var env = message.payload[0]; | |
| 264 List files = message.payload[1]; | |
| 265 Resolver resolver = new Resolver(env); | |
| 266 var workerHitmap = {}; | |
| 267 files.forEach((File fileEntry) { | |
| 268 // Read file sync, as it only contains 1 object. | |
| 269 String contents = fileEntry.readAsStringSync(); | |
| 270 if (contents.length > 0) { | |
| 271 mergeHitmaps(createHitmap(contents, resolver), workerHitmap); | |
| 272 } | |
| 273 }); | |
| 274 if (env["verbose"]) { | |
| 275 final end = new DateTime.now().millisecondsSinceEpoch; | |
| 276 print("worker[${me}]: Finished processing files. " | |
| 277 "Took ${end - start} ms."); | |
| 278 } | |
| 279 reply.send(new Message(Message.RESULT, [workerHitmap, resolver.failed])); | |
| 280 } | |
| 281 | |
| 282 }); | |
| 283 } | |
| 284 | |
| 285 class Message { | |
| 286 static const int SHUTDOWN = 1; | |
| 287 static const int RESULT = 2; | |
| 288 static const int WORK = 3; | |
| 289 | |
| 290 final int type; | |
| 291 final payload; | |
| 292 | |
| 293 Message(this.type, this.payload); | |
| 294 } | |
| 295 | |
| 296 final env = new Environment(); | |
| 297 | |
| 298 main() { | |
| 299 parseArgs(); | |
| 300 | |
| 301 List files = filesToProcess(env.input); | |
| 302 int filesPerWorker = (files.length / env.workers).toInt(); | |
|
Ivan Posva
2013/09/27 20:19:54
What do you do with the remainder? Also we have th
Michael Lippautz (Google)
2013/09/27 21:46:50
Done.
| |
| 303 List workerPorts = []; | |
| 304 int doneCnt = 0; | |
| 305 | |
| 306 List failedResolves = []; | |
| 307 List failedLoads = []; | |
| 308 Map globalHitmap = {}; | |
| 309 int start = new DateTime.now().millisecondsSinceEpoch; | |
| 310 | |
| 311 if (env.verbose) { | |
| 312 print("Environment:"); | |
| 313 print(" # files: ${files.length}"); | |
| 314 print(" # workers: ${env.workers}"); | |
| 315 print(" sdk-root: ${env.sdkRoot}"); | |
| 316 print(" package-root: ${env.pkgRoot}"); | |
| 317 } | |
| 318 | |
| 319 port.receive((Message message, reply) { | |
| 320 if (message.type == Message.RESULT) { | |
| 321 mergeHitmaps(message.payload[0], globalHitmap); | |
| 322 failedResolves.addAll(message.payload[1]); | |
| 323 doneCnt++; | |
| 324 } | |
| 325 | |
| 326 // All workers are done. Process the data. | |
| 327 if (doneCnt == env.workers) { | |
| 328 workerPorts.forEach((p) => p.send(new Message(Message.SHUTDOWN, null))); | |
| 329 if (env.verbose) { | |
| 330 final end = new DateTime.now().millisecondsSinceEpoch; | |
| 331 print("Done creating a global hitmap. Took ${end - start} ms."); | |
| 332 } | |
| 333 | |
| 334 Future out; | |
| 335 if (env.prettyPrint) { | |
| 336 out = prettyPrint(globalHitmap, failedLoads); | |
| 337 } | |
| 338 if (env.lcov) { | |
| 339 out = lcov(globalHitmap); | |
| 340 } | |
| 341 | |
| 342 out.then((_) { | |
| 343 env.output.close().then((_) { | |
| 344 if (env.verbose) { | |
| 345 final end = new DateTime.now().millisecondsSinceEpoch; | |
| 346 print("Done flushing output. Took ${end - start} ms."); | |
| 347 } | |
| 348 }); | |
| 349 port.close(); | |
| 350 | |
| 351 if (env.verbose) { | |
| 352 if (failedResolves.length > 0) { | |
| 353 print("Failed to resolve:"); | |
| 354 failedResolves.toSet().forEach((e) { | |
| 355 print(" ${e}"); | |
| 356 }); | |
| 357 } | |
| 358 if (failedLoads.length > 0) { | |
| 359 print("Failed to load:"); | |
| 360 failedLoads.toSet().forEach((e) { | |
| 361 print(" ${e}"); | |
| 362 }); | |
| 363 } | |
| 364 } | |
| 365 | |
| 366 }); | |
| 367 } | |
| 368 }); | |
| 369 | |
| 370 Map sharedEnv = { | |
| 371 "sdkRoot": env.sdkRoot, | |
| 372 "pkgRoot": env.pkgRoot, | |
| 373 "verbose": env.verbose, | |
| 374 }; | |
| 375 | |
| 376 // Create workers. | |
| 377 for (var i = 1; i < env.workers; i++) { | |
| 378 var p = spawnFunction(worker); | |
| 379 workerPorts.add(p); | |
| 380 var workerFiles = files.getRange(0, filesPerWorker).toList(); | |
| 381 files.removeRange(0, filesPerWorker); | |
| 382 p.send(new Message(Message.WORK, [sharedEnv, workerFiles]), port); | |
| 383 } | |
| 384 // Let the last worker deal with the rest of the files (which should be only | |
| 385 // off by at max (#workers - 1). | |
| 386 var p = spawnFunction(worker); | |
| 387 workerPorts.add(p); | |
| 388 p.send(new Message(Message.WORK, [sharedEnv, files]), port); | |
| 389 | |
| 390 return 0; | |
| 391 } | |
| 392 | |
| 393 /// Checks the validity of the provided arguments. Does not initialize actual | |
| 394 /// processing. | |
| 395 parseArgs() { | |
| 396 var parser = new ArgParser(); | |
| 397 | |
| 398 parser.addOption("sdk-root", abbr: "s", | |
| 399 help: "path to the SDK root"); | |
| 400 parser.addOption("package-root", abbr: "p", | |
| 401 help: "path to the package root", | |
| 402 defaultsTo: "."); | |
| 403 parser.addOption("in", abbr: "i", | |
| 404 help: "input(s): may be file or directory", | |
| 405 defaultsTo: "stdin"); | |
| 406 parser.addOption("out", abbr: "o", | |
| 407 help: "output: may be file or stdout", | |
| 408 defaultsTo: "stdout"); | |
| 409 parser.addOption("workers", abbr: "j", | |
| 410 help: "number of workers", | |
| 411 defaultsTo: "1"); | |
| 412 parser.addFlag("pretty-print", abbr: "r", | |
| 413 help: "convert coverage data to pretty print format", | |
| 414 negatable: false); | |
| 415 parser.addFlag("lcov", abbr :"l", | |
| 416 help: "convert coverage data to lcov format", | |
| 417 negatable: false); | |
| 418 parser.addFlag("verbose", abbr :"v", | |
| 419 help: "verbose output", | |
| 420 negatable: false); | |
| 421 parser.addFlag("help", abbr: "h", | |
| 422 help: "show this help", | |
| 423 negatable: false); | |
| 424 | |
| 425 var args = parser.parse(new Options().arguments); | |
| 426 | |
| 427 if (args["help"]) { | |
| 428 print("Usage: coverage [OPTION...]\n"); | |
| 429 print(parser.getUsage()); | |
| 430 exit(0); | |
| 431 } | |
| 432 | |
| 433 if (args["sdk-root"] == null) { | |
| 434 if (Platform.environment.containsKey("SDK_ROOT")) { | |
| 435 env.sdkRoot = | |
| 436 join(absolute(normalize(Platform.environment["SDK_ROOT"])), "lib"); | |
| 437 } else { | |
| 438 throw "No SDK root found, please specify one using --sdk-root."; | |
| 439 } | |
| 440 } else { | |
| 441 env.sdkRoot = join(absolute(normalize(args["sdk-root"])), "lib"); | |
| 442 } | |
| 443 if (!FileSystemEntity.isDirectorySync(env.sdkRoot)) { | |
| 444 throw "Provided SDK root ${args["sdk-root"]} is not a valid SDK " | |
| 445 "top-level directory"; | |
| 446 } | |
| 447 | |
| 448 if (args["package-root"] == null) { | |
| 449 env.pkgRoot = absolute(normalize("./packages")); | |
| 450 } else { | |
| 451 env.pkgRoot = absolute(normalize(args["package-root"])); | |
| 452 if (!FileSystemEntity.isDirectorySync(env.pkgRoot)) { | |
| 453 throw "Provided package root ${args["package-root"]} is not directory."; | |
| 454 } | |
| 455 } | |
| 456 | |
| 457 if (args["in"] == "stdin") { | |
| 458 env.input = "stdin"; | |
| 459 } else { | |
| 460 env.input = absolute(normalize(args["in"])); | |
| 461 if (!FileSystemEntity.isDirectorySync(env.input) && | |
| 462 !FileSystemEntity.isFileSync(env.input)) { | |
| 463 throw "Provided input ${args["in"]} is neither a directory, nor a file."; | |
| 464 } | |
| 465 } | |
| 466 | |
| 467 if (args["out"] == "stdout") { | |
| 468 env.output = stdout; | |
| 469 } else { | |
| 470 env.output = absolute(normalize(args["out"])); | |
| 471 env.output = new File(env.output).openWrite(); | |
| 472 } | |
| 473 | |
| 474 if (args["pretty-print"] && | |
| 475 args["lcov"]) { | |
| 476 throw "Choose either pretty-print or lcov output"; | |
| 477 } | |
| 478 | |
| 479 env.prettyPrint = args["pretty-print"]; | |
| 480 env.lcov = args["lcov"]; | |
| 481 env.verbose = args["verbose"]; | |
| 482 env.workers = int.parse("${args["workers"]}"); | |
| 483 } | |
| OLD | NEW |