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 |