Index: lib/src/runner/browser/server.dart |
diff --git a/lib/src/runner/browser/server.dart b/lib/src/runner/browser/server.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..5fdae5ed35b733b50e77b14942e7dcdab5f6d1ac |
--- /dev/null |
+++ b/lib/src/runner/browser/server.dart |
@@ -0,0 +1,160 @@ |
+// 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 unittest.runner.browser.server; |
+ |
+import 'dart:async'; |
+import 'dart:convert'; |
+import 'dart:io'; |
+ |
+import 'package:path/path.dart' as p; |
+import 'package:shelf/shelf.dart' as shelf; |
+import 'package:shelf/shelf_io.dart' as shelf_io; |
+import 'package:shelf_static/shelf_static.dart'; |
+import 'package:shelf_web_socket/shelf_web_socket.dart'; |
+ |
+import '../../backend/suite.dart'; |
+import '../../util/io.dart'; |
+import '../../util/one_off_handler.dart'; |
+import 'browser_manager.dart'; |
+import 'compiler_pool.dart'; |
+import 'chrome.dart'; |
+ |
+/// A server that serves JS-compiled tests to browsers. |
+/// |
+/// A test suite may be loaded for a given file using [loadSuite]. |
+class BrowserServer { |
+ /// Starts the server. |
+ /// |
+ /// If [packageRoot] is passed, it's used for all package imports when |
+ /// compiling tests to JS. Otherwise, the package root is inferred from the |
+ /// location of the source file. |
+ static Future<BrowserServer> start({String packageRoot}) { |
+ var server = new BrowserServer._(packageRoot); |
+ return server._load().then((_) => server); |
+ } |
+ |
+ /// The underlying HTTP server. |
+ HttpServer _server; |
+ |
+ /// The URL for this server. |
+ Uri get url => baseUrlForAddress(_server.address, _server.port); |
+ |
+ /// a [OneOffHandler] for servicing WebSocket connections for |
+ /// [BrowserManager]s. |
+ /// |
+ /// This is one-off because each [BrowserManager] can only connect to a single |
+ /// WebSocket, |
+ final _webSocketHandler = new OneOffHandler(); |
+ |
+ /// The [CompilerPool] managing active instances of `dart2js`. |
+ final _compilers = new CompilerPool(); |
+ |
+ /// The temporary directory in which compiled JS is emitted. |
+ final String _compiledDir; |
+ |
+ /// The package root which is passed to `dart2js`. |
+ final String _packageRoot; |
+ |
+ /// The browser in which test suites are loaded and run. |
+ /// |
+ /// This is `null` until a suite is loaded. |
+ Chrome _browser; |
+ |
+ /// A future that will complete to the [BrowserManager] for [_browser]. |
+ /// |
+ /// The first time this is called, it will start both the browser and the |
+ /// browser manager. Any further calls will return the existing manager. |
+ Future<BrowserManager> get _browserManager { |
+ if (_browserManagerCompleter == null) { |
+ _browserManagerCompleter = new Completer(); |
+ var path = _webSocketHandler.create(webSocketHandler((webSocket) { |
+ _browserManagerCompleter.complete(new BrowserManager(webSocket)); |
+ })); |
+ |
+ var webSocketUrl = url.replace(scheme: 'ws', path: '/$path'); |
+ _browser = new Chrome(url.replace(queryParameters: { |
+ 'managerUrl': webSocketUrl.toString() |
+ })); |
+ |
+ // TODO(nweiz): Gracefully handle the browser being killed before the |
+ // tests complete. |
+ _browser.onExit.catchError((error, stackTrace) { |
+ if (_browserManagerCompleter.isCompleted) return; |
+ _browserManagerCompleter.completeError(error, stackTrace); |
+ }); |
+ } |
+ return _browserManagerCompleter.future; |
+ } |
+ Completer<BrowserManager> _browserManagerCompleter; |
+ |
+ BrowserServer._(this._packageRoot) |
+ : _compiledDir = Directory.systemTemp.createTempSync('unittest_').path; |
+ |
+ /// Starts the underlying server. |
+ Future _load() { |
+ var staticPath = p.join(libDir(packageRoot: _packageRoot), |
+ 'src/runner/browser/static'); |
+ var cascade = new shelf.Cascade() |
+ .add(_webSocketHandler.handler) |
+ .add(createStaticHandler(staticPath, defaultDocument: 'index.html')) |
+ .add(createStaticHandler(_compiledDir, defaultDocument: 'index.html')); |
+ |
+ return shelf_io.serve(cascade.handler, 'localhost', 0).then((server) { |
+ _server = server; |
+ }); |
+ } |
+ |
+ /// Loads the test suite at [path]. |
+ /// |
+ /// This will start a browser to load the suite if one isn't already running. |
+ Future<Suite> loadSuite(String path) { |
+ return _compileSuite(path).then((dir) { |
+ // TODO(nweiz): Don't start the browser until all the suites are compiled. |
+ return _browserManager.then((browserManager) { |
+ // Add a trailing slash because at least on Chrome, the iframe's |
+ // window.location.href will do so automatically, and if that differs |
+ // from the original URL communication will fail. |
+ var suiteUrl = url.resolve( |
+ "/" + p.toUri(p.relative(dir, from: _compiledDir)).path + "/"); |
+ return browserManager.loadSuite(path, suiteUrl); |
+ }); |
+ }); |
+ } |
+ |
+ /// Compile the test suite at [dartPath] to JavaScript. |
+ /// |
+ /// Returns a [Future] that completes to the path to the JavaScript. |
+ Future<String> _compileSuite(String dartPath) { |
+ var dir = new Directory(_compiledDir).createTempSync('test_').path; |
+ var jsPath = p.join(dir, p.basename(dartPath) + ".js"); |
+ return _compilers.compile(dartPath, jsPath, |
+ packageRoot: packageRootFor(dartPath, _packageRoot)) |
+ .then((_) { |
+ // TODO(nweiz): support user-authored HTML files. |
+ new File(p.join(dir, "index.html")).writeAsStringSync(''' |
+<!DOCTYPE html> |
+<html> |
+<head> |
+ <title>${HTML_ESCAPE.convert(dartPath)} Test</title> |
+ <script src="${HTML_ESCAPE.convert(p.basename(jsPath))}"></script> |
+</head> |
+</html> |
+'''); |
+ return dir; |
+ }); |
+ } |
+ |
+ /// Closes the server and releases all its resources. |
+ /// |
+ /// Returns a [Future] that completes once the server is closed and its |
+ /// resources have been fully released. |
+ Future close() { |
+ new Directory(_compiledDir).deleteSync(recursive: true); |
+ return _server.close().then((_) { |
+ if (_browserManagerCompleter == null) return null; |
+ return _browserManager.then((_) => _browser.close()); |
+ }); |
+ } |
+} |