Chromium Code Reviews| 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 import 'dart:async'; | 5 import 'dart:async'; |
| 6 import 'dart:convert'; | 6 import 'dart:convert'; |
| 7 | 7 |
| 8 import 'package:async/async.dart'; | 8 import 'package:async/async.dart'; |
| 9 import 'package:http_parser/http_parser.dart'; | 9 import 'package:http_parser/http_parser.dart'; |
| 10 import 'package:pool/pool.dart'; | 10 import 'package:pool/pool.dart'; |
| 11 import 'package:stream_channel/stream_channel.dart'; | 11 import 'package:stream_channel/stream_channel.dart'; |
| 12 | 12 |
| 13 import '../../backend/metadata.dart'; | 13 import '../../backend/metadata.dart'; |
| 14 import '../../backend/test_platform.dart'; | 14 import '../../backend/test_platform.dart'; |
| 15 import '../../util/stack_trace_mapper.dart'; | 15 import '../../util/stack_trace_mapper.dart'; |
| 16 import '../application_exception.dart'; | 16 import '../application_exception.dart'; |
| 17 import '../environment.dart'; | 17 import '../environment.dart'; |
| 18 import '../plugin/platform_helpers.dart'; | |
| 18 import '../runner_suite.dart'; | 19 import '../runner_suite.dart'; |
| 19 import 'browser.dart'; | 20 import 'browser.dart'; |
| 20 import 'chrome.dart'; | 21 import 'chrome.dart'; |
| 21 import 'content_shell.dart'; | 22 import 'content_shell.dart'; |
| 22 import 'dartium.dart'; | 23 import 'dartium.dart'; |
| 23 import 'firefox.dart'; | 24 import 'firefox.dart'; |
| 24 import 'internet_explorer.dart'; | 25 import 'internet_explorer.dart'; |
| 25 import 'phantom_js.dart'; | 26 import 'phantom_js.dart'; |
| 26 import 'safari.dart'; | 27 import 'safari.dart'; |
| 27 import 'suite.dart'; | |
| 28 | 28 |
| 29 /// A class that manages the connection to a single running browser. | 29 /// A class that manages the connection to a single running browser. |
| 30 /// | 30 /// |
| 31 /// This is in charge of telling the browser which test suites to load and | 31 /// This is in charge of telling the browser which test suites to load and |
| 32 /// converting its responses into [Suite] objects. | 32 /// converting its responses into [Suite] objects. |
| 33 class BrowserManager { | 33 class BrowserManager { |
| 34 /// The browser instance that this is connected to via [_channel]. | 34 /// The browser instance that this is connected to via [_channel]. |
| 35 final Browser _browser; | 35 final Browser _browser; |
| 36 | 36 |
| 37 // TODO(nweiz): Consider removing the duplication between this and | 37 // TODO(nweiz): Consider removing the duplication between this and |
| 38 // [_browser.name]. | 38 // [_browser.name]. |
| 39 /// The [TestPlatform] for [_browser]. | 39 /// The [TestPlatform] for [_browser]. |
| 40 final TestPlatform _platform; | 40 final TestPlatform _platform; |
| 41 | 41 |
| 42 /// The channel used to communicate with the browser. | 42 /// The channel used to communicate with the browser. |
| 43 /// | 43 /// |
| 44 /// This is connected to a page running `static/host.dart`. | 44 /// This is connected to a page running `static/host.dart`. |
| 45 final MultiChannel _channel; | 45 MultiChannel _channel; |
| 46 | 46 |
| 47 /// A pool that ensures that limits the number of initial connections the | 47 /// A pool that ensures that limits the number of initial connections the |
| 48 /// manager will wait for at once. | 48 /// manager will wait for at once. |
| 49 /// | 49 /// |
| 50 /// This isn't the *total* number of connections; any number of iframes may be | 50 /// This isn't the *total* number of connections; any number of iframes may be |
| 51 /// loaded in the same browser. However, the browser can only load so many at | 51 /// loaded in the same browser. However, the browser can only load so many at |
| 52 /// once, and we want a timeout in case they fail so we only wait for so many | 52 /// once, and we want a timeout in case they fail so we only wait for so many |
| 53 /// at once. | 53 /// at once. |
| 54 final _pool = new Pool(8); | 54 final _pool = new Pool(8); |
| 55 | 55 |
| 56 /// The ID of the next suite to be loaded. | 56 /// The ID of the next suite to be loaded. |
| 57 /// | 57 /// |
| 58 /// This is used to ensure that the suites can be referred to consistently | 58 /// This is used to ensure that the suites can be referred to consistently |
| 59 /// across the client and server. | 59 /// across the client and server. |
| 60 int _suiteId = 0; | 60 int _suiteID = 0; |
| 61 | 61 |
| 62 /// Whether the channel to the browser has closed. | 62 /// Whether the channel to the browser has closed. |
| 63 bool _closed = false; | 63 bool _closed = false; |
| 64 | 64 |
| 65 /// The completer for [_BrowserEnvironment.displayPause]. | 65 /// The completer for [_BrowserEnvironment.displayPause]. |
| 66 /// | 66 /// |
| 67 /// This will be `null` as long as the browser isn't displaying a pause | 67 /// This will be `null` as long as the browser isn't displaying a pause |
| 68 /// screen. | 68 /// screen. |
| 69 CancelableCompleter _pauseCompleter; | 69 CancelableCompleter _pauseCompleter; |
| 70 | 70 |
| 71 /// The environment to attach to each suite. | 71 /// The environment to attach to each suite. |
| 72 Future<_BrowserEnvironment> _environment; | 72 Future<_BrowserEnvironment> _environment; |
| 73 | 73 |
| 74 /// Controllers for every suite in this browser. | |
| 75 /// | |
| 76 /// These are used to mark suites as debugging or not based on the browser's | |
| 77 /// pings. | |
| 78 final _controllers = new Set<RunnerSuiteController>(); | |
| 79 | |
| 80 // A timer that's reset whenever we receive a message from the browser. | |
| 81 // | |
| 82 // Because the browser stops running code when the user is actively debugging, | |
| 83 // this lets us detect whether they're debugging reasonably accurately. | |
| 84 RestartableTimer _timer; | |
| 85 | |
| 74 /// Starts the browser identified by [platform] and has it connect to [url]. | 86 /// Starts the browser identified by [platform] and has it connect to [url]. |
| 75 /// | 87 /// |
| 76 /// [url] should serve a page that establishes a WebSocket connection with | 88 /// [url] should serve a page that establishes a WebSocket connection with |
| 77 /// this process. That connection, once established, should be emitted via | 89 /// this process. That connection, once established, should be emitted via |
| 78 /// [future]. If [debug] is true, starts the browser in debug mode, with its | 90 /// [future]. If [debug] is true, starts the browser in debug mode, with its |
| 79 /// debugger interfaces on and detected. | 91 /// debugger interfaces on and detected. |
| 80 /// | 92 /// |
| 81 /// Returns the browser manager, or throws an [ApplicationException] if a | 93 /// Returns the browser manager, or throws an [ApplicationException] if a |
| 82 /// connection fails to be established. | 94 /// connection fails to be established. |
| 83 static Future<BrowserManager> start(TestPlatform platform, Uri url, | 95 static Future<BrowserManager> start(TestPlatform platform, Uri url, |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 126 case TestPlatform.firefox: return new Firefox(url); | 138 case TestPlatform.firefox: return new Firefox(url); |
| 127 case TestPlatform.safari: return new Safari(url); | 139 case TestPlatform.safari: return new Safari(url); |
| 128 case TestPlatform.internetExplorer: return new InternetExplorer(url); | 140 case TestPlatform.internetExplorer: return new InternetExplorer(url); |
| 129 default: | 141 default: |
| 130 throw new ArgumentError("$browser is not a browser."); | 142 throw new ArgumentError("$browser is not a browser."); |
| 131 } | 143 } |
| 132 } | 144 } |
| 133 | 145 |
| 134 /// Creates a new BrowserManager that communicates with [browser] over | 146 /// Creates a new BrowserManager that communicates with [browser] over |
| 135 /// [webSocket]. | 147 /// [webSocket]. |
| 136 BrowserManager._(this._browser, this._platform, WebSocketChannel webSocket) | 148 BrowserManager._(this._browser, this._platform, WebSocketChannel webSocket) { |
| 137 : _channel = new MultiChannel(webSocket.transform(jsonDocument)) { | 149 // The duration should be short enough that the debugging console is open as |
| 150 // soon as the user is done setting breakpoints, but long enough that a test | |
| 151 // doing a lot of synchronous work doesn't trigger a false positive. | |
| 152 // | |
| 153 // Start this canceled because we don't want it to start ticking until we | |
| 154 // get some response from the iframe. | |
| 155 _timer = new RestartableTimer(new Duration(seconds: 3), () { | |
|
kevmoo
2016/02/17 16:22:48
const Duration?
nweiz
2016/02/17 21:52:04
I really don't think const adds any value in most
| |
| 156 for (var controller in _controllers) { | |
| 157 controller.setDebugging(true); | |
| 158 } | |
| 159 })..cancel(); | |
| 160 | |
| 161 // Whenever we get a message, no matter which child channel it's for, we the | |
| 162 // know browser is still running code which means the user isn't debugging. | |
| 163 _channel = new MultiChannel(webSocket.transform(jsonDocument) | |
| 164 .changeStream((stream) { | |
| 165 return stream.map((message) { | |
| 166 _timer.reset(); | |
| 167 for (var controller in _controllers) { | |
| 168 controller.setDebugging(false); | |
| 169 } | |
| 170 | |
| 171 return message; | |
| 172 }); | |
| 173 })); | |
| 174 | |
| 138 _environment = _loadBrowserEnvironment(); | 175 _environment = _loadBrowserEnvironment(); |
| 139 _channel.stream.listen(_onMessage, onDone: close); | 176 _channel.stream.listen(_onMessage, onDone: close); |
| 140 } | 177 } |
| 141 | 178 |
| 142 /// Loads [_BrowserEnvironment]. | 179 /// Loads [_BrowserEnvironment]. |
| 143 Future<_BrowserEnvironment> _loadBrowserEnvironment() async { | 180 Future<_BrowserEnvironment> _loadBrowserEnvironment() async { |
| 144 var observatoryUrl; | 181 var observatoryUrl; |
| 145 if (_platform.isDartVM) observatoryUrl = await _browser.observatoryUrl; | 182 if (_platform.isDartVM) observatoryUrl = await _browser.observatoryUrl; |
| 146 | 183 |
| 147 var remoteDebuggerUrl; | 184 var remoteDebuggerUrl; |
| 148 if (_platform.isHeadless) { | 185 if (_platform.isHeadless) { |
| 149 remoteDebuggerUrl = await _browser.remoteDebuggerUrl; | 186 remoteDebuggerUrl = await _browser.remoteDebuggerUrl; |
| 150 } | 187 } |
| 151 | 188 |
| 152 return new _BrowserEnvironment(this, observatoryUrl, remoteDebuggerUrl); | 189 return new _BrowserEnvironment(this, observatoryUrl, remoteDebuggerUrl); |
| 153 } | 190 } |
| 154 | 191 |
| 155 /// Tells the browser the load a test suite from the URL [url]. | 192 /// Tells the browser the load a test suite from the URL [url]. |
| 156 /// | 193 /// |
| 157 /// [url] should be an HTML page with a reference to the JS-compiled test | 194 /// [url] should be an HTML page with a reference to the JS-compiled test |
| 158 /// suite. [path] is the path of the original test suite file, which is used | 195 /// suite. [path] is the path of the original test suite file, which is used |
| 159 /// for reporting. [metadata] is the parsed metadata for the test suite. | 196 /// for reporting. [metadata] is the parsed metadata for the test suite. |
| 160 /// | 197 /// |
| 161 /// If [mapper] is passed, it's used to map stack traces for errors coming | 198 /// If [mapper] is passed, it's used to map stack traces for errors coming |
| 162 /// from this test suite. | 199 /// from this test suite. |
| 163 Future<RunnerSuite> loadSuite(String path, Uri url, Metadata metadata, | 200 Future<RunnerSuite> load(String path, Uri url, Metadata metadata, |
| 164 {StackTraceMapper mapper}) async { | 201 {StackTraceMapper mapper}) async { |
| 165 url = url.replace(fragment: Uri.encodeFull(JSON.encode({ | 202 url = url.replace(fragment: Uri.encodeFull(JSON.encode({ |
| 166 "metadata": metadata.serialize(), | 203 "metadata": metadata.serialize(), |
| 167 "browser": _platform.identifier | 204 "browser": _platform.identifier |
| 168 }))); | 205 }))); |
| 169 | 206 |
| 170 // The stream may close before emitting a value if the browser is killed | 207 var suiteID = _suiteID++; |
| 171 // prematurely (e.g. via Control-C). | 208 var controller; |
| 172 var suiteVirtualChannel = _channel.virtualChannel(); | |
| 173 var suiteId = _suiteId++; | |
| 174 | |
| 175 closeIframe() { | 209 closeIframe() { |
| 176 if (_closed) return; | 210 if (_closed) return; |
| 211 _controllers.remove(controller); | |
| 177 _channel.sink.add({ | 212 _channel.sink.add({ |
| 178 "command": "closeSuite", | 213 "command": "closeSuite", |
| 179 "id": suiteId | 214 "id": suiteID |
| 180 }); | 215 }); |
| 181 } | 216 } |
| 182 | 217 |
| 218 // The virtual channel will be closed when the suite is closed, in which | |
| 219 // case we should unload the iframe. | |
| 220 var suiteChannel = _channel.virtualChannel(); | |
| 221 var suiteChannelID = suiteChannel.id; | |
| 222 suiteChannel = suiteChannel.transformStream( | |
| 223 new StreamTransformer.fromHandlers(handleDone: (sink) { | |
| 224 closeIframe(); | |
| 225 sink.close(); | |
| 226 })); | |
| 227 | |
| 183 return await _pool.withResource(() async { | 228 return await _pool.withResource(() async { |
| 184 _channel.sink.add({ | 229 _channel.sink.add({ |
| 185 "command": "loadSuite", | 230 "command": "loadSuite", |
| 186 "url": url.toString(), | 231 "url": url.toString(), |
| 187 "id": suiteId, | 232 "id": suiteID, |
| 188 "channel": suiteVirtualChannel.id | 233 "channel": suiteChannelID |
| 189 }); | 234 }); |
| 190 | 235 |
| 191 try { | 236 try { |
| 192 return await loadBrowserSuite( | 237 controller = await deserializeSuite( |
| 193 suiteVirtualChannel, await _environment, path, | 238 path, _platform, metadata, await _environment, suiteChannel, |
| 194 mapper: mapper, platform: _platform, onClose: () => closeIframe()); | 239 mapTrace: mapper?.mapStackTrace); |
| 240 _controllers.add(controller); | |
| 241 return controller.suite; | |
| 195 } catch (_) { | 242 } catch (_) { |
| 196 closeIframe(); | 243 closeIframe(); |
| 197 rethrow; | 244 rethrow; |
| 198 } | 245 } |
| 199 }); | 246 }); |
| 200 } | 247 } |
| 201 | 248 |
| 202 /// An implementation of [Environment.displayPause]. | 249 /// An implementation of [Environment.displayPause]. |
| 203 CancelableOperation _displayPause() { | 250 CancelableOperation _displayPause() { |
| 204 if (_pauseCompleter != null) return _pauseCompleter.operation; | 251 if (_pauseCompleter != null) return _pauseCompleter.operation; |
| 205 | 252 |
| 206 _pauseCompleter = new CancelableCompleter(onCancel: () { | 253 _pauseCompleter = new CancelableCompleter(onCancel: () { |
| 207 _channel.sink.add({"command": "resume"}); | 254 _channel.sink.add({"command": "resume"}); |
| 208 _pauseCompleter = null; | 255 _pauseCompleter = null; |
| 209 }); | 256 }); |
| 210 | 257 |
| 211 _pauseCompleter.operation.value.whenComplete(() { | 258 _pauseCompleter.operation.value.whenComplete(() { |
| 212 _pauseCompleter = null; | 259 _pauseCompleter = null; |
| 213 }); | 260 }); |
| 214 | 261 |
| 215 _channel.sink.add({"command": "displayPause"}); | 262 _channel.sink.add({"command": "displayPause"}); |
| 216 | 263 |
| 217 return _pauseCompleter.operation; | 264 return _pauseCompleter.operation; |
| 218 } | 265 } |
| 219 | 266 |
| 220 /// The callback for handling messages received from the host page. | 267 /// The callback for handling messages received from the host page. |
| 221 void _onMessage(Map message) { | 268 void _onMessage(Map message) { |
| 269 if (message["command"] == "ping") return; | |
| 270 | |
| 222 assert(message["command"] == "resume"); | 271 assert(message["command"] == "resume"); |
| 223 if (_pauseCompleter == null) return; | 272 if (_pauseCompleter == null) return; |
| 224 _pauseCompleter.complete(); | 273 _pauseCompleter.complete(); |
| 225 } | 274 } |
| 226 | 275 |
| 227 /// Closes the manager and releases any resources it owns, including closing | 276 /// Closes the manager and releases any resources it owns, including closing |
| 228 /// the browser. | 277 /// the browser. |
| 229 Future close() => _closeMemoizer.runOnce(() { | 278 Future close() => _closeMemoizer.runOnce(() { |
| 230 _closed = true; | 279 _closed = true; |
| 280 _timer.cancel(); | |
| 231 if (_pauseCompleter != null) _pauseCompleter.complete(); | 281 if (_pauseCompleter != null) _pauseCompleter.complete(); |
| 232 _pauseCompleter = null; | 282 _pauseCompleter = null; |
| 283 _controllers.clear(); | |
| 233 return _browser.close(); | 284 return _browser.close(); |
| 234 }); | 285 }); |
| 235 final _closeMemoizer = new AsyncMemoizer(); | 286 final _closeMemoizer = new AsyncMemoizer(); |
| 236 } | 287 } |
| 237 | 288 |
| 238 /// An implementation of [Environment] for the browser. | 289 /// An implementation of [Environment] for the browser. |
| 239 /// | 290 /// |
| 240 /// All methods forward directly to [BrowserManager]. | 291 /// All methods forward directly to [BrowserManager]. |
| 241 class _BrowserEnvironment implements Environment { | 292 class _BrowserEnvironment implements Environment { |
| 242 final BrowserManager _manager; | 293 final BrowserManager _manager; |
| 243 | 294 |
| 244 final supportsDebugging = true; | 295 final supportsDebugging = true; |
| 245 | 296 |
| 246 final Uri observatoryUrl; | 297 final Uri observatoryUrl; |
| 247 | 298 |
| 248 final Uri remoteDebuggerUrl; | 299 final Uri remoteDebuggerUrl; |
| 249 | 300 |
| 250 _BrowserEnvironment(this._manager, this.observatoryUrl, | 301 _BrowserEnvironment(this._manager, this.observatoryUrl, |
| 251 this.remoteDebuggerUrl); | 302 this.remoteDebuggerUrl); |
| 252 | 303 |
| 253 CancelableOperation displayPause() => _manager._displayPause(); | 304 CancelableOperation displayPause() => _manager._displayPause(); |
| 254 } | 305 } |
| OLD | NEW |