Index: mojo/public/dart/third_party/test/lib/src/runner/browser/browser_manager.dart |
diff --git a/mojo/public/dart/third_party/test/lib/src/runner/browser/browser_manager.dart b/mojo/public/dart/third_party/test/lib/src/runner/browser/browser_manager.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..7498efad5b352f9564f2abeb180637fcce176820 |
--- /dev/null |
+++ b/mojo/public/dart/third_party/test/lib/src/runner/browser/browser_manager.dart |
@@ -0,0 +1,300 @@ |
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+library test.runner.browser.browser_manager; |
+ |
+import 'dart:async'; |
+import 'dart:convert'; |
+ |
+import 'package:async/async.dart'; |
+import 'package:http_parser/http_parser.dart'; |
+import 'package:pool/pool.dart'; |
+ |
+import '../../backend/metadata.dart'; |
+import '../../backend/test_platform.dart'; |
+import '../../util/cancelable_future.dart'; |
+import '../../util/multi_channel.dart'; |
+import '../../util/remote_exception.dart'; |
+import '../../util/stack_trace_mapper.dart'; |
+import '../../utils.dart'; |
+import '../application_exception.dart'; |
+import '../environment.dart'; |
+import '../load_exception.dart'; |
+import '../runner_suite.dart'; |
+import 'browser.dart'; |
+import 'chrome.dart'; |
+import 'content_shell.dart'; |
+import 'dartium.dart'; |
+import 'firefox.dart'; |
+import 'iframe_test.dart'; |
+import 'internet_explorer.dart'; |
+import 'phantom_js.dart'; |
+import 'safari.dart'; |
+ |
+/// A class that manages the connection to a single running browser. |
+/// |
+/// This is in charge of telling the browser which test suites to load and |
+/// converting its responses into [Suite] objects. |
+class BrowserManager { |
+ /// The browser instance that this is connected to via [_channel]. |
+ final Browser _browser; |
+ |
+ // TODO(nweiz): Consider removing the duplication between this and |
+ // [_browser.name]. |
+ /// The [TestPlatform] for [_browser]. |
+ final TestPlatform _platform; |
+ |
+ /// The channel used to communicate with the browser. |
+ /// |
+ /// This is connected to a page running `static/host.dart`. |
+ final MultiChannel _channel; |
+ |
+ /// A pool that ensures that limits the number of initial connections the |
+ /// manager will wait for at once. |
+ /// |
+ /// This isn't the *total* number of connections; any number of iframes may be |
+ /// loaded in the same browser. However, the browser can only load so many at |
+ /// once, and we want a timeout in case they fail so we only wait for so many |
+ /// at once. |
+ final _pool = new Pool(8); |
+ |
+ /// The ID of the next suite to be loaded. |
+ /// |
+ /// This is used to ensure that the suites can be referred to consistently |
+ /// across the client and server. |
+ int _suiteId = 0; |
+ |
+ /// Whether the channel to the browser has closed. |
+ bool _closed = false; |
+ |
+ /// The completer for [_BrowserEnvironment.displayPause]. |
+ /// |
+ /// This will be `null` as long as the browser isn't displaying a pause |
+ /// screen. |
+ CancelableCompleter _pauseCompleter; |
+ |
+ /// The environment to attach to each suite. |
+ Future<_BrowserEnvironment> _environment; |
+ |
+ /// Starts the browser identified by [platform] and has it connect to [url]. |
+ /// |
+ /// [url] should serve a page that establishes a WebSocket connection with |
+ /// this process. That connection, once established, should be emitted via |
+ /// [future]. If [debug] is true, starts the browser in debug mode, with its |
+ /// debugger interfaces on and detected. |
+ /// |
+ /// Returns the browser manager, or throws an [ApplicationException] if a |
+ /// connection fails to be established. |
+ static Future<BrowserManager> start(TestPlatform platform, Uri url, |
+ Future<CompatibleWebSocket> future, {bool debug: false}) { |
+ var browser = _newBrowser(url, platform, debug: debug); |
+ |
+ var completer = new Completer(); |
+ |
+ // TODO(nweiz): Gracefully handle the browser being killed before the |
+ // tests complete. |
+ browser.onExit.then((_) { |
+ throw new ApplicationException( |
+ "${platform.name} exited before connecting."); |
+ }).catchError((error, stackTrace) { |
+ if (completer.isCompleted) return; |
+ completer.completeError(error, stackTrace); |
+ }); |
+ |
+ future.then((webSocket) { |
+ if (completer.isCompleted) return; |
+ completer.complete(new BrowserManager._(browser, platform, webSocket)); |
+ }).catchError((error, stackTrace) { |
+ browser.close(); |
+ if (completer.isCompleted) return; |
+ completer.completeError(error, stackTrace); |
+ }); |
+ |
+ return completer.future.timeout(new Duration(seconds: 30), onTimeout: () { |
+ browser.close(); |
+ throw new ApplicationException( |
+ "Timed out waiting for ${platform.name} to connect."); |
+ }); |
+ } |
+ |
+ /// Starts the browser identified by [browser] and has it load [url]. |
+ /// |
+ /// If [debug] is true, starts the browser in debug mode. |
+ static Browser _newBrowser(Uri url, TestPlatform browser, |
+ {bool debug: false}) { |
+ switch (browser) { |
+ case TestPlatform.dartium: return new Dartium(url, debug: debug); |
+ case TestPlatform.contentShell: |
+ return new ContentShell(url, debug: debug); |
+ case TestPlatform.chrome: return new Chrome(url); |
+ case TestPlatform.phantomJS: return new PhantomJS(url, debug: debug); |
+ case TestPlatform.firefox: return new Firefox(url); |
+ case TestPlatform.safari: return new Safari(url); |
+ case TestPlatform.internetExplorer: return new InternetExplorer(url); |
+ default: |
+ throw new ArgumentError("$browser is not a browser."); |
+ } |
+ } |
+ |
+ /// Creates a new BrowserManager that communicates with [browser] over |
+ /// [webSocket]. |
+ BrowserManager._(this._browser, this._platform, CompatibleWebSocket webSocket) |
+ : _channel = new MultiChannel( |
+ webSocket.map(JSON.decode), |
+ mapSink(webSocket, JSON.encode)) { |
+ _environment = _loadBrowserEnvironment(); |
+ _channel.stream.listen(_onMessage, onDone: close); |
+ } |
+ |
+ /// Loads [_BrowserEnvironment]. |
+ Future<_BrowserEnvironment> _loadBrowserEnvironment() async { |
+ var observatoryUrl; |
+ if (_platform.isDartVM) observatoryUrl = await _browser.observatoryUrl; |
+ |
+ var remoteDebuggerUrl; |
+ if (_platform.isHeadless) { |
+ remoteDebuggerUrl = await _browser.remoteDebuggerUrl; |
+ } |
+ |
+ return new _BrowserEnvironment(this, observatoryUrl, remoteDebuggerUrl); |
+ } |
+ |
+ /// Tells the browser the load a test suite from the URL [url]. |
+ /// |
+ /// [url] should be an HTML page with a reference to the JS-compiled test |
+ /// suite. [path] is the path of the original test suite file, which is used |
+ /// for reporting. [metadata] is the parsed metadata for the test suite. |
+ /// |
+ /// If [mapper] is passed, it's used to map stack traces for errors coming |
+ /// from this test suite. |
+ Future<RunnerSuite> loadSuite(String path, Uri url, Metadata metadata, |
+ {StackTraceMapper mapper}) async { |
+ url = url.replace(fragment: Uri.encodeFull(JSON.encode({ |
+ "metadata": metadata.serialize(), |
+ "browser": _platform.identifier |
+ }))); |
+ |
+ // The stream may close before emitting a value if the browser is killed |
+ // prematurely (e.g. via Control-C). |
+ var suiteVirtualChannel = _channel.virtualChannel(); |
+ var suiteId = _suiteId++; |
+ var suiteChannel; |
+ |
+ closeIframe() { |
+ if (_closed) return; |
+ suiteChannel.sink.close(); |
+ _channel.sink.add({ |
+ "command": "closeSuite", |
+ "id": suiteId |
+ }); |
+ } |
+ |
+ var response = await _pool.withResource(() { |
+ _channel.sink.add({ |
+ "command": "loadSuite", |
+ "url": url.toString(), |
+ "id": suiteId, |
+ "channel": suiteVirtualChannel.id |
+ }); |
+ |
+ // Create a nested MultiChannel because the iframe will be using a channel |
+ // wrapped within the host's channel. |
+ suiteChannel = new MultiChannel( |
+ suiteVirtualChannel.stream, suiteVirtualChannel.sink); |
+ |
+ var completer = new Completer(); |
+ suiteChannel.stream.listen((response) { |
+ if (response["type"] == "print") { |
+ print(response["line"]); |
+ } else { |
+ completer.complete(response); |
+ } |
+ }, onDone: () { |
+ if (!completer.isCompleted) completer.complete(); |
+ }); |
+ |
+ return completer.future.timeout(new Duration(minutes: 1), onTimeout: () { |
+ throw new LoadException( |
+ path, |
+ "Timed out waiting for the test suite to connect on " |
+ "${_platform.name}."); |
+ }); |
+ }); |
+ |
+ if (response == null) { |
+ closeIframe(); |
+ throw new LoadException( |
+ path, "Connection closed before test suite loaded."); |
+ } |
+ |
+ if (response["type"] == "loadException") { |
+ closeIframe(); |
+ throw new LoadException(path, response["message"]); |
+ } |
+ |
+ if (response["type"] == "error") { |
+ closeIframe(); |
+ var asyncError = RemoteException.deserialize(response["error"]); |
+ await new Future.error( |
+ new LoadException(path, asyncError.error), |
+ asyncError.stackTrace); |
+ } |
+ |
+ return new RunnerSuite(await _environment, response["tests"].map((test) { |
+ var testMetadata = new Metadata.deserialize(test['metadata']); |
+ var testChannel = suiteChannel.virtualChannel(test['channel']); |
+ return new IframeTest(test['name'], testMetadata, testChannel, |
+ mapper: mapper); |
+ }), platform: _platform, metadata: metadata, path: path, |
+ onClose: () => closeIframe()); |
+ } |
+ |
+ /// An implementation of [Environment.displayPause]. |
+ CancelableFuture _displayPause() { |
+ if (_pauseCompleter != null) return _pauseCompleter.future; |
+ |
+ _pauseCompleter = new CancelableCompleter(() { |
+ _channel.sink.add({"command": "resume"}); |
+ _pauseCompleter = null; |
+ }); |
+ |
+ _channel.sink.add({"command": "displayPause"}); |
+ return _pauseCompleter.future.whenComplete(() { |
+ _pauseCompleter = null; |
+ }); |
+ } |
+ |
+ /// The callback for handling messages received from the host page. |
+ void _onMessage(Map message) { |
+ assert(message["command"] == "resume"); |
+ if (_pauseCompleter == null) return; |
+ _pauseCompleter.complete(); |
+ } |
+ |
+ /// Closes the manager and releases any resources it owns, including closing |
+ /// the browser. |
+ Future close() => _closeMemoizer.runOnce(() { |
+ _closed = true; |
+ if (_pauseCompleter != null) _pauseCompleter.complete(); |
+ _pauseCompleter = null; |
+ return _browser.close(); |
+ }); |
+ final _closeMemoizer = new AsyncMemoizer(); |
+} |
+ |
+/// An implementation of [Environment] for the browser. |
+/// |
+/// All methods forward directly to [BrowserManager]. |
+class _BrowserEnvironment implements Environment { |
+ final BrowserManager _manager; |
+ |
+ final Uri observatoryUrl; |
+ |
+ final Uri remoteDebuggerUrl; |
+ |
+ _BrowserEnvironment(this._manager, this.observatoryUrl, |
+ this.remoteDebuggerUrl); |
+ |
+ CancelableFuture displayPause() => _manager._displayPause(); |
+} |