OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library test.runner.browser.server; | 5 library test.runner.browser.server; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:convert'; | 8 import 'dart:convert'; |
9 import 'dart:io'; | 9 import 'dart:io'; |
10 | 10 |
11 import 'package:path/path.dart' as p; | 11 import 'package:path/path.dart' as p; |
12 import 'package:pool/pool.dart'; | 12 import 'package:pool/pool.dart'; |
13 import 'package:shelf/shelf.dart' as shelf; | 13 import 'package:shelf/shelf.dart' as shelf; |
14 import 'package:shelf/shelf_io.dart' as shelf_io; | 14 import 'package:shelf/shelf_io.dart' as shelf_io; |
15 import 'package:shelf_static/shelf_static.dart'; | 15 import 'package:shelf_static/shelf_static.dart'; |
16 import 'package:shelf_web_socket/shelf_web_socket.dart'; | 16 import 'package:shelf_web_socket/shelf_web_socket.dart'; |
17 | 17 |
18 import '../../backend/suite.dart'; | 18 import '../../backend/suite.dart'; |
19 import '../../backend/test_platform.dart'; | 19 import '../../backend/test_platform.dart'; |
20 import '../../util/io.dart'; | 20 import '../../util/io.dart'; |
| 21 import '../../util/path_handler.dart'; |
21 import '../../util/one_off_handler.dart'; | 22 import '../../util/one_off_handler.dart'; |
22 import '../../utils.dart'; | 23 import '../../utils.dart'; |
23 import '../load_exception.dart'; | 24 import '../load_exception.dart'; |
24 import 'browser.dart'; | 25 import 'browser.dart'; |
25 import 'browser_manager.dart'; | 26 import 'browser_manager.dart'; |
26 import 'compiler_pool.dart'; | 27 import 'compiler_pool.dart'; |
27 import 'chrome.dart'; | 28 import 'chrome.dart'; |
28 import 'firefox.dart'; | 29 import 'firefox.dart'; |
29 | 30 |
30 /// A server that serves JS-compiled tests to browsers. | 31 /// A server that serves JS-compiled tests to browsers. |
31 /// | 32 /// |
32 /// A test suite may be loaded for a given file using [loadSuite]. | 33 /// A test suite may be loaded for a given file using [loadSuite]. |
33 class BrowserServer { | 34 class BrowserServer { |
34 /// Starts the server. | 35 /// Starts the server. |
35 /// | 36 /// |
| 37 /// [root] is the root directory that the server should serve. It defaults to |
| 38 /// the working directory. |
| 39 /// |
36 /// If [packageRoot] is passed, it's used for all package imports when | 40 /// If [packageRoot] is passed, it's used for all package imports when |
37 /// compiling tests to JS. Otherwise, the package root is inferred from the | 41 /// compiling tests to JS. Otherwise, the package root is inferred from the |
38 /// location of the source file. | 42 /// location of the source file. |
39 /// | 43 /// |
40 /// If [pubServeUrl] is passed, tests will be loaded from the `pub serve` | 44 /// If [pubServeUrl] is passed, tests will be loaded from the `pub serve` |
41 /// instance at that URL rather than from the filesystem. | 45 /// instance at that URL rather than from the filesystem. |
42 /// | 46 /// |
43 /// If [color] is true, console colors will be used when compiling Dart. | 47 /// If [color] is true, console colors will be used when compiling Dart. |
44 static Future<BrowserServer> start({String packageRoot, Uri pubServeUrl, | 48 static Future<BrowserServer> start({String root, String packageRoot, |
45 bool color: false}) { | 49 Uri pubServeUrl, bool color: false}) { |
46 var server = new BrowserServer._(packageRoot, pubServeUrl, color); | 50 var server = new BrowserServer._(root, packageRoot, pubServeUrl, color); |
47 return server._load().then((_) => server); | 51 return server._load().then((_) => server); |
48 } | 52 } |
49 | 53 |
50 /// The underlying HTTP server. | 54 /// The underlying HTTP server. |
51 HttpServer _server; | 55 HttpServer _server; |
52 | 56 |
| 57 /// A randomly-generated secret. |
| 58 /// |
| 59 /// This is used to ensure that other users on the same system can't snoop |
| 60 /// on data being served through this server. |
| 61 final _secret = randomBase64(24, urlSafe: true); |
| 62 |
53 /// The URL for this server. | 63 /// The URL for this server. |
54 Uri get url => baseUrlForAddress(_server.address, _server.port); | 64 Uri get url => baseUrlForAddress(_server.address, _server.port) |
| 65 .resolve(_secret + "/"); |
55 | 66 |
56 /// a [OneOffHandler] for servicing WebSocket connections for | 67 /// A [OneOffHandler] for servicing WebSocket connections for |
57 /// [BrowserManager]s. | 68 /// [BrowserManager]s. |
58 /// | 69 /// |
59 /// This is one-off because each [BrowserManager] can only connect to a single | 70 /// This is one-off because each [BrowserManager] can only connect to a single |
60 /// WebSocket, | 71 /// WebSocket, |
61 final _webSocketHandler = new OneOffHandler(); | 72 final _webSocketHandler = new OneOffHandler(); |
62 | 73 |
| 74 /// A [PathHandler] used to serve compiled JS. |
| 75 final _jsHandler = new PathHandler(); |
| 76 |
63 /// The [CompilerPool] managing active instances of `dart2js`. | 77 /// The [CompilerPool] managing active instances of `dart2js`. |
64 /// | 78 /// |
65 /// This is `null` if tests are loaded from `pub serve`. | 79 /// This is `null` if tests are loaded from `pub serve`. |
66 final CompilerPool _compilers; | 80 final CompilerPool _compilers; |
67 | 81 |
68 /// The temporary directory in which compiled JS is emitted. | 82 /// The temporary directory in which compiled JS is emitted. |
69 final String _compiledDir; | 83 final String _compiledDir; |
70 | 84 |
| 85 /// The root directory served statically by this server. |
| 86 final String _root; |
| 87 |
71 /// The package root which is passed to `dart2js`. | 88 /// The package root which is passed to `dart2js`. |
72 final String _packageRoot; | 89 final String _packageRoot; |
73 | 90 |
74 /// The URL for the `pub serve` instance to use to load tests. | 91 /// The URL for the `pub serve` instance to use to load tests. |
75 /// | 92 /// |
76 /// This is `null` if tests should be compiled manually. | 93 /// This is `null` if tests should be compiled manually. |
77 final Uri _pubServeUrl; | 94 final Uri _pubServeUrl; |
78 | 95 |
79 /// The pool of active `pub serve` compilations. | 96 /// The pool of active `pub serve` compilations. |
80 /// | 97 /// |
(...skipping 21 matching lines...) Expand all Loading... |
102 /// This should only be accessed through [_browserManagerFor]. | 119 /// This should only be accessed through [_browserManagerFor]. |
103 final _browserManagers = new Map<TestPlatform, Future<BrowserManager>>(); | 120 final _browserManagers = new Map<TestPlatform, Future<BrowserManager>>(); |
104 | 121 |
105 /// A map from test suite paths to Futures that will complete once those | 122 /// A map from test suite paths to Futures that will complete once those |
106 /// suites are finished compiling. | 123 /// suites are finished compiling. |
107 /// | 124 /// |
108 /// This is used to make sure that a given test suite is only compiled once | 125 /// This is used to make sure that a given test suite is only compiled once |
109 /// per run, rather than one per browser per run. | 126 /// per run, rather than one per browser per run. |
110 final _compileFutures = new Map<String, Future>(); | 127 final _compileFutures = new Map<String, Future>(); |
111 | 128 |
112 BrowserServer._(this._packageRoot, Uri pubServeUrl, bool color) | 129 BrowserServer._(String root, this._packageRoot, Uri pubServeUrl, bool color) |
113 : _pubServeUrl = pubServeUrl, | 130 : _root = root == null ? p.current : root, |
| 131 _pubServeUrl = pubServeUrl, |
114 _compiledDir = pubServeUrl == null ? createTempDir() : null, | 132 _compiledDir = pubServeUrl == null ? createTempDir() : null, |
115 _http = pubServeUrl == null ? null : new HttpClient(), | 133 _http = pubServeUrl == null ? null : new HttpClient(), |
116 _compilers = new CompilerPool(color: color); | 134 _compilers = new CompilerPool(color: color); |
117 | 135 |
118 /// Starts the underlying server. | 136 /// Starts the underlying server. |
119 Future _load() { | 137 Future _load() { |
120 var cascade = new shelf.Cascade() | 138 var cascade = new shelf.Cascade() |
121 .add(_webSocketHandler.handler); | 139 .add(_webSocketHandler.handler); |
122 | 140 |
123 if (_pubServeUrl == null) { | 141 if (_pubServeUrl == null) { |
124 var staticPath = p.join(libDir(packageRoot: _packageRoot), | |
125 'src/runner/browser/static'); | |
126 cascade = cascade | 142 cascade = cascade |
127 .add(createStaticHandler(staticPath, defaultDocument: 'index.html')) | 143 .add(_createPackagesHandler()) |
128 .add(createStaticHandler(_compiledDir, | 144 .add(_jsHandler.handler) |
129 defaultDocument: 'index.html')); | 145 .add(_wrapperHandler) |
| 146 .add(createStaticHandler(_root)); |
130 } | 147 } |
131 | 148 |
132 return shelf_io.serve(cascade.handler, 'localhost', 0).then((server) { | 149 var pipeline = new shelf.Pipeline() |
| 150 .addMiddleware(nestingMiddleware(_secret)) |
| 151 .addHandler(cascade.handler); |
| 152 |
| 153 return shelf_io.serve(pipeline, 'localhost', 0).then((server) { |
133 _server = server; | 154 _server = server; |
134 }); | 155 }); |
135 } | 156 } |
136 | 157 |
| 158 /// Returns a handler that serves the contents of the "packages/" directory |
| 159 /// for any URL that contains "packages/". |
| 160 /// |
| 161 /// This is a factory so it can wrap a static handler. |
| 162 shelf.Handler _createPackagesHandler() { |
| 163 var packageRoot = _packageRoot == null |
| 164 ? p.join(_root, 'packages') |
| 165 : _packageRoot; |
| 166 var staticHandler = |
| 167 createStaticHandler(packageRoot, serveFilesOutsidePath: true); |
| 168 |
| 169 return (request) { |
| 170 var segments = p.url.split(shelfUrl(request).path); |
| 171 |
| 172 for (var i = 0; i < segments.length; i++) { |
| 173 if (segments[i] != "packages") continue; |
| 174 return staticHandler( |
| 175 shelfChange(request, path: p.url.joinAll(segments.take(i + 1)))); |
| 176 } |
| 177 |
| 178 return new shelf.Response.notFound("Not found."); |
| 179 }; |
| 180 } |
| 181 |
| 182 /// A handler that serves wrapper HTML to bootstrap tests. |
| 183 shelf.Response _wrapperHandler(shelf.Request request) { |
| 184 var path = p.fromUri(shelfUrl(request)); |
| 185 var withoutExtensions = p.withoutExtension(p.withoutExtension(path)); |
| 186 var base = p.basename(withoutExtensions); |
| 187 |
| 188 if (path.endsWith(".browser_test.html")) { |
| 189 // TODO(nweiz): support user-authored HTML files. |
| 190 return new shelf.Response.ok(''' |
| 191 <!DOCTYPE html> |
| 192 <html> |
| 193 <head> |
| 194 <title>${HTML_ESCAPE.convert(base)}.dart Test</title> |
| 195 <script type="application/javascript" |
| 196 src="${HTML_ESCAPE.convert(base)}.browser_test.dart.js"> |
| 197 </script> |
| 198 </head> |
| 199 </html> |
| 200 ''', headers: {'Content-Type': 'text/html'}); |
| 201 } |
| 202 |
| 203 return new shelf.Response.notFound('Not found.'); |
| 204 } |
| 205 |
137 /// Loads the test suite at [path] on the browser [browser]. | 206 /// Loads the test suite at [path] on the browser [browser]. |
138 /// | 207 /// |
139 /// This will start a browser to load the suite if one isn't already running. | 208 /// This will start a browser to load the suite if one isn't already running. |
140 /// Throws an [ArgumentError] if [browser] isn't a browser platform. | 209 /// Throws an [ArgumentError] if [browser] isn't a browser platform. |
141 Future<Suite> loadSuite(String path, TestPlatform browser) { | 210 Future<Suite> loadSuite(String path, TestPlatform browser) { |
142 if (!browser.isBrowser) { | 211 if (!browser.isBrowser) { |
143 throw new ArgumentError("$browser is not a browser."); | 212 throw new ArgumentError("$browser is not a browser."); |
144 } | 213 } |
145 | 214 |
146 return new Future.sync(() { | 215 return new Future.sync(() { |
147 if (_pubServeUrl != null) { | 216 if (_pubServeUrl != null) { |
148 var suitePrefix = p.withoutExtension(p.relative(path, from: 'test')) + | 217 var suitePrefix = p.relative(path, from: p.join(_root, 'test')) + |
149 '.browser_test'; | 218 '.browser_test'; |
150 var jsUrl = _pubServeUrl.resolve('$suitePrefix.dart.js'); | 219 var jsUrl = _pubServeUrl.resolve('$suitePrefix.dart.js'); |
151 return _pubServeSuite(path, jsUrl) | 220 return _pubServeSuite(path, jsUrl).then((_) => |
152 .then((_) => _pubServeUrl.resolve('$suitePrefix.html')); | 221 _pubServeUrl.resolve('$suitePrefix.html')); |
153 } else { | 222 } |
154 return _compileSuite(path).then((dir) { | |
155 if (_closed) return null; | |
156 | 223 |
157 // Add a trailing slash because at least on Chrome, the iframe's | 224 return _compileSuite(path).then((_) { |
158 // window.location.href will do so automatically, and if that differs | 225 if (_closed) return null; |
159 // from the original URL communication will fail. | 226 return url.resolveUri( |
160 return url.resolve( | 227 p.toUri(p.relative(path, from: _root) + ".browser_test.html")); |
161 "/" + p.toUri(p.relative(dir, from: _compiledDir)).path + "/"); | 228 }); |
162 }); | |
163 } | |
164 }).then((suiteUrl) { | 229 }).then((suiteUrl) { |
165 if (_closed) return null; | 230 if (_closed) return null; |
166 | 231 |
167 // TODO(nweiz): Don't start the browser until all the suites are compiled. | 232 // TODO(nweiz): Don't start the browser until all the suites are compiled. |
168 return _browserManagerFor(browser).then((browserManager) { | 233 return _browserManagerFor(browser).then((browserManager) { |
169 if (_closed) return null; | 234 if (_closed) return null; |
170 return browserManager.loadSuite(path, suiteUrl); | 235 return browserManager.loadSuite(path, suiteUrl); |
171 }); | 236 }); |
172 }); | 237 }); |
173 } | 238 } |
(...skipping 29 matching lines...) Expand all Loading... |
203 throw new LoadException(path, | 268 throw new LoadException(path, |
204 "Error getting $jsUrl: ${response.statusCode} " | 269 "Error getting $jsUrl: ${response.statusCode} " |
205 "${response.reasonPhrase}\n" | 270 "${response.reasonPhrase}\n" |
206 'Make sure "pub serve" is serving the test/ directory.'); | 271 'Make sure "pub serve" is serving the test/ directory.'); |
207 }); | 272 }); |
208 }); | 273 }); |
209 } | 274 } |
210 | 275 |
211 /// Compile the test suite at [dartPath] to JavaScript. | 276 /// Compile the test suite at [dartPath] to JavaScript. |
212 /// | 277 /// |
213 /// Returns a [Future] that completes to the path to the JavaScript. | 278 /// Once the suite has been compiled, it's added to [_jsHandler] so it can be |
214 Future<String> _compileSuite(String dartPath) { | 279 /// served. |
| 280 Future _compileSuite(String dartPath) { |
215 return _compileFutures.putIfAbsent(dartPath, () { | 281 return _compileFutures.putIfAbsent(dartPath, () { |
216 var dir = new Directory(_compiledDir).createTempSync('test_').path; | 282 var dir = new Directory(_compiledDir).createTempSync('test_').path; |
217 var jsPath = p.join(dir, p.basename(dartPath) + ".js"); | 283 var jsPath = p.join(dir, p.basename(dartPath) + ".js"); |
| 284 |
218 return _compilers.compile(dartPath, jsPath, | 285 return _compilers.compile(dartPath, jsPath, |
219 packageRoot: packageRootFor(dartPath, _packageRoot)) | 286 packageRoot: packageRootFor(dartPath, _packageRoot)) |
220 .then((_) { | 287 .then((_) { |
221 if (_closed) return null; | 288 if (_closed) return; |
222 | 289 |
223 // TODO(nweiz): support user-authored HTML files. | 290 _jsHandler.add( |
224 new File(p.join(dir, "index.html")).writeAsStringSync(''' | 291 p.relative(dartPath, from: _root) + '.browser_test.dart.js', |
225 <!DOCTYPE html> | 292 (request) { |
226 <html> | 293 return new shelf.Response.ok(new File(jsPath).readAsStringSync(), |
227 <head> | 294 headers: {'Content-Type': 'application/javascript'}); |
228 <title>${HTML_ESCAPE.convert(dartPath)} Test</title> | 295 }); |
229 <script src="${HTML_ESCAPE.convert(p.basename(jsPath))}"></script> | |
230 </head> | |
231 </html> | |
232 '''); | |
233 return dir; | |
234 }); | 296 }); |
235 }); | 297 }); |
236 } | 298 } |
237 | 299 |
238 /// Returns the [BrowserManager] for [platform], which should be a browser. | 300 /// Returns the [BrowserManager] for [platform], which should be a browser. |
239 /// | 301 /// |
240 /// If no browser manager is running yet, starts one. | 302 /// If no browser manager is running yet, starts one. |
241 Future<BrowserManager> _browserManagerFor(TestPlatform platform) { | 303 Future<BrowserManager> _browserManagerFor(TestPlatform platform) { |
242 var manager = _browserManagers[platform]; | 304 var manager = _browserManagers[platform]; |
243 if (manager != null) return manager; | 305 if (manager != null) return manager; |
244 | 306 |
245 var completer = new Completer(); | 307 var completer = new Completer(); |
246 _browserManagers[platform] = completer.future; | 308 _browserManagers[platform] = completer.future; |
247 var path = _webSocketHandler.create(webSocketHandler((webSocket) { | 309 var path = _webSocketHandler.create(webSocketHandler((webSocket) { |
248 completer.complete(new BrowserManager(webSocket)); | 310 completer.complete(new BrowserManager(webSocket)); |
249 })); | 311 })); |
250 | 312 |
251 var webSocketUrl = url.replace(scheme: 'ws', path: '/$path'); | 313 var webSocketUrl = url.replace(scheme: 'ws').resolve(path); |
252 | 314 |
253 var hostUrl = url; | 315 var hostUrl = (_pubServeUrl == null ? url : _pubServeUrl) |
254 if (_pubServeUrl != null) { | 316 .resolve('packages/test/src/runner/browser/static/index.html'); |
255 hostUrl = _pubServeUrl.resolve( | |
256 '/packages/test/src/runner/browser/static/'); | |
257 } | |
258 | 317 |
259 var browser = _newBrowser(hostUrl.replace(queryParameters: { | 318 var browser = _newBrowser(hostUrl.replace(queryParameters: { |
260 'managerUrl': webSocketUrl.toString() | 319 'managerUrl': webSocketUrl.toString() |
261 }), platform); | 320 }), platform); |
262 _browsers[platform] = browser; | 321 _browsers[platform] = browser; |
263 | 322 |
264 // TODO(nweiz): Gracefully handle the browser being killed before the | 323 // TODO(nweiz): Gracefully handle the browser being killed before the |
265 // tests complete. | 324 // tests complete. |
266 browser.onExit.catchError((error, stackTrace) { | 325 browser.onExit.catchError((error, stackTrace) { |
267 if (completer.isCompleted) return; | 326 if (completer.isCompleted) return; |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
302 if (_pubServeUrl == null) { | 361 if (_pubServeUrl == null) { |
303 new Directory(_compiledDir).deleteSync(recursive: true); | 362 new Directory(_compiledDir).deleteSync(recursive: true); |
304 } else { | 363 } else { |
305 _http.close(); | 364 _http.close(); |
306 } | 365 } |
307 | 366 |
308 _closeCompleter.complete(); | 367 _closeCompleter.complete(); |
309 }).catchError(_closeCompleter.completeError); | 368 }).catchError(_closeCompleter.completeError); |
310 } | 369 } |
311 } | 370 } |
OLD | NEW |