blob: c1f39569b539a8b0a2b3f3f2281583892a15bf26 [file] [log] [blame]
// 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_experimental/src/generated/scanner.dart' show StringScanner;
import 'package:analyzer_experimental/src/generated/parser.dart' show Parser;
import 'package:analyzer_experimental/src/generated/ast.dart';
import 'package:analyzer_experimental/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 options = new Options();
var targetArgs = ['http://127.0.0.1:$port/$targetName'];
var dartExecutable = options.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) {
file.fullPath().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),
'..', '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_experimental/');
}
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 scanner = new StringScanner(source, code, 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();
}
}