| 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:shelf/shelf.dart' as shelf; | 13 import 'package:shelf/shelf.dart' as shelf; | 
| 13 import 'package:shelf/shelf_io.dart' as shelf_io; | 14 import 'package:shelf/shelf_io.dart' as shelf_io; | 
| 14 import 'package:shelf_static/shelf_static.dart'; | 15 import 'package:shelf_static/shelf_static.dart'; | 
| 15 import 'package:shelf_web_socket/shelf_web_socket.dart'; | 16 import 'package:shelf_web_socket/shelf_web_socket.dart'; | 
| 16 | 17 | 
| 17 import '../../backend/suite.dart'; | 18 import '../../backend/suite.dart'; | 
| 18 import '../../util/io.dart'; | 19 import '../../util/io.dart'; | 
| 19 import '../../util/one_off_handler.dart'; | 20 import '../../util/one_off_handler.dart'; | 
|  | 21 import '../../utils.dart'; | 
|  | 22 import '../load_exception.dart'; | 
| 20 import 'browser_manager.dart'; | 23 import 'browser_manager.dart'; | 
| 21 import 'compiler_pool.dart'; | 24 import 'compiler_pool.dart'; | 
| 22 import 'chrome.dart'; | 25 import 'chrome.dart'; | 
| 23 | 26 | 
| 24 /// A server that serves JS-compiled tests to browsers. | 27 /// A server that serves JS-compiled tests to browsers. | 
| 25 /// | 28 /// | 
| 26 /// A test suite may be loaded for a given file using [loadSuite]. | 29 /// A test suite may be loaded for a given file using [loadSuite]. | 
| 27 class BrowserServer { | 30 class BrowserServer { | 
| 28   /// Starts the server. | 31   /// Starts the server. | 
| 29   /// | 32   /// | 
| 30   /// If [packageRoot] is passed, it's used for all package imports when | 33   /// If [packageRoot] is passed, it's used for all package imports when | 
| 31   /// compiling tests to JS. Otherwise, the package root is inferred from the | 34   /// compiling tests to JS. Otherwise, the package root is inferred from the | 
| 32   /// location of the source file. | 35   /// location of the source file. | 
| 33   /// | 36   /// | 
|  | 37   /// If [pubServeUrl] is passed, tests will be loaded from the `pub serve` | 
|  | 38   /// instance at that URL rather than from the filesystem. | 
|  | 39   /// | 
| 34   /// If [color] is true, console colors will be used when compiling Dart. | 40   /// If [color] is true, console colors will be used when compiling Dart. | 
| 35   static Future<BrowserServer> start({String packageRoot, bool color: false}) { | 41   static Future<BrowserServer> start({String packageRoot, Uri pubServeUrl, | 
| 36     var server = new BrowserServer._(packageRoot, color); | 42       bool color: false}) { | 
|  | 43     var server = new BrowserServer._(packageRoot, pubServeUrl, color); | 
| 37     return server._load().then((_) => server); | 44     return server._load().then((_) => server); | 
| 38   } | 45   } | 
| 39 | 46 | 
| 40   /// The underlying HTTP server. | 47   /// The underlying HTTP server. | 
| 41   HttpServer _server; | 48   HttpServer _server; | 
| 42 | 49 | 
| 43   /// The URL for this server. | 50   /// The URL for this server. | 
| 44   Uri get url => baseUrlForAddress(_server.address, _server.port); | 51   Uri get url => baseUrlForAddress(_server.address, _server.port); | 
| 45 | 52 | 
| 46   /// a [OneOffHandler] for servicing WebSocket connections for | 53   /// a [OneOffHandler] for servicing WebSocket connections for | 
| 47   /// [BrowserManager]s. | 54   /// [BrowserManager]s. | 
| 48   /// | 55   /// | 
| 49   /// This is one-off because each [BrowserManager] can only connect to a single | 56   /// This is one-off because each [BrowserManager] can only connect to a single | 
| 50   /// WebSocket, | 57   /// WebSocket, | 
| 51   final _webSocketHandler = new OneOffHandler(); | 58   final _webSocketHandler = new OneOffHandler(); | 
| 52 | 59 | 
| 53   /// The [CompilerPool] managing active instances of `dart2js`. | 60   /// The [CompilerPool] managing active instances of `dart2js`. | 
|  | 61   /// | 
|  | 62   /// This is `null` if tests are loaded from `pub serve`. | 
| 54   final CompilerPool _compilers; | 63   final CompilerPool _compilers; | 
| 55 | 64 | 
| 56   /// The temporary directory in which compiled JS is emitted. | 65   /// The temporary directory in which compiled JS is emitted. | 
| 57   final String _compiledDir; | 66   final String _compiledDir; | 
| 58 | 67 | 
| 59   /// The package root which is passed to `dart2js`. | 68   /// The package root which is passed to `dart2js`. | 
| 60   final String _packageRoot; | 69   final String _packageRoot; | 
| 61 | 70 | 
|  | 71   /// The URL for the `pub serve` instance to use to load tests. | 
|  | 72   /// | 
|  | 73   /// This is `null` if tests should be compiled manually. | 
|  | 74   final Uri _pubServeUrl; | 
|  | 75 | 
|  | 76   /// The pool of active `pub serve` compilations. | 
|  | 77   /// | 
|  | 78   /// 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. | 
|  | 80   final _pubServePool = new Pool(1); | 
|  | 81 | 
|  | 82   /// The HTTP client to use when caching JS files in `pub serve`. | 
|  | 83   final HttpClient _http; | 
|  | 84 | 
| 62   /// The browser in which test suites are loaded and run. | 85   /// The browser in which test suites are loaded and run. | 
| 63   /// | 86   /// | 
| 64   /// This is `null` until a suite is loaded. | 87   /// This is `null` until a suite is loaded. | 
| 65   Chrome _browser; | 88   Chrome _browser; | 
| 66 | 89 | 
| 67   /// A future that will complete to the [BrowserManager] for [_browser]. | 90   /// A future that will complete to the [BrowserManager] for [_browser]. | 
| 68   /// | 91   /// | 
| 69   /// The first time this is called, it will start both the browser and the | 92   /// The first time this is called, it will start both the browser and the | 
| 70   /// browser manager. Any further calls will return the existing manager. | 93   /// browser manager. Any further calls will return the existing manager. | 
| 71   Future<BrowserManager> get _browserManager { | 94   Future<BrowserManager> get _browserManager { | 
| 72     if (_browserManagerCompleter == null) { | 95     if (_browserManagerCompleter == null) { | 
| 73       _browserManagerCompleter = new Completer(); | 96       _browserManagerCompleter = new Completer(); | 
| 74       var path = _webSocketHandler.create(webSocketHandler((webSocket) { | 97       var path = _webSocketHandler.create(webSocketHandler((webSocket) { | 
| 75         _browserManagerCompleter.complete(new BrowserManager(webSocket)); | 98         _browserManagerCompleter.complete(new BrowserManager(webSocket)); | 
| 76       })); | 99       })); | 
| 77 | 100 | 
| 78       var webSocketUrl = url.replace(scheme: 'ws', path: '/$path'); | 101       var webSocketUrl = url.replace(scheme: 'ws', path: '/$path'); | 
| 79       _browser = new Chrome(url.replace(queryParameters: { | 102 | 
|  | 103       var hostUrl = url; | 
|  | 104       if (_pubServeUrl != null) { | 
|  | 105         hostUrl = _pubServeUrl.resolve( | 
|  | 106             '/packages/test/src/runner/browser/static/'); | 
|  | 107       } | 
|  | 108 | 
|  | 109       _browser = new Chrome(hostUrl.replace(queryParameters: { | 
| 80         'managerUrl': webSocketUrl.toString() | 110         'managerUrl': webSocketUrl.toString() | 
| 81       })); | 111       })); | 
| 82 | 112 | 
| 83       // TODO(nweiz): Gracefully handle the browser being killed before the | 113       // TODO(nweiz): Gracefully handle the browser being killed before the | 
| 84       // tests complete. | 114       // tests complete. | 
| 85       _browser.onExit.catchError((error, stackTrace) { | 115       _browser.onExit.catchError((error, stackTrace) { | 
| 86         if (_browserManagerCompleter.isCompleted) return; | 116         if (_browserManagerCompleter.isCompleted) return; | 
| 87         _browserManagerCompleter.completeError(error, stackTrace); | 117         _browserManagerCompleter.completeError(error, stackTrace); | 
| 88       }); | 118       }); | 
| 89     } | 119     } | 
| 90     return _browserManagerCompleter.future; | 120     return _browserManagerCompleter.future; | 
| 91   } | 121   } | 
| 92   Completer<BrowserManager> _browserManagerCompleter; | 122   Completer<BrowserManager> _browserManagerCompleter; | 
| 93 | 123 | 
| 94   BrowserServer._(this._packageRoot, bool color) | 124   BrowserServer._(this._packageRoot, Uri pubServeUrl, bool color) | 
| 95       : _compiledDir = Directory.systemTemp.createTempSync('test_').path, | 125       : _pubServeUrl = pubServeUrl, | 
|  | 126         _compiledDir = pubServeUrl == null | 
|  | 127             ? Directory.systemTemp.createTempSync('test_').path | 
|  | 128             : null, | 
|  | 129         _http = pubServeUrl == null ? null : new HttpClient(), | 
| 96         _compilers = new CompilerPool(color: color); | 130         _compilers = new CompilerPool(color: color); | 
| 97 | 131 | 
| 98   /// Starts the underlying server. | 132   /// Starts the underlying server. | 
| 99   Future _load() { | 133   Future _load() { | 
| 100     var staticPath = p.join(libDir(packageRoot: _packageRoot), |  | 
| 101         'src/runner/browser/static'); |  | 
| 102     var cascade = new shelf.Cascade() | 134     var cascade = new shelf.Cascade() | 
| 103         .add(_webSocketHandler.handler) | 135         .add(_webSocketHandler.handler); | 
| 104         .add(createStaticHandler(staticPath, defaultDocument: 'index.html')) | 136 | 
| 105         .add(createStaticHandler(_compiledDir, defaultDocument: 'index.html')); | 137     if (_pubServeUrl == null) { | 
|  | 138       var staticPath = p.join(libDir(packageRoot: _packageRoot), | 
|  | 139           'src/runner/browser/static'); | 
|  | 140       cascade = cascade | 
|  | 141           .add(createStaticHandler(staticPath, defaultDocument: 'index.html')) | 
|  | 142           .add(createStaticHandler(_compiledDir, | 
|  | 143               defaultDocument: 'index.html')); | 
|  | 144     } | 
| 106 | 145 | 
| 107     return shelf_io.serve(cascade.handler, 'localhost', 0).then((server) { | 146     return shelf_io.serve(cascade.handler, 'localhost', 0).then((server) { | 
| 108       _server = server; | 147       _server = server; | 
| 109     }); | 148     }); | 
| 110   } | 149   } | 
| 111 | 150 | 
| 112   /// Loads the test suite at [path]. | 151   /// Loads the test suite at [path]. | 
| 113   /// | 152   /// | 
| 114   /// This will start a browser to load the suite if one isn't already running. | 153   /// This will start a browser to load the suite if one isn't already running. | 
| 115   Future<Suite> loadSuite(String path) { | 154   Future<Suite> loadSuite(String path) { | 
| 116     return _compileSuite(path).then((dir) { | 155     return new Future.sync(() { | 
|  | 156       if (_pubServeUrl != null) { | 
|  | 157 | 
|  | 158         var suitePrefix = p.withoutExtension(p.relative(path, from: 'test')) + | 
|  | 159             '.browser_test'; | 
|  | 160         var jsUrl = _pubServeUrl.resolve('$suitePrefix.dart.js'); | 
|  | 161         return _pubServeSuite(path, jsUrl) | 
|  | 162             .then((_) => _pubServeUrl.resolve('$suitePrefix.html')); | 
|  | 163       } else { | 
|  | 164         return _compileSuite(path).then((dir) { | 
|  | 165           // Add a trailing slash because at least on Chrome, the iframe's | 
|  | 166           // window.location.href will do so automatically, and if that differs | 
|  | 167           // from the original URL communication will fail. | 
|  | 168           return url.resolve( | 
|  | 169               "/" + p.toUri(p.relative(dir, from: _compiledDir)).path + "/"); | 
|  | 170         }); | 
|  | 171       } | 
|  | 172     }).then((suiteUrl) { | 
| 117       // TODO(nweiz): Don't start the browser until all the suites are compiled. | 173       // TODO(nweiz): Don't start the browser until all the suites are compiled. | 
| 118       return _browserManager.then((browserManager) { | 174       return _browserManager.then((browserManager) { | 
| 119         // Add a trailing slash because at least on Chrome, the iframe's |  | 
| 120         // window.location.href will do so automatically, and if that differs |  | 
| 121         // from the original URL communication will fail. |  | 
| 122         var suiteUrl = url.resolve( |  | 
| 123             "/" + p.toUri(p.relative(dir, from: _compiledDir)).path + "/"); |  | 
| 124         return browserManager.loadSuite(path, suiteUrl); | 175         return browserManager.loadSuite(path, suiteUrl); | 
| 125       }); | 176       }); | 
| 126     }); | 177     }); | 
| 127   } | 178   } | 
| 128 | 179 | 
|  | 180   /// Loads a test suite at [path] from the `pub serve` URL [jsUrl]. | 
|  | 181   /// | 
|  | 182   /// This ensures that only one suite is loaded at a time, and that any errors | 
|  | 183   /// are exposed as [LoadException]s. | 
|  | 184   Future _pubServeSuite(String path, Uri jsUrl) { | 
|  | 185     return _pubServePool.withResource(() { | 
|  | 186       var timer = new Timer(new Duration(seconds: 1), () { | 
|  | 187         print('"pub serve" is compiling $path...'); | 
|  | 188       }); | 
|  | 189 | 
|  | 190       return _http.headUrl(jsUrl) | 
|  | 191           .then((request) => request.close()) | 
|  | 192           .whenComplete(timer.cancel) | 
|  | 193           .catchError((error, stackTrace) { | 
|  | 194         if (error is! IOException) throw error; | 
|  | 195 | 
|  | 196         var message = getErrorMessage(error); | 
|  | 197         if (error is SocketException) { | 
|  | 198           message = "${error.osError.message} " | 
|  | 199               "(errno ${error.osError.errorCode})"; | 
|  | 200         } | 
|  | 201 | 
|  | 202         throw new LoadException(path, | 
|  | 203             "Error getting $jsUrl: $message\n" | 
|  | 204             'Make sure "pub serve" is running.'); | 
|  | 205       }).then((response) { | 
|  | 206         if (response.statusCode == 200) return; | 
|  | 207 | 
|  | 208         throw new LoadException(path, | 
|  | 209             "Error getting $jsUrl: ${response.statusCode} " | 
|  | 210                 "${response.reasonPhrase}\n" | 
|  | 211             'Make sure "pub serve" is serving the test/ directory.'); | 
|  | 212       }); | 
|  | 213     }); | 
|  | 214   } | 
|  | 215 | 
| 129   /// Compile the test suite at [dartPath] to JavaScript. | 216   /// Compile the test suite at [dartPath] to JavaScript. | 
| 130   /// | 217   /// | 
| 131   /// Returns a [Future] that completes to the path to the JavaScript. | 218   /// Returns a [Future] that completes to the path to the JavaScript. | 
| 132   Future<String> _compileSuite(String dartPath) { | 219   Future<String> _compileSuite(String dartPath) { | 
| 133     var dir = new Directory(_compiledDir).createTempSync('test_').path; | 220     var dir = new Directory(_compiledDir).createTempSync('test_').path; | 
| 134     var jsPath = p.join(dir, p.basename(dartPath) + ".js"); | 221     var jsPath = p.join(dir, p.basename(dartPath) + ".js"); | 
| 135     return _compilers.compile(dartPath, jsPath, | 222     return _compilers.compile(dartPath, jsPath, | 
| 136             packageRoot: packageRootFor(dartPath, _packageRoot)) | 223             packageRoot: packageRootFor(dartPath, _packageRoot)) | 
| 137         .then((_) { | 224         .then((_) { | 
| 138       // TODO(nweiz): support user-authored HTML files. | 225       // TODO(nweiz): support user-authored HTML files. | 
| 139       new File(p.join(dir, "index.html")).writeAsStringSync(''' | 226       new File(p.join(dir, "index.html")).writeAsStringSync(''' | 
| 140 <!DOCTYPE html> | 227 <!DOCTYPE html> | 
| 141 <html> | 228 <html> | 
| 142 <head> | 229 <head> | 
| 143   <title>${HTML_ESCAPE.convert(dartPath)} Test</title> | 230   <title>${HTML_ESCAPE.convert(dartPath)} Test</title> | 
| 144   <script src="${HTML_ESCAPE.convert(p.basename(jsPath))}"></script> | 231   <script src="${HTML_ESCAPE.convert(p.basename(jsPath))}"></script> | 
| 145 </head> | 232 </head> | 
| 146 </html> | 233 </html> | 
| 147 '''); | 234 '''); | 
| 148       return dir; | 235       return dir; | 
| 149     }); | 236     }); | 
| 150   } | 237   } | 
| 151 | 238 | 
| 152   /// Closes the server and releases all its resources. | 239   /// Closes the server and releases all its resources. | 
| 153   /// | 240   /// | 
| 154   /// Returns a [Future] that completes once the server is closed and its | 241   /// Returns a [Future] that completes once the server is closed and its | 
| 155   /// resources have been fully released. | 242   /// resources have been fully released. | 
| 156   Future close() { | 243   Future close() { | 
| 157     new Directory(_compiledDir).deleteSync(recursive: true); | 244     if (_pubServeUrl == null) { | 
|  | 245       new Directory(_compiledDir).deleteSync(recursive: true); | 
|  | 246     } else { | 
|  | 247       _http.close(); | 
|  | 248     } | 
|  | 249 | 
| 158     return _server.close().then((_) { | 250     return _server.close().then((_) { | 
| 159       if (_browserManagerCompleter == null) return null; | 251       if (_browserManagerCompleter == null) return null; | 
| 160       return _browserManager.then((_) => _browser.close()); | 252       return _browserManager.then((_) => _browser.close()); | 
| 161     }); | 253     }); | 
| 162   } | 254   } | 
| 163 } | 255 } | 
| OLD | NEW | 
|---|