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 |