| 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 |