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 |