Chromium Code Reviews| Index: tools/full-coverage.dart |
| diff --git a/tools/full-coverage.dart b/tools/full-coverage.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..b188db4a6b88737e05e9f4ea6d75c6b2b603aed6 |
| --- /dev/null |
| +++ b/tools/full-coverage.dart |
| @@ -0,0 +1,483 @@ |
| +// 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 "dart:mirrors"; |
| + |
| +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 { |
| + const DART_PREFIX = "dart:"; |
| + const PACKAGE_PREFIX = "package:"; |
| + const FILE_PREFIX = "file://"; |
| + 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)) { |
| + 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)) { |
| + 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).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) { |
| + if (FileSystemEntity.isDirectorySync(absPath)) { |
| + Directory d = new Directory(absPath); |
| + List files = []; |
| + d.listSync(recursive: true).forEach((FileSystemEntity entity) { |
| + if (entity is File) { |
| + files.add(entity as File); |
| + } |
| + }); |
| + return files; |
| + } else if (FileSystemEntity.isFileSync(absPath)) { |
| + return [ new File(absPath) ]; |
| + } |
| +} |
| + |
| +worker() { |
| + final start = new DateTime.now().millisecondsSinceEpoch; |
| + String me = currentMirrorSystem().isolate.debugName; |
| + |
| + port.receive((Message message, reply) { |
| + if (message.type == Message.SHUTDOWN) { |
| + port.close(); |
| + } |
| + |
| + if (message.type == Message.WORK) { |
| + var env = message.payload[0]; |
| + List files = message.payload[1]; |
| + 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("worker[${me}]: Finished processing files. " |
| + "Took ${end - start} ms."); |
| + } |
| + reply.send(new Message(Message.RESULT, [workerHitmap, resolver.failed])); |
| + } |
| + |
| + }); |
| +} |
| + |
| +class Message { |
| + static const int SHUTDOWN = 1; |
| + static const int RESULT = 2; |
| + static const int WORK = 3; |
| + |
| + final int type; |
| + final payload; |
| + |
| + Message(this.type, this.payload); |
| +} |
| + |
| +final env = new Environment(); |
| + |
| +main() { |
| + parseArgs(); |
| + |
| + List files = filesToProcess(env.input); |
| + 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.
|
| + List workerPorts = []; |
| + int doneCnt = 0; |
| + |
| + 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}"); |
| + } |
| + |
| + port.receive((Message message, reply) { |
| + if (message.type == Message.RESULT) { |
| + mergeHitmaps(message.payload[0], globalHitmap); |
| + failedResolves.addAll(message.payload[1]); |
| + doneCnt++; |
| + } |
| + |
| + // All workers are done. Process the data. |
| + if (doneCnt == env.workers) { |
| + workerPorts.forEach((p) => p.send(new Message(Message.SHUTDOWN, null))); |
| + 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."); |
| + } |
| + }); |
| + port.close(); |
| + |
| + 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}"); |
| + }); |
| + } |
| + } |
| + |
| + }); |
| + } |
| + }); |
| + |
| + Map sharedEnv = { |
| + "sdkRoot": env.sdkRoot, |
| + "pkgRoot": env.pkgRoot, |
| + "verbose": env.verbose, |
| + }; |
| + |
| + // Create workers. |
| + for (var i = 1; i < env.workers; i++) { |
| + var p = spawnFunction(worker); |
| + workerPorts.add(p); |
| + var workerFiles = files.getRange(0, filesPerWorker).toList(); |
| + files.removeRange(0, filesPerWorker); |
| + p.send(new Message(Message.WORK, [sharedEnv, workerFiles]), port); |
| + } |
| + // Let the last worker deal with the rest of the files (which should be only |
| + // off by at max (#workers - 1). |
| + var p = spawnFunction(worker); |
| + workerPorts.add(p); |
| + p.send(new Message(Message.WORK, [sharedEnv, files]), port); |
| + |
| + return 0; |
| +} |
| + |
| +/// Checks the validity of the provided arguments. Does not initialize actual |
| +/// processing. |
| +parseArgs() { |
| + 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", |
| + defaultsTo: "."); |
| + parser.addOption("in", abbr: "i", |
| + help: "input(s): may be file or directory", |
| + defaultsTo: "stdin"); |
| + 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(new Options().arguments); |
| + |
| + if (args["help"]) { |
| + print("Usage: coverage [OPTION...]\n"); |
| + print(parser.getUsage()); |
| + exit(0); |
| + } |
| + |
| + if (args["sdk-root"] == null) { |
| + if (Platform.environment.containsKey("SDK_ROOT")) { |
| + env.sdkRoot = |
| + join(absolute(normalize(Platform.environment["SDK_ROOT"])), "lib"); |
| + } else { |
| + throw "No SDK root found, please specify one using --sdk-root."; |
| + } |
| + } else { |
| + env.sdkRoot = join(absolute(normalize(args["sdk-root"])), "lib"); |
| + } |
| + if (!FileSystemEntity.isDirectorySync(env.sdkRoot)) { |
| + throw "Provided SDK root ${args["sdk-root"]} is not a valid SDK " |
| + "top-level directory"; |
| + } |
| + |
| + if (args["package-root"] == null) { |
| + env.pkgRoot = absolute(normalize("./packages")); |
| + } else { |
| + env.pkgRoot = absolute(normalize(args["package-root"])); |
| + if (!FileSystemEntity.isDirectorySync(env.pkgRoot)) { |
| + throw "Provided package root ${args["package-root"]} is not directory."; |
| + } |
| + } |
| + |
| + if (args["in"] == "stdin") { |
| + env.input = "stdin"; |
| + } else { |
| + env.input = absolute(normalize(args["in"])); |
| + if (!FileSystemEntity.isDirectorySync(env.input) && |
| + !FileSystemEntity.isFileSync(env.input)) { |
| + throw "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(); |
| + } |
| + |
| + if (args["pretty-print"] && |
| + args["lcov"]) { |
| + throw "Choose either pretty-print or lcov output"; |
| + } |
| + |
| + env.prettyPrint = args["pretty-print"]; |
| + env.lcov = args["lcov"]; |
| + env.verbose = args["verbose"]; |
| + env.workers = int.parse("${args["workers"]}"); |
| +} |