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 |