Chromium Code Reviews| Index: pkg/analyzer_experimental/lib/src/services/runtime/coverage_server_impl.dart |
| diff --git a/pkg/analyzer_experimental/lib/src/services/runtime/coverage_server_impl.dart b/pkg/analyzer_experimental/lib/src/services/runtime/coverage_server_impl.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..87b9191f80e6e5631b99e2e4ba87b8c3db7eb92f |
| --- /dev/null |
| +++ b/pkg/analyzer_experimental/lib/src/services/runtime/coverage_server_impl.dart |
| @@ -0,0 +1,154 @@ |
| +/// A library for code coverage support for Dart. |
|
pquitslund
2013/06/11 17:18:56
Again. Probably want a header.
scheglov
2013/06/11 19:54:23
Done.
|
| +library runtime.coverage.server_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 |
|
pquitslund
2013/06/11 17:18:56
prepare -> Prepare (upper case)
scheglov
2013/06/11 19:54:23
Done.
|
| + String path = _basePath + '/' + request.uri.path; |
|
pquitslund
2013/06/11 17:18:56
var path
scheglov
2013/06/11 19:54:23
Done.
|
| + path = po.normalize(path); |
| + logger.info('[$path] Requested.'); |
| + // may be we have path content |
|
pquitslund
2013/06/11 17:18:56
Upper case 'may'
scheglov
2013/06/11 19:54:23
Done.
|
| + { |
| + String content = rewritePathContent(path); |
| + if (content != null) { |
| + logger.info('[$path] Request served by path.'); |
| + response.write(content); |
| + response.close(); |
| + return; |
| + } |
| + } |
| + // get content from file |
|
pquitslund
2013/06/11 17:18:56
More upper case (and below). Pretty sure the styl
scheglov
2013/06/11 19:54:23
Done.
|
| + logger.info('[$path] Serving file.'); |
| + File file = new File(path); |
|
pquitslund
2013/06/11 17:18:56
var file
scheglov
2013/06/11 19:54:23
Done.
|
| + 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; |
| + CompilationUnit unit = _parseCode(code); |
|
pquitslund
2013/06/11 17:18:56
Again, drop the type annotation (var unit).
scheglov
2013/06/11 19:54:23
Done.
|
| + print(unit); |
| + var injector = new StringInjector(code); |
| + // inject coverage_lib.dart 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) { |
| + Source source = null; |
|
pquitslund
2013/06/11 17:18:56
And here.
scheglov
2013/06/11 19:54:23
Done.
|
| + 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); |
| + int offset = node.end; |
|
pquitslund
2013/06/11 17:18:56
And here.
scheglov
2013/06/11 19:54:23
Done.
|
| + 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; |
| + } |
| +} |