blob: 9f84ff91e66122452800fbed6047ad396813bedd [file] [log] [blame]
// 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:io';
import 'dart:isolate';
import 'package:args/args.dart';
import 'package:coverage/coverage.dart';
import 'package:path/path.dart';
/// 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;
List files = msg.files;
var resolver = new Resolver(packageRoot: msg.pkgRoot, sdkRoot: msg.sdkRoot);
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 (msg.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 String sdkRoot;
final String pkgRoot;
final List files;
final SendPort replyPort;
final bool verbose;
WorkMessage(this.workerName, this.pkgRoot, this.sdkRoot, this.files,
this.replyPort, this.verbose);
}
class ResultMessage {
final hitmap;
final failedResolves;
ResultMessage(this.hitmap, this.failedResolves);
}
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, pkgRoot, sdkRoot, files, verbose) {
RawReceivePort port = new RawReceivePort();
var completer = new Completer();
port.handler = ((ResultMessage msg) {
completer.complete(msg);
port.close();
});
var msg = new WorkMessage(name, pkgRoot, sdkRoot, files, port.sendPort, verbose);
Isolate.spawn(worker, msg);
return completer.future;
}
/// [Environment] stores gathered arguments information.
class Environment {
String sdkRoot;
String pkgRoot;
String input;
IOSink output;
int workers;
bool prettyPrint;
bool lcov;
bool expectMarkers;
bool verbose;
}
main(List<String> arguments) {
final env = 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}');
}
// Create workers.
int workerId = 0;
var results = split(files, env.workers).map((workerFiles) {
var result = spawnWorker('Worker ${workerId++}', env.pkgRoot, env.sdkRoot,
workerFiles, env.verbose);
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, env.output);
} else if (env.lcov) {
out = lcov(globalHitmap, env.output);
}
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) {
final env = new Environment();
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', defaultsTo: 'stdout',
help: 'output: may be file or stdout');
parser.addOption('workers', abbr: 'j', defaultsTo: '1',
help: 'number of workers');
parser.addFlag('pretty-print', abbr: 'r', negatable: false,
help: 'convert coverage data to pretty print format');
parser.addFlag('lcov', abbr :'l', negatable: false,
help: 'convert coverage data to lcov format');
parser.addFlag('verbose', abbr :'v', negatable: false,
help: 'verbose output');
parser.addFlag('help', abbr: 'h', negatable: false,
help: 'show this help');
var args = parser.parse(arguments);
printUsage() {
print('Usage: dart format_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('DART_SDK')) {
env.sdkRoot =
join(absolute(normalize(Platform.environment['DART_SDK'])), '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.');
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 {
var outpath = absolute(normalize(args['out']));
var outfile = new File(outpath)
..createSync(recursive: true);
env.output = outfile.openWrite();
}
env.lcov = args['lcov'];
if (args['pretty-print'] && env.lcov) {
fail('Choose one of pretty-print or lcov output');
}
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'];
return env;
}