OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// A tool to gather coverage data from an app generated with dart2js. |
| 6 /// This tool starts a server that answers to mainly 2 requests: |
| 7 /// * a GET request to retrieve the application |
| 8 /// * POST requests to record coverage data. |
| 9 /// |
| 10 /// It is intended to be used as follows: |
| 11 /// * generate an app by running dart2js with the environment boolean |
| 12 /// -DinstrumentForCoverage=true provided to the vm, and the --dump-info |
| 13 /// flag provided to dart2js. |
| 14 /// * start this server, and proxy requests from your normal frontend |
| 15 /// server to this one. |
| 16 library compiler.tool.coverage_log_server; |
| 17 |
| 18 import 'dart:convert'; |
| 19 import 'dart:io'; |
| 20 import 'dart:async'; |
| 21 import 'package:path/path.dart' as path; |
| 22 import 'package:args/args.dart'; |
| 23 import 'package:shelf/shelf.dart' as shelf; |
| 24 import 'package:shelf/shelf_io.dart' as shelf; |
| 25 |
| 26 const _DEFAULT_OUT_TEMPLATE = '<dart2js-out-file>.coverage.json'; |
| 27 |
| 28 main(argv) async { |
| 29 var parser = new ArgParser() |
| 30 ..addOption('port', abbr: 'p', help: 'port number', defaultsTo: "8080") |
| 31 ..addOption('host', help: 'host name (use 0.0.0.0 for all interfaces)', |
| 32 defaultsTo: 'localhost') |
| 33 ..addFlag('help', abbr: 'h', help: 'show this help message', |
| 34 negatable: false) |
| 35 ..addOption('uri-prefix', |
| 36 help: 'uri path prefix that will hit this server. This will be injected' |
| 37 ' into the .js file', |
| 38 defaultsTo: '') |
| 39 ..addOption('out', abbr: 'o', help: 'output log file', |
| 40 defaultsTo: _DEFAULT_OUT_TEMPLATE); |
| 41 var args = parser.parse(argv); |
| 42 if (args['help'] == true || args.rest.isEmpty) { |
| 43 print('usage: dart coverage_logging.dart [options] ' |
| 44 '<dart2js-out-file> [<html-file>]'); |
| 45 print(parser.usage); |
| 46 exit(1); |
| 47 } |
| 48 |
| 49 var jsPath = args.rest[0]; |
| 50 var htmlPath = null; |
| 51 if (args.rest.length > 1) { |
| 52 htmlPath = args.rest[1]; |
| 53 } |
| 54 var outPath = args['out']; |
| 55 if (outPath == _DEFAULT_OUT_TEMPLATE) outPath = '$jsPath.coverage.json'; |
| 56 var server = new _Server(args['host'], int.parse(args['port']), jsPath, |
| 57 htmlPath, outPath, args['uri-prefix']); |
| 58 await server.run(); |
| 59 } |
| 60 |
| 61 class _Server { |
| 62 /// Server hostname, typically `localhost`, but can be `0.0.0.0`. |
| 63 final String hostname; |
| 64 |
| 65 /// Port the server will listen to. |
| 66 final int port; |
| 67 |
| 68 /// JS file (previously generated by dart2js) to serve. |
| 69 final String jsPath; |
| 70 |
| 71 /// HTML file to serve, if any. |
| 72 final String htmlPath; |
| 73 |
| 74 /// Contents of jsPath, adjusted to use the appropriate server url. |
| 75 String jsCode; |
| 76 |
| 77 /// Location where we'll dump the coverage data. |
| 78 final String outPath; |
| 79 |
| 80 /// Uri prefix used on all requests to this server. This will be injected into |
| 81 /// the .js file. |
| 82 final String prefix; |
| 83 |
| 84 // TODO(sigmund): add support to load also simple HTML files to test small |
| 85 // simple apps. |
| 86 |
| 87 /// Data received so far. The data is just an array of pairs, showing the |
| 88 /// hashCode and name of the element used. This can be later cross-checked |
| 89 /// against dump-info data. |
| 90 Map data = {}; |
| 91 |
| 92 String get _serializedData => new JsonEncoder.withIndent(' ').convert(data); |
| 93 |
| 94 _Server(this.hostname, this.port, String jsPath, this.htmlPath, |
| 95 this.outPath, String prefix) |
| 96 : jsPath = jsPath, |
| 97 jsCode = _adjustRequestUrl(new File(jsPath).readAsStringSync(), prefix), |
| 98 prefix = _normalize(prefix); |
| 99 |
| 100 run() async { |
| 101 await shelf.serve(_handler, hostname, port); |
| 102 var urlBase = "http://$hostname:$port${prefix == '' ? '/' : '/$prefix/'}"; |
| 103 var htmlFilename = htmlPath == null ? '' : path.basename(htmlPath); |
| 104 print("Server is listening\n" |
| 105 " - html page: $urlBase$htmlFilename\n" |
| 106 " - js code: $urlBase${path.basename(jsPath)}\n" |
| 107 " - coverage reporting: ${urlBase}coverage\n"); |
| 108 } |
| 109 |
| 110 _expectedPath(String tail) => prefix == '' ? tail : '$prefix/$tail'; |
| 111 |
| 112 _handler(shelf.Request request) async { |
| 113 var urlPath = request.url.path; |
| 114 print('received request: $urlPath'); |
| 115 var baseJsName = path.basename(jsPath); |
| 116 var baseHtmlName = htmlPath == null ? '' : path.basename(htmlPath); |
| 117 |
| 118 // Serve an HTML file at the default prefix, or a path matching the HTML |
| 119 // file name |
| 120 if (urlPath == prefix || urlPath == '$prefix/' |
| 121 || urlPath == _expectedPath(baseHtmlName)) { |
| 122 var contents = htmlPath == null |
| 123 ? '<html><script src="$baseJsName"></script>' |
| 124 : await new File(htmlPath).readAsString(); |
| 125 return new shelf.Response.ok(contents, headers: HTML_HEADERS); |
| 126 } |
| 127 |
| 128 if (urlPath == _expectedPath(baseJsName)) { |
| 129 return new shelf.Response.ok(jsCode, headers: JS_HEADERS); |
| 130 } |
| 131 |
| 132 // Handle POST requests to record coverage data, and GET requests to display |
| 133 // the currently coverage resutls. |
| 134 if (urlPath == _expectedPath('coverage')) { |
| 135 if (request.method == 'GET') { |
| 136 return new shelf.Response.ok(_serializedData, headers: TEXT_HEADERS); |
| 137 } |
| 138 |
| 139 if (request.method == 'POST') { |
| 140 _record(JSON.decode(await request.readAsString())); |
| 141 return new shelf.Response.ok("Thanks!"); |
| 142 } |
| 143 } |
| 144 |
| 145 // Any other request is not supported. |
| 146 return new shelf.Response.notFound('Not found: "$urlPath"'); |
| 147 } |
| 148 |
| 149 _record(List entries) { |
| 150 for (var entry in entries) { |
| 151 var id = entry[0]; |
| 152 data.putIfAbsent('$id', () => {'name': entry[1], 'count': 0}); |
| 153 data['$id']['count']++; |
| 154 } |
| 155 _enqueueSave(); |
| 156 } |
| 157 |
| 158 bool _savePending = false; |
| 159 int _total = 0; |
| 160 _enqueueSave() async { |
| 161 if (!_savePending) { |
| 162 _savePending = true; |
| 163 await new Future.delayed(new Duration(seconds: 3)); |
| 164 await new File(outPath).writeAsString(_serializedData); |
| 165 var diff = data.length - _total; |
| 166 print(diff ? ' - no new element covered' |
| 167 : ' - $diff new elements covered'); |
| 168 _savePending = false; |
| 169 _total = data.length; |
| 170 } |
| 171 } |
| 172 } |
| 173 |
| 174 /// Removes leading and trailing slashes of [uriPath]. |
| 175 _normalize(String uriPath) { |
| 176 if (uriPath.startsWith('/')) uriPath = uriPath.substring(1); |
| 177 if (uriPath.endsWith('/')) uriPath = uriPath.substring(0, uriPath.length - 1); |
| 178 return uriPath; |
| 179 } |
| 180 |
| 181 _adjustRequestUrl(String code, String prefix) { |
| 182 var newUrl = prefix == '' ? 'coverage' : '$prefix/coverage'; |
| 183 return code.replaceFirst( |
| 184 '"/coverage_uri_to_amend_by_server"', |
| 185 '"/$newUrl" /*url-prefix updated!*/'); |
| 186 } |
| 187 |
| 188 const HTML_HEADERS = const {'content-type': 'text/html'}; |
| 189 const JS_HEADERS = const {'content-type': 'text/javascript'}; |
| 190 const TEXT_HEADERS = const {'content-type': 'text/plain'}; |
OLD | NEW |