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