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.browser_manager; | 5 library test.runner.browser.browser_manager; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:convert'; | 8 import 'dart:convert'; |
9 | 9 |
| 10 import 'package:async/async.dart'; |
10 import 'package:http_parser/http_parser.dart'; | 11 import 'package:http_parser/http_parser.dart'; |
11 import 'package:pool/pool.dart'; | 12 import 'package:pool/pool.dart'; |
12 | 13 |
13 import '../../backend/metadata.dart'; | 14 import '../../backend/metadata.dart'; |
14 import '../../backend/test_platform.dart'; | 15 import '../../backend/test_platform.dart'; |
15 import '../../util/cancelable_future.dart'; | 16 import '../../util/cancelable_future.dart'; |
16 import '../../util/multi_channel.dart'; | 17 import '../../util/multi_channel.dart'; |
17 import '../../util/remote_exception.dart'; | 18 import '../../util/remote_exception.dart'; |
18 import '../../util/stack_trace_mapper.dart'; | 19 import '../../util/stack_trace_mapper.dart'; |
19 import '../../utils.dart'; | 20 import '../../utils.dart'; |
| 21 import '../application_exception.dart'; |
20 import '../environment.dart'; | 22 import '../environment.dart'; |
21 import '../load_exception.dart'; | 23 import '../load_exception.dart'; |
22 import '../runner_suite.dart'; | 24 import '../runner_suite.dart'; |
| 25 import 'browser.dart'; |
| 26 import 'chrome.dart'; |
| 27 import 'content_shell.dart'; |
| 28 import 'dartium.dart'; |
| 29 import 'firefox.dart'; |
23 import 'iframe_test.dart'; | 30 import 'iframe_test.dart'; |
| 31 import 'internet_explorer.dart'; |
| 32 import 'phantom_js.dart'; |
| 33 import 'safari.dart'; |
24 | 34 |
25 /// A class that manages the connection to a single running browser. | 35 /// A class that manages the connection to a single running browser. |
26 /// | 36 /// |
27 /// This is in charge of telling the browser which test suites to load and | 37 /// This is in charge of telling the browser which test suites to load and |
28 /// converting its responses into [Suite] objects. | 38 /// converting its responses into [Suite] objects. |
29 class BrowserManager { | 39 class BrowserManager { |
30 /// The browser that this is managing. | 40 /// The browser instance that this is connected to via [_channel]. |
31 final TestPlatform browser; | 41 final Browser _browser; |
| 42 |
| 43 // TODO(nweiz): Consider removing the duplication between this and |
| 44 // [_browser.name]. |
| 45 /// The [TestPlatform] for [_browser]. |
| 46 final TestPlatform _platform; |
32 | 47 |
33 /// The channel used to communicate with the browser. | 48 /// The channel used to communicate with the browser. |
34 /// | 49 /// |
35 /// This is connected to a page running `static/host.dart`. | 50 /// This is connected to a page running `static/host.dart`. |
36 final MultiChannel _channel; | 51 final MultiChannel _channel; |
37 | 52 |
38 /// A pool that ensures that limits the number of initial connections the | 53 /// A pool that ensures that limits the number of initial connections the |
39 /// manager will wait for at once. | 54 /// manager will wait for at once. |
40 /// | 55 /// |
41 /// This isn't the *total* number of connections; any number of iframes may be | 56 /// This isn't the *total* number of connections; any number of iframes may be |
(...skipping 11 matching lines...) Expand all Loading... |
53 /// Whether the channel to the browser has closed. | 68 /// Whether the channel to the browser has closed. |
54 bool _closed = false; | 69 bool _closed = false; |
55 | 70 |
56 /// The completer for [_BrowserEnvironment.displayPause]. | 71 /// The completer for [_BrowserEnvironment.displayPause]. |
57 /// | 72 /// |
58 /// This will be `null` as long as the browser isn't displaying a pause | 73 /// This will be `null` as long as the browser isn't displaying a pause |
59 /// screen. | 74 /// screen. |
60 CancelableCompleter _pauseCompleter; | 75 CancelableCompleter _pauseCompleter; |
61 | 76 |
62 /// The environment to attach to each suite. | 77 /// The environment to attach to each suite. |
63 _BrowserEnvironment _environment; | 78 Future<_BrowserEnvironment> _environment; |
| 79 |
| 80 /// Starts the browser identified by [platform] and has it connect to [url]. |
| 81 /// |
| 82 /// [url] should serve a page that establishes a WebSocket connection with |
| 83 /// this process. That connection, once established, should be emitted via |
| 84 /// [future]. |
| 85 /// |
| 86 /// Returns the browser manager, or throws an [ApplicationException] if a |
| 87 /// connection fails to be established. |
| 88 static Future<BrowserManager> start(TestPlatform platform, Uri url, |
| 89 Future<CompatibleWebSocket> future) { |
| 90 var browser = _newBrowser(url, platform); |
| 91 |
| 92 var completer = new Completer(); |
| 93 |
| 94 // TODO(nweiz): Gracefully handle the browser being killed before the |
| 95 // tests complete. |
| 96 browser.onExit.then((_) { |
| 97 throw new ApplicationException( |
| 98 "${platform.name} exited before connecting."); |
| 99 }).catchError((error, stackTrace) { |
| 100 if (completer.isCompleted) return; |
| 101 completer.completeError(error, stackTrace); |
| 102 }); |
| 103 |
| 104 future.then((webSocket) { |
| 105 if (completer.isCompleted) return; |
| 106 completer.complete(new BrowserManager._(browser, platform, webSocket)); |
| 107 }).catchError((error, stackTrace) { |
| 108 browser.close(); |
| 109 if (completer.isCompleted) return; |
| 110 completer.completeError(error, stackTrace); |
| 111 }); |
| 112 |
| 113 return completer.future.timeout(new Duration(seconds: 30), onTimeout: () { |
| 114 browser.close(); |
| 115 throw new ApplicationException( |
| 116 "Timed out waiting for ${platform.name} to connect."); |
| 117 }); |
| 118 } |
| 119 |
| 120 /// Starts the browser identified by [browser] and has it load [url]. |
| 121 static Browser _newBrowser(Uri url, TestPlatform browser) { |
| 122 switch (browser) { |
| 123 case TestPlatform.dartium: return new Dartium(url); |
| 124 case TestPlatform.contentShell: return new ContentShell(url); |
| 125 case TestPlatform.chrome: return new Chrome(url); |
| 126 case TestPlatform.phantomJS: return new PhantomJS(url); |
| 127 case TestPlatform.firefox: return new Firefox(url); |
| 128 case TestPlatform.safari: return new Safari(url); |
| 129 case TestPlatform.internetExplorer: return new InternetExplorer(url); |
| 130 default: |
| 131 throw new ArgumentError("$browser is not a browser."); |
| 132 } |
| 133 } |
64 | 134 |
65 /// Creates a new BrowserManager that communicates with [browser] over | 135 /// Creates a new BrowserManager that communicates with [browser] over |
66 /// [webSocket]. | 136 /// [webSocket]. |
67 BrowserManager(this.browser, CompatibleWebSocket webSocket) | 137 BrowserManager._(this._browser, this._platform, CompatibleWebSocket webSocket) |
68 : _channel = new MultiChannel( | 138 : _channel = new MultiChannel( |
69 webSocket.map(JSON.decode), | 139 webSocket.map(JSON.decode), |
70 mapSink(webSocket, JSON.encode)) { | 140 mapSink(webSocket, JSON.encode)) { |
71 _environment = new _BrowserEnvironment(this); | 141 _environment = _loadBrowserEnvironment(); |
72 _channel.stream.listen(_onMessage, onDone: _onDone); | 142 _channel.stream.listen(_onMessage, onDone: close); |
| 143 } |
| 144 |
| 145 /// Loads [_BrowserEnvironment]. |
| 146 Future<_BrowserEnvironment> _loadBrowserEnvironment() async { |
| 147 var observatoryUrl; |
| 148 if (_platform.isDartVM) observatoryUrl = await _browser.observatoryUrl; |
| 149 return new _BrowserEnvironment(this, observatoryUrl); |
73 } | 150 } |
74 | 151 |
75 /// Tells the browser the load a test suite from the URL [url]. | 152 /// Tells the browser the load a test suite from the URL [url]. |
76 /// | 153 /// |
77 /// [url] should be an HTML page with a reference to the JS-compiled test | 154 /// [url] should be an HTML page with a reference to the JS-compiled test |
78 /// suite. [path] is the path of the original test suite file, which is used | 155 /// suite. [path] is the path of the original test suite file, which is used |
79 /// for reporting. [metadata] is the parsed metadata for the test suite. | 156 /// for reporting. [metadata] is the parsed metadata for the test suite. |
80 /// | 157 /// |
81 /// If [mapper] is passed, it's used to map stack traces for errors coming | 158 /// If [mapper] is passed, it's used to map stack traces for errors coming |
82 /// from this test suite. | 159 /// from this test suite. |
83 Future<RunnerSuite> loadSuite(String path, Uri url, Metadata metadata, | 160 Future<RunnerSuite> loadSuite(String path, Uri url, Metadata metadata, |
84 {StackTraceMapper mapper}) async { | 161 {StackTraceMapper mapper}) async { |
85 url = url.replace(fragment: Uri.encodeFull(JSON.encode({ | 162 url = url.replace(fragment: Uri.encodeFull(JSON.encode({ |
86 "metadata": metadata.serialize(), | 163 "metadata": metadata.serialize(), |
87 "browser": browser.identifier | 164 "browser": _platform.identifier |
88 }))); | 165 }))); |
89 | 166 |
90 // The stream may close before emitting a value if the browser is killed | 167 // The stream may close before emitting a value if the browser is killed |
91 // prematurely (e.g. via Control-C). | 168 // prematurely (e.g. via Control-C). |
92 var suiteVirtualChannel = _channel.virtualChannel(); | 169 var suiteVirtualChannel = _channel.virtualChannel(); |
93 var suiteId = _suiteId++; | 170 var suiteId = _suiteId++; |
94 var suiteChannel; | 171 var suiteChannel; |
95 | 172 |
96 closeIframe() { | 173 closeIframe() { |
97 if (_closed) return; | 174 if (_closed) return; |
(...skipping 25 matching lines...) Expand all Loading... |
123 completer.complete(response); | 200 completer.complete(response); |
124 } | 201 } |
125 }, onDone: () { | 202 }, onDone: () { |
126 if (!completer.isCompleted) completer.complete(); | 203 if (!completer.isCompleted) completer.complete(); |
127 }); | 204 }); |
128 | 205 |
129 return completer.future.timeout(new Duration(minutes: 1), onTimeout: () { | 206 return completer.future.timeout(new Duration(minutes: 1), onTimeout: () { |
130 throw new LoadException( | 207 throw new LoadException( |
131 path, | 208 path, |
132 "Timed out waiting for the test suite to connect on " | 209 "Timed out waiting for the test suite to connect on " |
133 "${browser.name}."); | 210 "${_platform.name}."); |
134 }); | 211 }); |
135 }); | 212 }); |
136 | 213 |
137 if (response == null) { | 214 if (response == null) { |
138 closeIframe(); | 215 closeIframe(); |
139 return null; | 216 throw new LoadException( |
| 217 path, "Connection closed before test suite loaded."); |
140 } | 218 } |
141 | 219 |
142 if (response["type"] == "loadException") { | 220 if (response["type"] == "loadException") { |
143 closeIframe(); | 221 closeIframe(); |
144 throw new LoadException(path, response["message"]); | 222 throw new LoadException(path, response["message"]); |
145 } | 223 } |
146 | 224 |
147 if (response["type"] == "error") { | 225 if (response["type"] == "error") { |
148 closeIframe(); | 226 closeIframe(); |
149 var asyncError = RemoteException.deserialize(response["error"]); | 227 var asyncError = RemoteException.deserialize(response["error"]); |
150 await new Future.error( | 228 await new Future.error( |
151 new LoadException(path, asyncError.error), | 229 new LoadException(path, asyncError.error), |
152 asyncError.stackTrace); | 230 asyncError.stackTrace); |
153 } | 231 } |
154 | 232 |
155 return new RunnerSuite(_environment, response["tests"].map((test) { | 233 return new RunnerSuite(await _environment, response["tests"].map((test) { |
156 var testMetadata = new Metadata.deserialize(test['metadata']); | 234 var testMetadata = new Metadata.deserialize(test['metadata']); |
157 var testChannel = suiteChannel.virtualChannel(test['channel']); | 235 var testChannel = suiteChannel.virtualChannel(test['channel']); |
158 return new IframeTest(test['name'], testMetadata, testChannel, | 236 return new IframeTest(test['name'], testMetadata, testChannel, |
159 mapper: mapper); | 237 mapper: mapper); |
160 }), platform: browser, metadata: metadata, path: path, | 238 }), platform: _platform, metadata: metadata, path: path, |
161 onClose: () => closeIframe()); | 239 onClose: () => closeIframe()); |
162 } | 240 } |
163 | 241 |
164 /// An implementation of [Environment.displayPause]. | 242 /// An implementation of [Environment.displayPause]. |
165 CancelableFuture _displayPause() { | 243 CancelableFuture _displayPause() { |
166 if (_pauseCompleter != null) return _pauseCompleter.future; | 244 if (_pauseCompleter != null) return _pauseCompleter.future; |
167 | 245 |
168 _pauseCompleter = new CancelableCompleter(() { | 246 _pauseCompleter = new CancelableCompleter(() { |
169 _channel.sink.add({"command": "resume"}); | 247 _channel.sink.add({"command": "resume"}); |
170 _pauseCompleter = null; | 248 _pauseCompleter = null; |
171 }); | 249 }); |
172 | 250 |
173 _channel.sink.add({"command": "displayPause"}); | 251 _channel.sink.add({"command": "displayPause"}); |
174 return _pauseCompleter.future.whenComplete(() { | 252 return _pauseCompleter.future.whenComplete(() { |
175 _pauseCompleter = null; | 253 _pauseCompleter = null; |
176 }); | 254 }); |
177 } | 255 } |
178 | 256 |
179 /// The callback for handling messages received from the host page. | 257 /// The callback for handling messages received from the host page. |
180 void _onMessage(Map message) { | 258 void _onMessage(Map message) { |
181 assert(message["command"] == "resume"); | 259 assert(message["command"] == "resume"); |
182 if (_pauseCompleter == null) return; | 260 if (_pauseCompleter == null) return; |
183 _pauseCompleter.complete(); | 261 _pauseCompleter.complete(); |
184 } | 262 } |
185 | 263 |
186 /// The callback called when the WebSocket is closed. | 264 /// Closes the manager and releases any resources it owns, including closing |
187 void _onDone() { | 265 /// the browser. |
| 266 Future close() => _closeMemoizer.runOnce(() { |
188 _closed = true; | 267 _closed = true; |
189 if (_pauseCompleter != null) _pauseCompleter.complete(); | 268 if (_pauseCompleter != null) _pauseCompleter.complete(); |
190 _pauseCompleter = null; | 269 _pauseCompleter = null; |
191 } | 270 return _browser.close(); |
| 271 }); |
| 272 final _closeMemoizer = new AsyncMemoizer(); |
192 } | 273 } |
193 | 274 |
194 /// An implementation of [Environment] for the browser. | 275 /// An implementation of [Environment] for the browser. |
195 /// | 276 /// |
196 /// All methods forward directly to [BrowserManager]. | 277 /// All methods forward directly to [BrowserManager]. |
197 class _BrowserEnvironment implements Environment { | 278 class _BrowserEnvironment implements Environment { |
198 final BrowserManager _manager; | 279 final BrowserManager _manager; |
199 | 280 |
200 _BrowserEnvironment(this._manager); | 281 final Uri observatoryUrl; |
| 282 |
| 283 _BrowserEnvironment(this._manager, this.observatoryUrl); |
201 | 284 |
202 CancelableFuture displayPause() => _manager._displayPause(); | 285 CancelableFuture displayPause() => _manager._displayPause(); |
203 } | 286 } |
OLD | NEW |