| 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 '../../util/io.dart'; | 20 import '../../util/io.dart'; |
| 20 import '../../util/one_off_handler.dart'; | 21 import '../../util/one_off_handler.dart'; |
| 21 import '../../utils.dart'; | 22 import '../../utils.dart'; |
| 22 import '../load_exception.dart'; | 23 import '../load_exception.dart'; |
| 24 import 'browser.dart'; |
| 23 import 'browser_manager.dart'; | 25 import 'browser_manager.dart'; |
| 24 import 'compiler_pool.dart'; | 26 import 'compiler_pool.dart'; |
| 25 import 'chrome.dart'; | 27 import 'chrome.dart'; |
| 28 import 'firefox.dart'; |
| 26 | 29 |
| 27 /// A server that serves JS-compiled tests to browsers. | 30 /// A server that serves JS-compiled tests to browsers. |
| 28 /// | 31 /// |
| 29 /// A test suite may be loaded for a given file using [loadSuite]. | 32 /// A test suite may be loaded for a given file using [loadSuite]. |
| 30 class BrowserServer { | 33 class BrowserServer { |
| 31 /// Starts the server. | 34 /// Starts the server. |
| 32 /// | 35 /// |
| 33 /// If [packageRoot] is passed, it's used for all package imports when | 36 /// If [packageRoot] is passed, it's used for all package imports when |
| 34 /// compiling tests to JS. Otherwise, the package root is inferred from the | 37 /// compiling tests to JS. Otherwise, the package root is inferred from the |
| 35 /// location of the source file. | 38 /// location of the source file. |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 75 | 78 |
| 76 /// The pool of active `pub serve` compilations. | 79 /// The pool of active `pub serve` compilations. |
| 77 /// | 80 /// |
| 78 /// Pub itself ensures that only one compilation runs at a time; we just use | 81 /// Pub itself ensures that only one compilation runs at a time; we just use |
| 79 /// this pool to make sure that the output is nice and linear. | 82 /// this pool to make sure that the output is nice and linear. |
| 80 final _pubServePool = new Pool(1); | 83 final _pubServePool = new Pool(1); |
| 81 | 84 |
| 82 /// The HTTP client to use when caching JS files in `pub serve`. | 85 /// The HTTP client to use when caching JS files in `pub serve`. |
| 83 final HttpClient _http; | 86 final HttpClient _http; |
| 84 | 87 |
| 85 /// The browser in which test suites are loaded and run. | |
| 86 /// | |
| 87 /// This is `null` until a suite is loaded. | |
| 88 Chrome _browser; | |
| 89 | |
| 90 /// Whether [close] has been called. | 88 /// Whether [close] has been called. |
| 91 bool get _closed => _closeCompleter != null; | 89 bool get _closed => _closeCompleter != null; |
| 92 | 90 |
| 93 /// The completer for the [Future] returned by [close]. | 91 /// The completer for the [Future] returned by [close]. |
| 94 Completer _closeCompleter; | 92 Completer _closeCompleter; |
| 95 | 93 |
| 96 /// A future that will complete to the [BrowserManager] for [_browser]. | 94 /// All currently-running browsers. |
| 97 /// | 95 /// |
| 98 /// The first time this is called, it will start both the browser and the | 96 /// These are controlled by [_browserManager]s. |
| 99 /// browser manager. Any further calls will return the existing manager. | 97 final _browsers = new Map<TestPlatform, Browser>(); |
| 100 Future<BrowserManager> get _browserManager { | |
| 101 if (_browserManagerCompleter == null) { | |
| 102 _browserManagerCompleter = new Completer(); | |
| 103 var path = _webSocketHandler.create(webSocketHandler((webSocket) { | |
| 104 _browserManagerCompleter.complete(new BrowserManager(webSocket)); | |
| 105 })); | |
| 106 | 98 |
| 107 var webSocketUrl = url.replace(scheme: 'ws', path: '/$path'); | 99 /// A map from browser identifiers to futures that will complete to the |
| 100 /// [BrowserManager]s for those browsers. |
| 101 /// |
| 102 /// This should only be accessed through [_browserManagerFor]. |
| 103 final _browserManagers = new Map<TestPlatform, Future<BrowserManager>>(); |
| 108 | 104 |
| 109 var hostUrl = url; | 105 /// A map from test suite paths to Futures that will complete once those |
| 110 if (_pubServeUrl != null) { | 106 /// suites are finished compiling. |
| 111 hostUrl = _pubServeUrl.resolve( | 107 /// |
| 112 '/packages/test/src/runner/browser/static/'); | 108 /// This is used to make sure that a given test suite is only compiled once |
| 113 } | 109 /// per run, rather than one per browser per run. |
| 114 | 110 final _compileFutures = new Map<String, Future>(); |
| 115 _browser = new Chrome(hostUrl.replace(queryParameters: { | |
| 116 'managerUrl': webSocketUrl.toString() | |
| 117 })); | |
| 118 | |
| 119 // TODO(nweiz): Gracefully handle the browser being killed before the | |
| 120 // tests complete. | |
| 121 _browser.onExit.catchError((error, stackTrace) { | |
| 122 if (_browserManagerCompleter.isCompleted) return; | |
| 123 _browserManagerCompleter.completeError(error, stackTrace); | |
| 124 }); | |
| 125 } | |
| 126 return _browserManagerCompleter.future; | |
| 127 } | |
| 128 Completer<BrowserManager> _browserManagerCompleter; | |
| 129 | 111 |
| 130 BrowserServer._(this._packageRoot, Uri pubServeUrl, bool color) | 112 BrowserServer._(this._packageRoot, Uri pubServeUrl, bool color) |
| 131 : _pubServeUrl = pubServeUrl, | 113 : _pubServeUrl = pubServeUrl, |
| 132 _compiledDir = pubServeUrl == null ? createTempDir() : null, | 114 _compiledDir = pubServeUrl == null ? createTempDir() : null, |
| 133 _http = pubServeUrl == null ? null : new HttpClient(), | 115 _http = pubServeUrl == null ? null : new HttpClient(), |
| 134 _compilers = new CompilerPool(color: color); | 116 _compilers = new CompilerPool(color: color); |
| 135 | 117 |
| 136 /// Starts the underlying server. | 118 /// Starts the underlying server. |
| 137 Future _load() { | 119 Future _load() { |
| 138 var cascade = new shelf.Cascade() | 120 var cascade = new shelf.Cascade() |
| 139 .add(_webSocketHandler.handler); | 121 .add(_webSocketHandler.handler); |
| 140 | 122 |
| 141 if (_pubServeUrl == null) { | 123 if (_pubServeUrl == null) { |
| 142 var staticPath = p.join(libDir(packageRoot: _packageRoot), | 124 var staticPath = p.join(libDir(packageRoot: _packageRoot), |
| 143 'src/runner/browser/static'); | 125 'src/runner/browser/static'); |
| 144 cascade = cascade | 126 cascade = cascade |
| 145 .add(createStaticHandler(staticPath, defaultDocument: 'index.html')) | 127 .add(createStaticHandler(staticPath, defaultDocument: 'index.html')) |
| 146 .add(createStaticHandler(_compiledDir, | 128 .add(createStaticHandler(_compiledDir, |
| 147 defaultDocument: 'index.html')); | 129 defaultDocument: 'index.html')); |
| 148 } | 130 } |
| 149 | 131 |
| 150 return shelf_io.serve(cascade.handler, 'localhost', 0).then((server) { | 132 return shelf_io.serve(cascade.handler, 'localhost', 0).then((server) { |
| 151 _server = server; | 133 _server = server; |
| 152 }); | 134 }); |
| 153 } | 135 } |
| 154 | 136 |
| 155 /// Loads the test suite at [path]. | 137 /// Loads the test suite at [path] on the browser [browser]. |
| 156 /// | 138 /// |
| 157 /// This will start a browser to load the suite if one isn't already running. | 139 /// This will start a browser to load the suite if one isn't already running. |
| 158 Future<Suite> loadSuite(String path) { | 140 /// Throws an [ArgumentError] if [browser] isn't a browser platform. |
| 141 Future<Suite> loadSuite(String path, TestPlatform browser) { |
| 142 if (!browser.isBrowser) { |
| 143 throw new ArgumentError("$browser is not a browser."); |
| 144 } |
| 145 |
| 159 return new Future.sync(() { | 146 return new Future.sync(() { |
| 160 if (_pubServeUrl != null) { | 147 if (_pubServeUrl != null) { |
| 161 var suitePrefix = p.withoutExtension(p.relative(path, from: 'test')) + | 148 var suitePrefix = p.withoutExtension(p.relative(path, from: 'test')) + |
| 162 '.browser_test'; | 149 '.browser_test'; |
| 163 var jsUrl = _pubServeUrl.resolve('$suitePrefix.dart.js'); | 150 var jsUrl = _pubServeUrl.resolve('$suitePrefix.dart.js'); |
| 164 return _pubServeSuite(path, jsUrl) | 151 return _pubServeSuite(path, jsUrl) |
| 165 .then((_) => _pubServeUrl.resolve('$suitePrefix.html')); | 152 .then((_) => _pubServeUrl.resolve('$suitePrefix.html')); |
| 166 } else { | 153 } else { |
| 167 return _compileSuite(path).then((dir) { | 154 return _compileSuite(path).then((dir) { |
| 168 if (_closed) return null; | 155 if (_closed) return null; |
| 169 | 156 |
| 170 // Add a trailing slash because at least on Chrome, the iframe's | 157 // Add a trailing slash because at least on Chrome, the iframe's |
| 171 // window.location.href will do so automatically, and if that differs | 158 // window.location.href will do so automatically, and if that differs |
| 172 // from the original URL communication will fail. | 159 // from the original URL communication will fail. |
| 173 return url.resolve( | 160 return url.resolve( |
| 174 "/" + p.toUri(p.relative(dir, from: _compiledDir)).path + "/"); | 161 "/" + p.toUri(p.relative(dir, from: _compiledDir)).path + "/"); |
| 175 }); | 162 }); |
| 176 } | 163 } |
| 177 }).then((suiteUrl) { | 164 }).then((suiteUrl) { |
| 178 if (_closed) return null; | 165 if (_closed) return null; |
| 179 | 166 |
| 180 // TODO(nweiz): Don't start the browser until all the suites are compiled. | 167 // TODO(nweiz): Don't start the browser until all the suites are compiled. |
| 181 return _browserManager.then((browserManager) { | 168 return _browserManagerFor(browser).then((browserManager) { |
| 182 if (_closed) return null; | 169 if (_closed) return null; |
| 183 return browserManager.loadSuite(path, suiteUrl); | 170 return browserManager.loadSuite(path, suiteUrl); |
| 184 }); | 171 }); |
| 185 }); | 172 }); |
| 186 } | 173 } |
| 187 | 174 |
| 188 /// Loads a test suite at [path] from the `pub serve` URL [jsUrl]. | 175 /// Loads a test suite at [path] from the `pub serve` URL [jsUrl]. |
| 189 /// | 176 /// |
| 190 /// This ensures that only one suite is loaded at a time, and that any errors | 177 /// This ensures that only one suite is loaded at a time, and that any errors |
| 191 /// are exposed as [LoadException]s. | 178 /// are exposed as [LoadException]s. |
| (...skipping 26 matching lines...) Expand all Loading... |
| 218 "${response.reasonPhrase}\n" | 205 "${response.reasonPhrase}\n" |
| 219 'Make sure "pub serve" is serving the test/ directory.'); | 206 'Make sure "pub serve" is serving the test/ directory.'); |
| 220 }); | 207 }); |
| 221 }); | 208 }); |
| 222 } | 209 } |
| 223 | 210 |
| 224 /// Compile the test suite at [dartPath] to JavaScript. | 211 /// Compile the test suite at [dartPath] to JavaScript. |
| 225 /// | 212 /// |
| 226 /// Returns a [Future] that completes to the path to the JavaScript. | 213 /// Returns a [Future] that completes to the path to the JavaScript. |
| 227 Future<String> _compileSuite(String dartPath) { | 214 Future<String> _compileSuite(String dartPath) { |
| 228 var dir = new Directory(_compiledDir).createTempSync('test_').path; | 215 return _compileFutures.putIfAbsent(dartPath, () { |
| 229 var jsPath = p.join(dir, p.basename(dartPath) + ".js"); | 216 var dir = new Directory(_compiledDir).createTempSync('test_').path; |
| 230 return _compilers.compile(dartPath, jsPath, | 217 var jsPath = p.join(dir, p.basename(dartPath) + ".js"); |
| 231 packageRoot: packageRootFor(dartPath, _packageRoot)) | 218 return _compilers.compile(dartPath, jsPath, |
| 232 .then((_) { | 219 packageRoot: packageRootFor(dartPath, _packageRoot)) |
| 233 if (_closed) return null; | 220 .then((_) { |
| 221 if (_closed) return null; |
| 234 | 222 |
| 235 // TODO(nweiz): support user-authored HTML files. | 223 // TODO(nweiz): support user-authored HTML files. |
| 236 new File(p.join(dir, "index.html")).writeAsStringSync(''' | 224 new File(p.join(dir, "index.html")).writeAsStringSync(''' |
| 237 <!DOCTYPE html> | 225 <!DOCTYPE html> |
| 238 <html> | 226 <html> |
| 239 <head> | 227 <head> |
| 240 <title>${HTML_ESCAPE.convert(dartPath)} Test</title> | 228 <title>${HTML_ESCAPE.convert(dartPath)} Test</title> |
| 241 <script src="${HTML_ESCAPE.convert(p.basename(jsPath))}"></script> | 229 <script src="${HTML_ESCAPE.convert(p.basename(jsPath))}"></script> |
| 242 </head> | 230 </head> |
| 243 </html> | 231 </html> |
| 244 '''); | 232 '''); |
| 245 return dir; | 233 return dir; |
| 234 }); |
| 246 }); | 235 }); |
| 247 } | 236 } |
| 248 | 237 |
| 238 /// Returns the [BrowserManager] for [platform], which should be a browser. |
| 239 /// |
| 240 /// If no browser manager is running yet, starts one. |
| 241 Future<BrowserManager> _browserManagerFor(TestPlatform platform) { |
| 242 var manager = _browserManagers[platform]; |
| 243 if (manager != null) return manager; |
| 244 |
| 245 var completer = new Completer(); |
| 246 _browserManagers[platform] = completer.future; |
| 247 var path = _webSocketHandler.create(webSocketHandler((webSocket) { |
| 248 completer.complete(new BrowserManager(webSocket)); |
| 249 })); |
| 250 |
| 251 var webSocketUrl = url.replace(scheme: 'ws', path: '/$path'); |
| 252 |
| 253 var hostUrl = url; |
| 254 if (_pubServeUrl != null) { |
| 255 hostUrl = _pubServeUrl.resolve( |
| 256 '/packages/test/src/runner/browser/static/'); |
| 257 } |
| 258 |
| 259 var browser = _newBrowser(hostUrl.replace(queryParameters: { |
| 260 'managerUrl': webSocketUrl.toString() |
| 261 }), platform); |
| 262 _browsers[platform] = browser; |
| 263 |
| 264 // TODO(nweiz): Gracefully handle the browser being killed before the |
| 265 // tests complete. |
| 266 browser.onExit.catchError((error, stackTrace) { |
| 267 if (completer.isCompleted) return; |
| 268 completer.completeError(error, stackTrace); |
| 269 }); |
| 270 |
| 271 return completer.future; |
| 272 } |
| 273 |
| 274 /// Starts the browser identified by [browser] and has it load [url]. |
| 275 Browser _newBrowser(Uri url, TestPlatform browser) { |
| 276 switch (browser) { |
| 277 case TestPlatform.chrome: return new Chrome(url); |
| 278 case TestPlatform.firefox: return new Firefox(url); |
| 279 default: |
| 280 throw new ArgumentError("$browser is not a browser."); |
| 281 } |
| 282 } |
| 283 |
| 249 /// Closes the server and releases all its resources. | 284 /// Closes the server and releases all its resources. |
| 250 /// | 285 /// |
| 251 /// Returns a [Future] that completes once the server is closed and its | 286 /// Returns a [Future] that completes once the server is closed and its |
| 252 /// resources have been fully released. | 287 /// resources have been fully released. |
| 253 Future close() { | 288 Future close() { |
| 254 if (_closeCompleter != null) return _closeCompleter.future; | 289 if (_closeCompleter != null) return _closeCompleter.future; |
| 255 _closeCompleter = new Completer(); | 290 _closeCompleter = new Completer(); |
| 256 | 291 |
| 257 return Future.wait([ | 292 return Future.wait([ |
| 258 _server.close(), | 293 _server.close(), |
| 259 _compilers.close() | 294 _compilers.close() |
| 260 ]).then((_) { | 295 ]).then((_) { |
| 261 if (_browserManagerCompleter == null) return null; | 296 if (_browserManagers.isEmpty) return null; |
| 262 return _browserManager.then((_) => _browser.close()); | 297 return Future.wait(_browserManagers.keys.map((platform) { |
| 298 return _browserManagers[platform] |
| 299 .then((_) => _browsers[platform].close()); |
| 300 })); |
| 263 }).then((_) { | 301 }).then((_) { |
| 264 if (_pubServeUrl == null) { | 302 if (_pubServeUrl == null) { |
| 265 new Directory(_compiledDir).deleteSync(recursive: true); | 303 new Directory(_compiledDir).deleteSync(recursive: true); |
| 266 } else { | 304 } else { |
| 267 _http.close(); | 305 _http.close(); |
| 268 } | 306 } |
| 269 | 307 |
| 270 _closeCompleter.complete(); | 308 _closeCompleter.complete(); |
| 271 }).catchError(_closeCompleter.completeError); | 309 }).catchError(_closeCompleter.completeError); |
| 272 } | 310 } |
| 273 } | 311 } |
| OLD | NEW |