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