| // Copyright (c) 2015, 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 tool to gather coverage data from an app generated with dart2js. This |
| /// depends on code that has been landed in the bleeding_edge version of dart2js |
| /// and that we expect to become publicly visible in version 0.13.0 of the Dart |
| /// SDK). |
| /// |
| /// This tool starts a server that answers to mainly 2 requests: |
| /// * a GET request to retrieve the application |
| /// * POST requests to record coverage data. |
| /// |
| /// It is intended to be used as follows: |
| /// * generate an app by running dart2js with the environment value |
| /// -DtraceCalls=post provided to the vm, and the --dump-info |
| /// flag provided to dart2js. |
| /// * start this server, and proxy requests from your normal frontend |
| /// server to this one. |
| library dart2js_info.bin.coverage_log_server; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:args/command_runner.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:shelf/shelf.dart' as shelf; |
| import 'package:shelf/shelf_io.dart' as shelf; |
| |
| import 'usage_exception.dart'; |
| |
| class CoverageLogServerCommand extends Command<void> with PrintUsageException { |
| @override |
| final String name = 'coverage_server'; |
| @override |
| final String description = 'Server to gather code coverage data'; |
| |
| CoverageLogServerCommand() { |
| argParser |
| ..addOption('port', abbr: 'p', help: 'port number', defaultsTo: "8080") |
| ..addOption('host', |
| help: 'host name (use 0.0.0.0 for all interfaces)', |
| defaultsTo: 'localhost') |
| ..addOption('uri-prefix', |
| help: |
| 'uri path prefix that will hit this server. This will be injected' |
| ' into the .js file', |
| defaultsTo: '') |
| ..addOption('out', |
| abbr: 'o', help: 'output log file', defaultsTo: _defaultOutTemplate); |
| } |
| |
| @override |
| void run() async { |
| final args = argResults!; |
| if (args.rest.isEmpty) { |
| usageException('Missing arguments: <dart2js-out-file> [<html-file>]'); |
| } |
| |
| var jsPath = args.rest[0]; |
| String? htmlPath; |
| if (args.rest.length > 1) { |
| htmlPath = args.rest[1]; |
| } |
| var outPath = args['out']; |
| if (outPath == _defaultOutTemplate) outPath = '$jsPath.coverage.json'; |
| var server = _Server(args['host'], int.parse(args['port']), jsPath, |
| htmlPath, outPath, args['uri-prefix']); |
| await server.run(); |
| } |
| } |
| |
| const _defaultOutTemplate = '<dart2js-out-file>.coverage.json'; |
| |
| class _Server { |
| /// Server hostname, typically `localhost`, but can be `0.0.0.0`. |
| final String hostname; |
| |
| /// Port the server will listen to. |
| final int port; |
| |
| /// JS file (previously generated by dart2js) to serve. |
| final String jsPath; |
| |
| /// HTML file to serve, if any. |
| final String? htmlPath; |
| |
| /// Contents of jsPath, adjusted to use the appropriate server url. |
| String jsCode; |
| |
| /// Location where we'll dump the coverage data. |
| final String outPath; |
| |
| /// Uri prefix used on all requests to this server. This will be injected into |
| /// the .js file. |
| final String prefix; |
| |
| // TODO(sigmund): add support to load also simple HTML files to test small |
| // simple apps. |
| |
| /// Data received so far. The data is just an array of pairs, showing the |
| /// hashCode and name of the element used. This can be later cross-checked |
| /// against dump-info data. |
| Map data = {}; |
| |
| String get _serializedData => JsonEncoder.withIndent(' ').convert(data); |
| |
| _Server(this.hostname, this.port, this.jsPath, this.htmlPath, this.outPath, |
| String prefix) |
| : jsCode = _adjustRequestUrl(File(jsPath).readAsStringSync(), prefix), |
| prefix = _normalize(prefix); |
| |
| run() async { |
| await shelf.serve(_handler, hostname, port); |
| var urlBase = "http://$hostname:$port${prefix == '' ? '/' : '/$prefix/'}"; |
| var htmlFilename = htmlPath == null ? '' : path.basename(htmlPath!); |
| print("Server is listening\n" |
| " - html page: $urlBase$htmlFilename\n" |
| " - js code: $urlBase${path.basename(jsPath)}\n" |
| " - coverage reporting: ${urlBase}coverage\n"); |
| } |
| |
| _expectedPath(String tail) => prefix == '' ? tail : '$prefix/$tail'; |
| |
| FutureOr<shelf.Response> _handler(shelf.Request request) async { |
| var urlPath = request.url.path; |
| print('received request: $urlPath'); |
| var baseJsName = path.basename(jsPath); |
| var baseHtmlName = htmlPath == null ? '' : path.basename(htmlPath!); |
| |
| // Serve an HTML file at the default prefix, or a path matching the HTML |
| // file name |
| if (urlPath == prefix || |
| urlPath == '$prefix/' || |
| urlPath == _expectedPath(baseHtmlName)) { |
| var contents = htmlPath == null |
| ? '<html><script src="$baseJsName"></script>' |
| : await File(htmlPath!).readAsString(); |
| return shelf.Response.ok(contents, headers: _htmlHeaders); |
| } |
| |
| if (urlPath == _expectedPath(baseJsName)) { |
| return shelf.Response.ok(jsCode, headers: _jsHeaders); |
| } |
| |
| // Handle POST requests to record coverage data, and GET requests to display |
| // the currently coverage results. |
| if (urlPath == _expectedPath('coverage')) { |
| if (request.method == 'GET') { |
| return shelf.Response.ok(_serializedData, headers: _textHeaders); |
| } |
| |
| if (request.method == 'POST') { |
| _record(jsonDecode(await request.readAsString())); |
| return shelf.Response.ok("Thanks!"); |
| } |
| } |
| |
| // Any other request is not supported. |
| return shelf.Response.notFound('Not found: "$urlPath"'); |
| } |
| |
| _record(List entries) { |
| for (var entry in entries) { |
| var id = entry[0]; |
| data.putIfAbsent('$id', () => {'name': entry[1], 'count': 0}); |
| data['$id']['count']++; |
| } |
| _enqueueSave(); |
| } |
| |
| bool _savePending = false; |
| int _total = 0; |
| _enqueueSave() async { |
| if (!_savePending) { |
| _savePending = true; |
| await Future.delayed(Duration(seconds: 3)); |
| await File(outPath).writeAsString(_serializedData); |
| var diff = data.length - _total; |
| print(diff == 0 |
| ? ' - no new element covered' |
| : ' - $diff new elements covered'); |
| _savePending = false; |
| _total = data.length; |
| } |
| } |
| } |
| |
| /// Removes leading and trailing slashes of [uriPath]. |
| _normalize(String uriPath) { |
| if (uriPath.startsWith('/')) uriPath = uriPath.substring(1); |
| if (uriPath.endsWith('/')) uriPath = uriPath.substring(0, uriPath.length - 1); |
| return uriPath; |
| } |
| |
| _adjustRequestUrl(String code, String prefix) { |
| var url = prefix == '' ? 'coverage' : '$prefix/coverage'; |
| var hook = ''' |
| self.dartCallInstrumentation = function(id, name) { |
| if (!this.traceBuffer) { |
| this.traceBuffer = []; |
| } |
| var buffer = this.traceBuffer; |
| if (buffer.length == 0) { |
| window.setTimeout(function() { |
| var xhr = new XMLHttpRequest(); |
| xhr.open("POST", "/$url"); |
| xhr.send(JSON.stringify(buffer)); |
| buffer.length = 0; |
| }, 1000); |
| } |
| buffer.push([id, name]); |
| }; |
| '''; |
| return '$hook$code'; |
| } |
| |
| const _htmlHeaders = {'content-type': 'text/html'}; |
| const _jsHeaders = {'content-type': 'text/javascript'}; |
| const _textHeaders = {'content-type': 'text/plain'}; |