| 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();
|
| + }
|
| +}
|
|
|