Chromium Code Reviews| 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 /// A library for code coverage support for Dart. | |
| 6 library runtime.coverage.impl; | |
| 7 | |
| 8 import 'dart:async'; | |
| 9 import "dart:collection" show SplayTreeMap; | |
| 10 import "dart:io"; | |
| 11 import "dart:json" as json; | |
| 12 | |
| 13 import "package:pathos/path.dart" as pathos; | |
| 14 | |
| 15 import 'package:analyzer_experimental/src/generated/source.dart' show Source, So urceRange; | |
| 16 import 'package:analyzer_experimental/src/generated/scanner.dart' show StringSca nner; | |
| 17 import 'package:analyzer_experimental/src/generated/parser.dart' show Parser; | |
| 18 import 'package:analyzer_experimental/src/generated/ast.dart'; | |
| 19 import 'package:analyzer_experimental/src/generated/engine.dart' show RecordingE rrorListener; | |
|
Bob Nystrom
2013/06/17 21:30:11
I'd remove these "show" clauses.
scheglov
2013/06/18 06:11:04
These libraries are huge.
I feel uncomfortable if
| |
| 20 | |
| 21 import '../log.dart' as log; | |
| 22 import 'models.dart'; | |
| 23 import 'utils.dart'; | |
| 24 | |
| 25 /// Run the [targetPath] with code coverage rewriting. | |
| 26 /// Redirects stdandard process streams. | |
| 27 /// On process exit dumps coverage statistics into the [outPath]. | |
| 28 void runServerApplication(String targetPath, String outPath) { | |
| 29 var targetFolder = pathos.dirname(targetPath); | |
| 30 var targetName = pathos.basename(targetPath); | |
| 31 new CoverageServer(targetFolder, targetPath, outPath) | |
| 32 .start() | |
| 33 .then((int port) { | |
|
Bob Nystrom
2013/06/17 21:30:11
Most people don't type annotate lambdas. It's usua
scheglov
2013/06/18 06:11:04
Done.
| |
| 34 var options = new Options(); | |
| 35 var targetArgs = ['http://127.0.0.1:$port/$targetName']; | |
| 36 var dartExecutable = options.executable; | |
| 37 // TODO(scheglov) remove this after https://codereview.chromium.org/1636 8002/ | |
| 38 dartExecutable = '/Users/scheglov/Source/Dart/dart/xcodebuild/ReleaseIA3 2/dart'; | |
| 39 Process.start(dartExecutable, targetArgs).then((Process process) { | |
|
Bob Nystrom
2013/06/17 21:30:11
return Process.start...
If you create a future in
scheglov
2013/06/18 06:11:04
Done.
| |
| 40 process.exitCode.then(exit); | |
| 41 // Redirect process streams. | |
| 42 stdin.pipe(process.stdin); | |
| 43 process.stdout.pipe(stdout); | |
| 44 process.stderr.pipe(stderr); | |
| 45 }); | |
| 46 }); | |
| 47 } | |
| 48 | |
| 49 | |
| 50 /// Abstract server to listen requests and serve files, may be rewriting them. | |
| 51 abstract class RewriteServer { | |
| 52 final String basePath; | |
| 53 int port; | |
| 54 | |
| 55 RewriteServer(this.basePath); | |
| 56 | |
| 57 /// Runs the HTTP server on the ephemeral port and returns [Future] with it. | |
| 58 Future<int> start() { | |
| 59 return HttpServer.bind("127.0.0.1", 0).then((HttpServer server) { | |
| 60 port = server.port; | |
| 61 log.info('RewriteServer is listening at: $port.'); | |
| 62 server.listen((HttpRequest request) { | |
| 63 if (request.method == "GET") { | |
| 64 handleGetRequest(request); | |
| 65 } | |
| 66 if (request.method == "POST") { | |
| 67 handlePostRequest(request); | |
| 68 } | |
| 69 }); | |
| 70 return port; | |
| 71 }); | |
| 72 } | |
| 73 | |
| 74 handlePostRequest(HttpRequest request); | |
| 75 | |
| 76 handleGetRequest(HttpRequest request) { | |
| 77 var response = request.response; | |
| 78 // Prepare path. | |
| 79 var path = basePath + '/' + request.uri.path; | |
|
Bob Nystrom
2013/06/17 21:30:11
Use pathos.join for this.
scheglov
2013/06/18 06:11:04
Done.
| |
| 80 path = pathos.normalize(path); | |
|
Bob Nystrom
2013/06/17 21:30:11
If you're using pathos to work with URLs, you shou
scheglov
2013/06/18 06:11:04
Done.
| |
| 81 log.info('[$path] Requested.'); | |
| 82 // May be serve using just path. | |
| 83 { | |
|
Bob Nystrom
2013/06/17 21:30:11
Is there a reason for this block?
scheglov
2013/06/18 06:11:04
Just to don't leak variable 'content' into outer s
| |
| 84 String content = rewritePathContent(path); | |
|
pquitslund
2013/06/17 20:15:58
String -> var
scheglov
2013/06/17 20:50:59
Done.
| |
| 85 if (content != null) { | |
| 86 log.info('[$path] Request served by path.'); | |
| 87 response.write(content); | |
| 88 response.close(); | |
| 89 return; | |
| 90 } | |
| 91 } | |
| 92 // Serve from file. | |
| 93 log.info('[$path] Serving file.'); | |
| 94 var file = new File(path); | |
| 95 file.exists().then((bool found) { | |
|
Bob Nystrom
2013/06/17 21:30:11
.then() returns a future, and ideally you would do
scheglov
2013/06/18 06:11:04
I've added 'catchError'.
| |
| 96 if (found) { | |
| 97 // May be this files should be sent as is. | |
| 98 if (!shouldRewriteFile(path)) { | |
| 99 sendFile(request, file); | |
| 100 return; | |
| 101 } | |
| 102 // Rewrite content of the file. | |
| 103 file.readAsString().then((String content) { | |
| 104 log.finest('[$path] Done reading ${content.length} characters.'); | |
| 105 content = rewriteFileContent(path, content); | |
| 106 log.fine('[$path] Rewritten.'); | |
| 107 response.write(content); | |
| 108 response.close(); | |
| 109 }); | |
| 110 } else { | |
| 111 log.severe('[$path] File not found.'); | |
| 112 response.statusCode = HttpStatus.NOT_FOUND; | |
| 113 response.close(); | |
| 114 } | |
| 115 }); | |
| 116 } | |
| 117 | |
| 118 void sendFile(HttpRequest request, File file) { | |
| 119 file.fullPath().then((String fullPath) { | |
| 120 file.openRead() | |
| 121 .pipe(request.response) | |
| 122 .catchError((e) {}); | |
| 123 }); | |
| 124 } | |
| 125 | |
| 126 bool shouldRewriteFile(String path); | |
| 127 | |
| 128 /// Subclasses implement this method to rewrite the provided [code] of the fil e with [path]. | |
| 129 /// Returns some content or `null` if file content should be requested. | |
| 130 String rewritePathContent(String path); | |
| 131 | |
| 132 /// Subclasses implement this method to rewrite the provided [code] of the fil e with [path]. | |
| 133 String rewriteFileContent(String path, String code); | |
| 134 } | |
| 135 | |
| 136 | |
| 137 /// Server that rewrites Dart code so that it reports execution of statements an d other nodes. | |
| 138 class CoverageServer extends RewriteServer { | |
| 139 final AppInfo appInfo = new AppInfo(); | |
| 140 final String targetPath; | |
| 141 final String outPath; | |
| 142 | |
| 143 CoverageServer(String basePath, this.targetPath, this.outPath) : super(basePat h); | |
| 144 | |
| 145 void handlePostRequest(HttpRequest request) { | |
| 146 var id = 0; | |
| 147 var executedIds = new Set<int>(); | |
| 148 request.listen((List<int> data) { | |
|
pquitslund
2013/06/17 20:15:58
I'm guessing Bob will ask you to remove type annot
scheglov
2013/06/17 20:50:59
Well, may be.
Unfortunately we don't have type pro
Bob Nystrom
2013/06/17 21:30:11
Yes!
| |
| 149 log.fine('Received statistics, ${data.length} bytes.'); | |
| 150 while (true) { | |
| 151 int listIndex = id ~/ 8; | |
| 152 if (listIndex >= data.length) break; | |
| 153 int bitIndex = id % 8; | |
|
pquitslund
2013/06/17 20:15:58
More possibly unnecessary/unwanted type annotation
scheglov
2013/06/17 20:50:59
Done.
| |
| 154 if ((data[listIndex] & (1 << bitIndex)) != 0) { | |
| 155 executedIds.add(id); | |
| 156 } | |
| 157 id++; | |
| 158 } | |
| 159 }).onDone(() { | |
| 160 log.fine('Received all statistics.'); | |
| 161 { | |
| 162 var sb = new StringBuffer(); | |
| 163 appInfo.print(sb, executedIds); | |
| 164 new File(outPath).writeAsString(sb.toString()); | |
|
Bob Nystrom
2013/06/17 21:30:11
writeAsStringSync() or handle the returned future.
scheglov
2013/06/18 06:11:04
Actually I've found new File(outPath).openWrite()
| |
| 165 } | |
| 166 log.fine('Results are written to $outPath.'); | |
| 167 request.response.close(); | |
| 168 }); | |
| 169 } | |
| 170 | |
| 171 String rewritePathContent(String path) { | |
| 172 if (path.endsWith('__coverage_lib.dart')) { | |
| 173 String implPath = pathos.joinAll([ | |
| 174 pathos.dirname(new Options().script), | |
| 175 '..', 'lib', 'src', 'services', 'runtime', 'coverage', 'coverage_lib.d art']); | |
| 176 var content = new File(implPath).readAsStringSync(); | |
| 177 content = content.replaceAll('0; // replaced during rewrite', '$port;'); | |
| 178 return content; | |
| 179 } | |
| 180 return null; | |
| 181 } | |
| 182 | |
| 183 bool shouldRewriteFile(String path) { | |
| 184 if (pathos.extension(path).toLowerCase() != '.dart') return false; | |
| 185 // Rewrite target itself, only to send statistics. | |
| 186 if (path == targetPath) { | |
| 187 return true; | |
| 188 } | |
| 189 // TODO(scheglov) use configuration | |
| 190 if (path.contains('/packages/analyzer_experimental/')) { | |
|
Bob Nystrom
2013/06/17 21:30:11
return path.contains(...)
scheglov
2013/06/18 06:11:04
Done.
| |
| 191 return true; | |
| 192 } | |
| 193 return false; | |
| 194 } | |
| 195 | |
| 196 String rewriteFileContent(String path, String code) { | |
| 197 var unit = _parseCode(code); | |
| 198 log.finest('[$path] Parsed.'); | |
| 199 var injector = new CodeInjector(code); | |
| 200 // Inject imports. | |
| 201 var directives = unit.directives; | |
| 202 if (directives.isNotEmpty && directives[0] is LibraryDirective) { | |
| 203 injector.inject(directives[0].end, | |
| 204 'import "package:unittest/unittest.dart" as __cc_ut;' | |
| 205 'import "http://127.0.0.1:$port/__coverage_lib.dart" as __cc;'); | |
| 206 } | |
| 207 // Inject statistics sender. | |
| 208 var isTargetScript = path == targetPath; | |
| 209 if (isTargetScript) { | |
| 210 for (var node in unit.declarations) { | |
| 211 if (node is FunctionDeclaration) { | |
| 212 var body = node.functionExpression.body; | |
| 213 if (node.name.name == 'main' && body is BlockFunctionBody) { | |
| 214 injector.inject(node.offset, | |
| 215 'class __CCC extends __cc_ut.Configuration {' | |
|
Bob Nystrom
2013/06/17 21:30:11
Multiline string?
scheglov
2013/06/18 06:11:04
Done.
| |
| 216 ' void onDone(bool success) {' | |
| 217 ' __cc.postStatistics();' | |
| 218 ' super.onDone(success);' | |
| 219 ' }' | |
| 220 '}'); | |
| 221 injector.inject(body.offset + 1, '__cc_ut.unittestConfiguration = ne w __CCC();'); | |
| 222 } | |
| 223 } | |
| 224 } | |
| 225 } | |
| 226 // Inject touch() invocations. | |
| 227 if (!isTargetScript) { | |
| 228 appInfo.enterUnit(path, code); | |
| 229 unit.accept(new InsertTouchInvocationsVisitor(appInfo, injector)); | |
| 230 } | |
| 231 // Done. | |
| 232 return injector.getResult(); | |
| 233 } | |
| 234 | |
| 235 CompilationUnit _parseCode(String code) { | |
| 236 var source = null; | |
| 237 var errorListener = new RecordingErrorListener(); | |
| 238 var parser = new Parser(source, errorListener); | |
| 239 var scanner = new StringScanner(source, code, errorListener); | |
| 240 var token = scanner.tokenize(); | |
| 241 return parser.parseCompilationUnit(token); | |
| 242 } | |
| 243 } | |
| 244 | |
| 245 | |
| 246 /// The visitor that inserts `touch` method invocations. | |
| 247 class InsertTouchInvocationsVisitor extends GeneralizingASTVisitor { | |
| 248 final AppInfo appInfo; | |
| 249 final CodeInjector injector; | |
| 250 | |
| 251 InsertTouchInvocationsVisitor(this.appInfo, this.injector); | |
| 252 | |
| 253 visitClassDeclaration(ClassDeclaration node) { | |
| 254 appInfo.enter('class', node.name.name); | |
| 255 super.visitClassDeclaration(node); | |
| 256 appInfo.leave(); | |
| 257 } | |
| 258 | |
| 259 visitConstructorDeclaration(ConstructorDeclaration node) { | |
| 260 var className = (node.parent as ClassDeclaration).name.name; | |
| 261 var constructorName; | |
| 262 if (node.name == null) { | |
| 263 constructorName = className; | |
| 264 } else { | |
| 265 constructorName = className + '.' + node.name.name; | |
| 266 } | |
| 267 appInfo.enter('constructor', constructorName); | |
| 268 super.visitConstructorDeclaration(node); | |
| 269 appInfo.leave(); | |
| 270 } | |
| 271 | |
| 272 visitMethodDeclaration(MethodDeclaration node) { | |
| 273 if (node.isAbstract()) { | |
| 274 super.visitMethodDeclaration(node); | |
| 275 } else { | |
| 276 var kind; | |
| 277 if (node.isGetter()) { | |
| 278 kind = 'getter'; | |
| 279 } else if (node.isSetter()) { | |
| 280 kind = 'setter'; | |
| 281 } else { | |
| 282 kind = 'method'; | |
| 283 } | |
| 284 appInfo.enter(kind, node.name.name); | |
| 285 super.visitMethodDeclaration(node); | |
| 286 appInfo.leave(); | |
| 287 } | |
| 288 } | |
| 289 | |
| 290 visitStatement(Statement node) { | |
| 291 insertTouch(node); | |
| 292 super.visitStatement(node); | |
| 293 } | |
| 294 | |
| 295 void insertTouch(Statement node) { | |
| 296 if (node is Block) return; | |
| 297 if (node.parent is LabeledStatement) return; | |
| 298 if (node.parent is! Block) return; | |
| 299 // Inject 'touch' invocation. | |
| 300 var offset = node.offset; | |
| 301 var id = appInfo.addNode(node); | |
| 302 injector.inject(offset, '__cc.touch($id);'); | |
| 303 } | |
| 304 } | |
| 305 | |
| 306 | |
| 307 /// Helper for injecting fragments into some existing code. | |
| 308 class CodeInjector { | |
| 309 final String _code; | |
| 310 final Map<int, String> offsetFragmentMap = new SplayTreeMap<int, String>(); | |
| 311 | |
| 312 CodeInjector(this._code); | |
| 313 | |
| 314 void inject(int offset, String fragment) { | |
| 315 offsetFragmentMap[offset] = fragment; | |
| 316 } | |
| 317 | |
| 318 String getResult() { | |
| 319 var sb = new StringBuffer(); | |
| 320 var lastOffset = 0; | |
| 321 offsetFragmentMap.forEach((int offset, String fragment) { | |
| 322 sb.write(_code.substring(lastOffset, offset)); | |
| 323 sb.write(fragment); | |
| 324 lastOffset = offset; | |
| 325 }); | |
| 326 sb.write(_code.substring(lastOffset, _code.length)); | |
| 327 return sb.toString(); | |
| 328 } | |
| 329 } | |
| OLD | NEW |