Index: observatory_pub_packages/analyzer/src/services/runtime/coverage/coverage_impl.dart |
=================================================================== |
--- observatory_pub_packages/analyzer/src/services/runtime/coverage/coverage_impl.dart (revision 0) |
+++ observatory_pub_packages/analyzer/src/services/runtime/coverage/coverage_impl.dart (working copy) |
@@ -0,0 +1,339 @@ |
+// 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. |
+ |
+/// A library for code coverage support for Dart. |
+library runtime.coverage.impl; |
+ |
+import 'dart:async'; |
+import 'dart:collection' show SplayTreeMap; |
+import 'dart:io'; |
+ |
+import 'package:path/path.dart' as pathos; |
+ |
+import 'package:analyzer/src/generated/scanner.dart' show CharSequenceReader, Scanner; |
+import 'package:analyzer/src/generated/parser.dart' show Parser; |
+import 'package:analyzer/src/generated/ast.dart'; |
+import 'package:analyzer/src/generated/engine.dart' show RecordingErrorListener; |
+ |
+import '../log.dart' as log; |
+import 'models.dart'; |
+ |
+/// Run the [targetPath] with code coverage rewriting. |
+/// Redirects stdandard process streams. |
+/// On process exit dumps coverage statistics into the [outPath]. |
+void runServerApplication(String targetPath, String outPath) { |
+ var targetFolder = pathos.dirname(targetPath); |
+ var targetName = pathos.basename(targetPath); |
+ var server = new CoverageServer(targetFolder, targetPath, outPath); |
+ server.start().then((port) { |
+ var targetArgs = ['http://127.0.0.1:$port/$targetName']; |
+ var dartExecutable = Platform.executable; |
+ return Process.start(dartExecutable, targetArgs); |
+ }).then((process) { |
+ stdin.pipe(process.stdin); |
+ process.stdout.pipe(stdout); |
+ process.stderr.pipe(stderr); |
+ return process.exitCode; |
+ }).then(exit).catchError((e) { |
+ log.severe('Error starting $targetPath. $e'); |
+ }); |
+} |
+ |
+ |
+/// Abstract server to listen requests and serve files, may be rewriting them. |
+abstract class RewriteServer { |
+ final String basePath; |
+ int port; |
+ |
+ RewriteServer(this.basePath); |
+ |
+ /// Runs the HTTP server on the ephemeral port and returns [Future] with it. |
+ Future<int> start() { |
+ return HttpServer.bind('127.0.0.1', 0).then((server) { |
+ port = server.port; |
+ log.info('RewriteServer is listening at: $port.'); |
+ server.listen((request) { |
+ if (request.method == 'GET') { |
+ handleGetRequest(request); |
+ } |
+ if (request.method == 'POST') { |
+ handlePostRequest(request); |
+ } |
+ }); |
+ return port; |
+ }); |
+ } |
+ |
+ void handlePostRequest(HttpRequest request); |
+ |
+ void handleGetRequest(HttpRequest request) { |
+ var response = request.response; |
+ // Prepare path. |
+ var path = getFilePath(request.uri); |
+ log.info('[$path] Requested.'); |
+ // May be serve using just path. |
+ { |
+ var content = rewritePathContent(path); |
+ if (content != null) { |
+ log.info('[$path] Request served by path.'); |
+ response.write(content); |
+ response.close(); |
+ return; |
+ } |
+ } |
+ // Serve from file. |
+ log.info('[$path] Serving file.'); |
+ var file = new File(path); |
+ file.exists().then((found) { |
+ if (found) { |
+ // May be this files should be sent as is. |
+ if (!shouldRewriteFile(path)) { |
+ return sendFile(request, file); |
+ } |
+ // Rewrite content of the file. |
+ return file.readAsString().then((content) { |
+ log.finest('[$path] Done reading ${content.length} characters.'); |
+ content = rewriteFileContent(path, content); |
+ log.fine('[$path] Rewritten.'); |
+ response.write(content); |
+ return response.close(); |
+ }); |
+ } else { |
+ log.severe('[$path] File not found.'); |
+ response.statusCode = HttpStatus.NOT_FOUND; |
+ return response.close(); |
+ } |
+ }).catchError((e) { |
+ log.severe('[$path] $e.'); |
+ response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR; |
+ return response.close(); |
+ }); |
+ } |
+ |
+ String getFilePath(Uri uri) { |
+ var path = uri.path; |
+ path = pathos.joinAll(uri.pathSegments); |
+ path = pathos.join(basePath, path); |
+ return pathos.normalize(path); |
+ } |
+ |
+ Future sendFile(HttpRequest request, File file) { |
+ return file.resolveSymbolicLinks().then((fullPath) { |
+ return file.openRead().pipe(request.response); |
+ }); |
+ } |
+ |
+ bool shouldRewriteFile(String path); |
+ |
+ /// Subclasses implement this method to rewrite the provided [code] of the |
+ /// file with [path]. Returns some content or `null` if file content |
+ /// should be requested. |
+ String rewritePathContent(String path); |
+ |
+ /// Subclasses implement this method to rewrite the provided [code] of the |
+ /// file with [path]. |
+ String rewriteFileContent(String path, String code); |
+} |
+ |
+ |
+/// Here `CCC` means 'code coverage configuration'. |
+const TEST_UNIT_CCC = ''' |
+class __CCC extends __cc_ut.Configuration { |
+ void onDone(bool success) { |
+ __cc.postStatistics(); |
+ super.onDone(success); |
+ } |
+}'''; |
+ |
+const TEST_UNIT_CCC_SET = '__cc_ut.unittestConfiguration = new __CCC();'; |
+ |
+ |
+/// Server that rewrites Dart code so that it reports execution of statements |
+/// and other nodes. |
+class CoverageServer extends RewriteServer { |
+ final appInfo = new AppInfo(); |
+ final String targetPath; |
+ final String outPath; |
+ |
+ CoverageServer(String basePath, this.targetPath, this.outPath) |
+ : super(basePath); |
+ |
+ void handlePostRequest(HttpRequest request) { |
+ var id = 0; |
+ var executedIds = new Set<int>(); |
+ request.listen((data) { |
+ log.fine('Received statistics, ${data.length} bytes.'); |
+ while (true) { |
+ var listIndex = id ~/ 8; |
+ if (listIndex >= data.length) break; |
+ var bitIndex = id % 8; |
+ if ((data[listIndex] & (1 << bitIndex)) != 0) { |
+ executedIds.add(id); |
+ } |
+ id++; |
+ } |
+ }).onDone(() { |
+ log.fine('Received all statistics.'); |
+ var buffer = new StringBuffer(); |
+ appInfo.write(buffer, executedIds); |
+ new File(outPath).writeAsString(buffer.toString()).then((_) { |
+ return request.response.close(); |
+ }).catchError((e) { |
+ log.severe('Error in receiving statistics $e.'); |
+ return request.response.close(); |
+ }); |
+ }); |
+ } |
+ |
+ String rewritePathContent(String path) { |
+ if (path.endsWith('__coverage_lib.dart')) { |
+ String implPath = pathos.joinAll([ |
+ pathos.dirname(Platform.script.toFilePath()), |
+ '..', 'lib', 'src', 'services', 'runtime', 'coverage', |
+ 'coverage_lib.dart']); |
+ var content = new File(implPath).readAsStringSync(); |
+ return content.replaceAll('0; // replaced during rewrite', '$port;'); |
+ } |
+ return null; |
+ } |
+ |
+ bool shouldRewriteFile(String path) { |
+ if (pathos.extension(path).toLowerCase() != '.dart') return false; |
+ // Rewrite target itself, only to send statistics. |
+ if (path == targetPath) { |
+ return true; |
+ } |
+ // TODO(scheglov) use configuration |
+ return path.contains('/packages/analyzer/'); |
+ } |
+ |
+ String rewriteFileContent(String path, String code) { |
+ var unit = _parseCode(code); |
+ log.finest('[$path] Parsed.'); |
+ var injector = new CodeInjector(code); |
+ // Inject imports. |
+ var directives = unit.directives; |
+ if (directives.isNotEmpty && directives[0] is LibraryDirective) { |
+ injector.inject(directives[0].end, |
+ 'import "package:unittest/unittest.dart" as __cc_ut;' |
+ 'import "http://127.0.0.1:$port/__coverage_lib.dart" as __cc;'); |
+ } |
+ // Inject statistics sender. |
+ var isTargetScript = path == targetPath; |
+ if (isTargetScript) { |
+ for (var node in unit.declarations) { |
+ if (node is FunctionDeclaration) { |
+ var body = node.functionExpression.body; |
+ if (node.name.name == 'main' && body is BlockFunctionBody) { |
+ injector.inject(node.offset, TEST_UNIT_CCC); |
+ injector.inject(body.offset + 1, TEST_UNIT_CCC_SET); |
+ } |
+ } |
+ } |
+ } |
+ // Inject touch() invocations. |
+ if (!isTargetScript) { |
+ appInfo.enterUnit(path, code); |
+ unit.accept(new InsertTouchInvocationsVisitor(appInfo, injector)); |
+ } |
+ // Done. |
+ return injector.getResult(); |
+ } |
+ |
+ CompilationUnit _parseCode(String code) { |
+ var source = null; |
+ var errorListener = new RecordingErrorListener(); |
+ var parser = new Parser(source, errorListener); |
+ var reader = new CharSequenceReader(code); |
+ var scanner = new Scanner(null, reader, errorListener); |
+ var token = scanner.tokenize(); |
+ return parser.parseCompilationUnit(token); |
+ } |
+} |
+ |
+ |
+/// The visitor that inserts `touch` method invocations. |
+class InsertTouchInvocationsVisitor extends GeneralizingAstVisitor { |
+ final AppInfo appInfo; |
+ final CodeInjector injector; |
+ |
+ InsertTouchInvocationsVisitor(this.appInfo, this.injector); |
+ |
+ visitClassDeclaration(ClassDeclaration node) { |
+ appInfo.enter('class', node.name.name); |
+ super.visitClassDeclaration(node); |
+ appInfo.leave(); |
+ } |
+ |
+ visitConstructorDeclaration(ConstructorDeclaration node) { |
+ var className = (node.parent as ClassDeclaration).name.name; |
+ var constructorName; |
+ if (node.name == null) { |
+ constructorName = className; |
+ } else { |
+ constructorName = className + '.' + node.name.name; |
+ } |
+ appInfo.enter('constructor', constructorName); |
+ super.visitConstructorDeclaration(node); |
+ appInfo.leave(); |
+ } |
+ |
+ visitMethodDeclaration(MethodDeclaration node) { |
+ if (node.isAbstract) { |
+ super.visitMethodDeclaration(node); |
+ } else { |
+ var kind; |
+ if (node.isGetter) { |
+ kind = 'getter'; |
+ } else if (node.isSetter) { |
+ kind = 'setter'; |
+ } else { |
+ kind = 'method'; |
+ } |
+ appInfo.enter(kind, node.name.name); |
+ super.visitMethodDeclaration(node); |
+ appInfo.leave(); |
+ } |
+ } |
+ |
+ visitStatement(Statement node) { |
+ insertTouch(node); |
+ super.visitStatement(node); |
+ } |
+ |
+ void insertTouch(Statement node) { |
+ if (node is Block) return; |
+ if (node.parent is LabeledStatement) return; |
+ if (node.parent is! Block) return; |
+ // Inject 'touch' invocation. |
+ var offset = node.offset; |
+ var id = appInfo.addNode(node); |
+ injector.inject(offset, '__cc.touch($id);'); |
+ } |
+} |
+ |
+ |
+/// Helper for injecting fragments into some existing code. |
+class CodeInjector { |
+ final String _code; |
+ final offsetFragmentMap = new SplayTreeMap<int, String>(); |
+ |
+ CodeInjector(this._code); |
+ |
+ void inject(int offset, String fragment) { |
+ offsetFragmentMap[offset] = fragment; |
+ } |
+ |
+ String getResult() { |
+ var sb = new StringBuffer(); |
+ var lastOffset = 0; |
+ offsetFragmentMap.forEach((offset, fragment) { |
+ sb.write(_code.substring(lastOffset, offset)); |
+ sb.write(fragment); |
+ lastOffset = offset; |
+ }); |
+ sb.write(_code.substring(lastOffset, _code.length)); |
+ return sb.toString(); |
+ } |
+} |