blob: df37f7baa99d09951a026bf2fbfb6471fbb23483 [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:io";
import "package:logging/logging.dart" as log;
import "package:pathos/path.dart" as po;
import 'package:analyzer_experimental/src/generated/source.dart' show Source;
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;
log.Logger logger = log.Logger.root;
/// Abstract server that listens requests and serves files, may be rewriting them.
abstract class RewriteServer {
String _basePath;
RewriteServer(this._basePath);
void start() {
HttpServer.bind("127.0.0.1", 3445).then((HttpServer server) {
logger.info('RewriteServer is listening at: ${server.port}.');
server.listen((HttpRequest request) {
var response = request.response;
// Prepare path.
var path = _basePath + '/' + request.uri.path;
path = po.normalize(path);
logger.info('[$path] Requested.');
// May be serve using just path.
{
String content = rewritePathContent(path);
if (content != null) {
logger.info('[$path] Request served by path.');
response.write(content);
response.close();
return;
}
}
// Serve from file.
logger.info('[$path] Serving file.');
var file = new File(path);
file.exists().then((bool found) {
if (found) {
logger.finest('[$path] Found file.');
file.readAsString().then((String content) {
logger.finest('[$path] Got file content.');
var sw = new Stopwatch();
sw.start();
try {
content = rewriteFileContent(path, content);
} finally {
sw.stop();
logger.fine('[$path] Rewritten in ${sw.elapsedMilliseconds} ms.');
}
response.write(content);
response.close();
});
} else {
logger.severe('[$path] File not found.');
response.statusCode = HttpStatus.NOT_FOUND;
response.close();
}
});
});
});
}
/// 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);
}
/// Server that rewrites Dart code so that it reports execution of statements and other nodes.
class CoverageServer extends RewriteServer {
CoverageServer(String basePath) : super(basePath);
String rewritePathContent(String path) {
if (path.endsWith('__coverage_impl.dart')) {
String implPath = po.joinAll([
po.dirname(new Options().script),
'..', 'lib', 'src', 'services', 'runtime', 'coverage_lib.dart']);
return new File(implPath).readAsStringSync();
}
return null;
}
String rewriteFileContent(String path, String code) {
if (po.extension(path).toLowerCase() != '.dart') return code;
if (path.contains('packages')) return code;
var unit = _parseCode(code);
var injector = new StringInjector(code);
// Inject coverage library import.
var directives = unit.directives;
if (directives.isNotEmpty && directives[0] is LibraryDirective) {
injector.inject(directives[0].end, 'import "__coverage_impl.dart" as __cc;');
} else {
throw new Exception('Only single library coverage is implemented.');
}
// Insert touch() invocations.
unit.accept(new InsertTouchInvocationsVisitor(injector));
// Done.
code = injector.code;
logger.finest('[$path] Rewritten content\n$code');
return code;
}
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 {
StringInjector injector;
InsertTouchInvocationsVisitor(this.injector);
visitStatement(Statement node) {
super.visitStatement(node);
var offset = node.end;
if (node is Block) {
offset--;
}
if (node is Block && node.parent is BlockFunctionBody) return null;
injector.inject(offset, '__cc.touch(${node.offset});');
return null;
}
}
/// Helper for injecting fragments into some existing [String].
class StringInjector {
String code;
int _lastOffset = -1;
int _delta = 0;
StringInjector(this.code);
void inject(int offset, String fragment) {
if (offset < _lastOffset) {
throw new ArgumentError('Only forward inserts are supported, was $_lastOffset given $offset');
}
_lastOffset = offset;
offset += _delta;
code = code.substring(0, offset) + fragment + code.substring(offset);
_delta += fragment.length;
}
}