|  | // Copyright (c) 2013, 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. | 
|  |  | 
|  | import "dart:async"; | 
|  | import "dart:convert"; | 
|  | import "dart:io"; | 
|  | import "dart:isolate"; | 
|  |  | 
|  | import "package:args/args.dart"; | 
|  | import "package:path/path.dart"; | 
|  |  | 
|  | /// [Environment] stores gathered arguments information. | 
|  | class Environment { | 
|  | String sdkRoot; | 
|  | String pkgRoot; | 
|  | var input; | 
|  | var output; | 
|  | int workers; | 
|  | bool prettyPrint; | 
|  | bool lcov; | 
|  | bool expectMarkers; | 
|  | bool verbose; | 
|  | } | 
|  |  | 
|  | /// [Resolver] resolves imports with respect to a given environment. | 
|  | class Resolver { | 
|  | static const DART_PREFIX = "dart:"; | 
|  | static const PACKAGE_PREFIX = "package:"; | 
|  | static const FILE_PREFIX = "file://"; | 
|  | static const HTTP_PREFIX = "http://"; | 
|  |  | 
|  | Map _env; | 
|  | List failed = []; | 
|  |  | 
|  | Resolver(this._env); | 
|  |  | 
|  | /// Returns the absolute path wrt. to the given environment or null, if the | 
|  | /// import could not be resolved. | 
|  | resolve(String import) { | 
|  | if (import.startsWith(DART_PREFIX)) { | 
|  | if (_env["sdkRoot"] == null) { | 
|  | // No sdk-root given, do not resolve dart: URIs. | 
|  | return null; | 
|  | } | 
|  | var slashPos = import.indexOf("/"); | 
|  | var filePath; | 
|  | if (slashPos != -1) { | 
|  | var path = import.substring(DART_PREFIX.length, slashPos); | 
|  | // Drop patch files, since we don't have their source in the compiled | 
|  | // SDK. | 
|  | if (path.endsWith("-patch")) { | 
|  | failed.add(import); | 
|  | return null; | 
|  | } | 
|  | // Canonicalize path. For instance: _collection-dev => _collection_dev. | 
|  | path = path.replaceAll("-", "_"); | 
|  | filePath = "${_env["sdkRoot"]}" | 
|  | "/${path}${import.substring(slashPos, import.length)}"; | 
|  | } else { | 
|  | // Resolve 'dart:something' to be something/something.dart in the SDK. | 
|  | var lib = import.substring(DART_PREFIX.length, import.length); | 
|  | filePath = "${_env["sdkRoot"]}/${lib}/${lib}.dart"; | 
|  | } | 
|  | return filePath; | 
|  | } | 
|  | if (import.startsWith(PACKAGE_PREFIX)) { | 
|  | if (_env["pkgRoot"] == null) { | 
|  | // No package-root given, do not resolve package: URIs. | 
|  | return null; | 
|  | } | 
|  | var filePath = | 
|  | "${_env["pkgRoot"]}" | 
|  | "/${import.substring(PACKAGE_PREFIX.length, import.length)}"; | 
|  | return filePath; | 
|  | } | 
|  | if (import.startsWith(FILE_PREFIX)) { | 
|  | var filePath = fromUri(Uri.parse(import)); | 
|  | return filePath; | 
|  | } | 
|  | if (import.startsWith(HTTP_PREFIX)) { | 
|  | return import; | 
|  | } | 
|  | // We cannot deal with anything else. | 
|  | failed.add(import); | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Converts the given hitmap to lcov format and appends the result to | 
|  | /// env.output. | 
|  | /// | 
|  | /// Returns a [Future] that completes as soon as all map entries have been | 
|  | /// emitted. | 
|  | Future lcov(Map hitmap) { | 
|  | var emitOne = (key) { | 
|  | var v = hitmap[key]; | 
|  | StringBuffer entry = new StringBuffer(); | 
|  | entry.write("SF:${key}\n"); | 
|  | v.keys.toList() | 
|  | ..sort() | 
|  | ..forEach((k) { | 
|  | entry.write("DA:${k},${v[k]}\n"); | 
|  | }); | 
|  | entry.write("end_of_record\n"); | 
|  | env.output.write(entry.toString()); | 
|  | return new Future.value(null); | 
|  | }; | 
|  |  | 
|  | return Future.forEach(hitmap.keys, emitOne); | 
|  | } | 
|  |  | 
|  | /// Converts the given hitmap to a pretty-print format and appends the result | 
|  | /// to env.output. | 
|  | /// | 
|  | /// Returns a [Future] that completes as soon as all map entries have been | 
|  | /// emitted. | 
|  | Future prettyPrint(Map hitMap, List failedLoads) { | 
|  | var emitOne = (key) { | 
|  | var v = hitMap[key]; | 
|  | var c = new Completer(); | 
|  | loadResource(key).then((lines) { | 
|  | if (lines == null) { | 
|  | failedLoads.add(key); | 
|  | c.complete(); | 
|  | return; | 
|  | } | 
|  | env.output.write("${key}\n"); | 
|  | for (var line = 1; line <= lines.length; line++) { | 
|  | String prefix = "       "; | 
|  | if (v.containsKey(line)) { | 
|  | prefix = v[line].toString(); | 
|  | StringBuffer b = new StringBuffer(); | 
|  | for (int i = prefix.length; i < 7; i++) { | 
|  | b.write(" "); | 
|  | } | 
|  | b.write(prefix); | 
|  | prefix = b.toString(); | 
|  | } | 
|  | env.output.write("${prefix}|${lines[line-1]}\n"); | 
|  | } | 
|  | c.complete(); | 
|  | }); | 
|  | return c.future; | 
|  | }; | 
|  |  | 
|  | return Future.forEach(hitMap.keys, emitOne); | 
|  | } | 
|  |  | 
|  | /// Load an import resource and return a [Future] with a [List] of its lines. | 
|  | /// Returns [null] instead of a list if the resource could not be loaded. | 
|  | Future<List> loadResource(String import) { | 
|  | if (import.startsWith("http")) { | 
|  | Completer c = new Completer(); | 
|  | HttpClient client = new HttpClient(); | 
|  | client.getUrl(Uri.parse(import)) | 
|  | .then((HttpClientRequest request) { | 
|  | return request.close(); | 
|  | }) | 
|  | .then((HttpClientResponse response) { | 
|  | response.transform(new StringDecoder()).toList().then((data) { | 
|  | c.complete(data); | 
|  | httpClient.close(); | 
|  | }); | 
|  | }) | 
|  | .catchError((e) { | 
|  | c.complete(null); | 
|  | }); | 
|  | return c.future; | 
|  | } else { | 
|  | File f = new File(import); | 
|  | return f.readAsLines() | 
|  | .catchError((e) { | 
|  | return new Future.value(null); | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Creates a single hitmap from a raw json object. Throws away all entries that | 
|  | /// are not resolvable. | 
|  | Map createHitmap(String rawJson, Resolver resolver) { | 
|  | Map<String, Map<int,int>> hitMap = {}; | 
|  |  | 
|  | addToMap(source, line, count) { | 
|  | if (!hitMap[source].containsKey(line)) { | 
|  | hitMap[source][line] = 0; | 
|  | } | 
|  | hitMap[source][line] += count; | 
|  | } | 
|  |  | 
|  | JSON.decode(rawJson)['coverage'].forEach((Map e) { | 
|  | String source = resolver.resolve(e["source"]); | 
|  | if (source == null) { | 
|  | // Couldnt resolve import, so skip this entry. | 
|  | return; | 
|  | } | 
|  | if (!hitMap.containsKey(source)) { | 
|  | hitMap[source] = {}; | 
|  | } | 
|  | var hits = e["hits"]; | 
|  | // hits is a flat array of the following format: | 
|  | // [ <line|linerange>, <hitcount>,...] | 
|  | // line: number. | 
|  | // linerange: "<line>-<line>". | 
|  | for (var i = 0; i < hits.length; i += 2) { | 
|  | var k = hits[i]; | 
|  | if (k is num) { | 
|  | // Single line. | 
|  | addToMap(source, k, hits[i+1]); | 
|  | } | 
|  | if (k is String) { | 
|  | // Linerange. We expand line ranges to actual lines at this point. | 
|  | var splitPos = k.indexOf("-"); | 
|  | int start = int.parse(k.substring(0, splitPos)); | 
|  | int end = int.parse(k.substring(splitPos + 1, k.length)); | 
|  | for (var j = start; j <= end; j++) { | 
|  | addToMap(source, j, hits[i+1]); | 
|  | } | 
|  | } | 
|  | } | 
|  | }); | 
|  | return hitMap; | 
|  | } | 
|  |  | 
|  | /// Merges [newMap] into [result]. | 
|  | mergeHitmaps(Map newMap, Map result) { | 
|  | newMap.forEach((String file, Map v) { | 
|  | if (result.containsKey(file)) { | 
|  | v.forEach((int line, int cnt) { | 
|  | if (result[file][line] == null) { | 
|  | result[file][line] = cnt; | 
|  | } else { | 
|  | result[file][line] += cnt; | 
|  | } | 
|  | }); | 
|  | } else { | 
|  | result[file] = v; | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | /// Given an absolute path absPath, this function returns a [List] of files | 
|  | /// are contained by it if it is a directory, or a [List] containing the file if | 
|  | /// it is a file. | 
|  | List filesToProcess(String absPath) { | 
|  | var filePattern = new RegExp(r"^dart-cov-\d+-\d+.json$"); | 
|  | if (FileSystemEntity.isDirectorySync(absPath)) { | 
|  | return new Directory(absPath).listSync(recursive: true) | 
|  | .where((entity) => entity is File && | 
|  | filePattern.hasMatch(basename(entity.path))) | 
|  | .toList(); | 
|  | } | 
|  |  | 
|  | return [new File(absPath)]; | 
|  | } | 
|  |  | 
|  | worker(WorkMessage msg) { | 
|  | final start = new DateTime.now().millisecondsSinceEpoch; | 
|  |  | 
|  | var env = msg.environment; | 
|  | List files = msg.files; | 
|  | Resolver resolver = new Resolver(env); | 
|  | var workerHitmap = {}; | 
|  | files.forEach((File fileEntry) { | 
|  | // Read file sync, as it only contains 1 object. | 
|  | String contents = fileEntry.readAsStringSync(); | 
|  | if (contents.length > 0) { | 
|  | mergeHitmaps(createHitmap(contents, resolver), workerHitmap); | 
|  | } | 
|  | }); | 
|  |  | 
|  | if (env["verbose"]) { | 
|  | final end = new DateTime.now().millisecondsSinceEpoch; | 
|  | print("${msg.workerName}: Finished processing ${files.length} files. " | 
|  | "Took ${end - start} ms."); | 
|  | } | 
|  |  | 
|  | msg.replyPort.send(new ResultMessage(workerHitmap, resolver.failed)); | 
|  | } | 
|  |  | 
|  | class WorkMessage { | 
|  | final String workerName; | 
|  | final Map environment; | 
|  | final List files; | 
|  | final SendPort replyPort; | 
|  | WorkMessage(this.workerName, this.environment, this.files, this.replyPort); | 
|  | } | 
|  |  | 
|  | class ResultMessage { | 
|  | final hitmap; | 
|  | final failedResolves; | 
|  | ResultMessage(this.hitmap, this.failedResolves); | 
|  | } | 
|  |  | 
|  | final env = new Environment(); | 
|  |  | 
|  | List<List> split(List list, int nBuckets) { | 
|  | var buckets = new List(nBuckets); | 
|  | var bucketSize = list.length ~/ nBuckets; | 
|  | var leftover = list.length % nBuckets; | 
|  | var taken = 0; | 
|  | var start = 0; | 
|  | for (int i = 0; i < nBuckets; i++) { | 
|  | var end = (i < leftover) ? (start + bucketSize + 1) : (start + bucketSize); | 
|  | buckets[i] = list.sublist(start, end); | 
|  | taken += buckets[i].length; | 
|  | start = end; | 
|  | } | 
|  | if (taken != list.length) throw "Error splitting"; | 
|  | return buckets; | 
|  | } | 
|  |  | 
|  | Future<ResultMessage> spawnWorker(name, environment, files) { | 
|  | RawReceivePort port = new RawReceivePort(); | 
|  | var completer = new Completer(); | 
|  | port.handler = ((ResultMessage msg) { | 
|  | completer.complete(msg); | 
|  | port.close(); | 
|  | }); | 
|  | var msg = new WorkMessage(name, environment, files, port.sendPort); | 
|  | Isolate.spawn(worker, msg); | 
|  | return completer.future; | 
|  | } | 
|  |  | 
|  | main(List<String> arguments) { | 
|  | parseArgs(arguments); | 
|  |  | 
|  | List files = filesToProcess(env.input); | 
|  |  | 
|  | List failedResolves = []; | 
|  | List failedLoads = []; | 
|  | Map globalHitmap = {}; | 
|  | int start = new DateTime.now().millisecondsSinceEpoch; | 
|  |  | 
|  | if (env.verbose) { | 
|  | print("Environment:"); | 
|  | print("  # files: ${files.length}"); | 
|  | print("  # workers: ${env.workers}"); | 
|  | print("  sdk-root: ${env.sdkRoot}"); | 
|  | print("  package-root: ${env.pkgRoot}"); | 
|  | } | 
|  |  | 
|  | Map sharedEnv = { | 
|  | "sdkRoot": env.sdkRoot, | 
|  | "pkgRoot": env.pkgRoot, | 
|  | "verbose": env.verbose, | 
|  | }; | 
|  |  | 
|  | // Create workers. | 
|  | int workerId = 0; | 
|  | var results = split(files, env.workers).map((workerFiles) { | 
|  | var result = spawnWorker("Worker ${workerId++}", sharedEnv, workerFiles); | 
|  | return result.then((ResultMessage message) { | 
|  | mergeHitmaps(message.hitmap, globalHitmap); | 
|  | failedResolves.addAll(message.failedResolves); | 
|  | }); | 
|  | }); | 
|  |  | 
|  | Future.wait(results).then((ignore) { | 
|  | // All workers are done. Process the data. | 
|  | if (env.verbose) { | 
|  | final end = new DateTime.now().millisecondsSinceEpoch; | 
|  | print("Done creating a global hitmap. Took ${end - start} ms."); | 
|  | } | 
|  |  | 
|  | Future out; | 
|  | if (env.prettyPrint) { | 
|  | out = prettyPrint(globalHitmap, failedLoads); | 
|  | } | 
|  | if (env.lcov) { | 
|  | out = lcov(globalHitmap); | 
|  | } | 
|  |  | 
|  | out.then((_) { | 
|  | env.output.close().then((_) { | 
|  | if (env.verbose) { | 
|  | final end = new DateTime.now().millisecondsSinceEpoch; | 
|  | print("Done flushing output. Took ${end - start} ms."); | 
|  | } | 
|  | }); | 
|  |  | 
|  | if (env.verbose) { | 
|  | if (failedResolves.length > 0) { | 
|  | print("Failed to resolve:"); | 
|  | failedResolves.toSet().forEach((e) { | 
|  | print("  ${e}"); | 
|  | }); | 
|  | } | 
|  | if (failedLoads.length > 0) { | 
|  | print("Failed to load:"); | 
|  | failedLoads.toSet().forEach((e) { | 
|  | print("  ${e}"); | 
|  | }); | 
|  | } | 
|  | } | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /// Checks the validity of the provided arguments. Does not initialize actual | 
|  | /// processing. | 
|  | parseArgs(List<String> arguments) { | 
|  | var parser = new ArgParser(); | 
|  |  | 
|  | parser.addOption("sdk-root", abbr: "s", | 
|  | help: "path to the SDK root"); | 
|  | parser.addOption("package-root", abbr: "p", | 
|  | help: "path to the package root"); | 
|  | parser.addOption("in", abbr: "i", | 
|  | help: "input(s): may be file or directory"); | 
|  | parser.addOption("out", abbr: "o", | 
|  | help: "output: may be file or stdout", | 
|  | defaultsTo: "stdout"); | 
|  | parser.addOption("workers", abbr: "j", | 
|  | help: "number of workers", | 
|  | defaultsTo: "1"); | 
|  | parser.addFlag("pretty-print", abbr: "r", | 
|  | help: "convert coverage data to pretty print format", | 
|  | negatable: false); | 
|  | parser.addFlag("lcov", abbr :"l", | 
|  | help: "convert coverage data to lcov format", | 
|  | negatable: false); | 
|  | parser.addFlag("verbose", abbr :"v", | 
|  | help: "verbose output", | 
|  | negatable: false); | 
|  | parser.addFlag("help", abbr: "h", | 
|  | help: "show this help", | 
|  | negatable: false); | 
|  |  | 
|  | var args = parser.parse(arguments); | 
|  |  | 
|  | printUsage() { | 
|  | print("Usage: dart full-coverage.dart [OPTION...]\n"); | 
|  | print(parser.getUsage()); | 
|  | } | 
|  |  | 
|  | fail(String msg) { | 
|  | print("\n$msg\n"); | 
|  | printUsage(); | 
|  | exit(1); | 
|  | } | 
|  |  | 
|  | if (args["help"]) { | 
|  | printUsage(); | 
|  | exit(0); | 
|  | } | 
|  |  | 
|  | env.sdkRoot = args["sdk-root"]; | 
|  | if (env.sdkRoot == null) { | 
|  | if (Platform.environment.containsKey("SDK_ROOT")) { | 
|  | env.sdkRoot = | 
|  | join(absolute(normalize(Platform.environment["SDK_ROOT"])), "lib"); | 
|  | } | 
|  | } else { | 
|  | env.sdkRoot = join(absolute(normalize(env.sdkRoot)), "lib"); | 
|  | } | 
|  | if ((env.sdkRoot != null) && !FileSystemEntity.isDirectorySync(env.sdkRoot)) { | 
|  | fail("Provided SDK root '${args["sdk-root"]}' is not a valid SDK " | 
|  | "top-level directory"); | 
|  | } | 
|  |  | 
|  | env.pkgRoot = args["package-root"]; | 
|  | if (env.pkgRoot != null) { | 
|  | env.pkgRoot = absolute(normalize(args["package-root"])); | 
|  | if (!FileSystemEntity.isDirectorySync(env.pkgRoot)) { | 
|  | fail("Provided package root '${args["package-root"]}' is not directory."); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (args["in"] == null) { | 
|  | fail("No input files given."); | 
|  | } else { | 
|  | env.input = absolute(normalize(args["in"])); | 
|  | if (!FileSystemEntity.isDirectorySync(env.input) && | 
|  | !FileSystemEntity.isFileSync(env.input)) { | 
|  | fail("Provided input '${args["in"]}' is neither a directory, nor a file."); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (args["out"] == "stdout") { | 
|  | env.output = stdout; | 
|  | } else { | 
|  | env.output = absolute(normalize(args["out"])); | 
|  | env.output = new File(env.output).openWrite(); | 
|  | } | 
|  |  | 
|  | env.lcov = args["lcov"]; | 
|  | if (args["pretty-print"] && env.lcov) { | 
|  | fail("Choose one of pretty-print or lcov output"); | 
|  | } else if (!env.lcov) { | 
|  | // Use pretty-print either explicitly or by default. | 
|  | env.prettyPrint = true; | 
|  | } | 
|  |  | 
|  | try { | 
|  | env.workers = int.parse("${args["workers"]}"); | 
|  | } catch (e) { | 
|  | fail("Invalid worker count: $e"); | 
|  | } | 
|  |  | 
|  | env.verbose = args["verbose"]; | 
|  | } |