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