| Index: lib/src/runner/browser/server.dart
|
| diff --git a/lib/src/runner/browser/server.dart b/lib/src/runner/browser/server.dart
|
| deleted file mode 100644
|
| index 62085162adfb83410f4225d5f831223b7237eec1..0000000000000000000000000000000000000000
|
| --- a/lib/src/runner/browser/server.dart
|
| +++ /dev/null
|
| @@ -1,427 +0,0 @@
|
| -// 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.
|
| -
|
| -import 'dart:async';
|
| -import 'dart:convert';
|
| -import 'dart:io';
|
| -
|
| -import 'package:async/async.dart';
|
| -import 'package:http_multi_server/http_multi_server.dart';
|
| -import 'package:path/path.dart' as p;
|
| -import 'package:pool/pool.dart';
|
| -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/metadata.dart';
|
| -import '../../backend/suite.dart';
|
| -import '../../backend/test_platform.dart';
|
| -import '../../util/io.dart';
|
| -import '../../util/one_off_handler.dart';
|
| -import '../../util/path_handler.dart';
|
| -import '../../util/stack_trace_mapper.dart';
|
| -import '../../utils.dart';
|
| -import '../configuration.dart';
|
| -import '../load_exception.dart';
|
| -import 'browser_manager.dart';
|
| -import 'compiler_pool.dart';
|
| -import 'polymer.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.
|
| - ///
|
| - /// [root] is the root directory that the server should serve. It defaults to
|
| - /// the working directory.
|
| - static Future<BrowserServer> start(Configuration config, {String root})
|
| - async {
|
| - var server = new shelf_io.IOServer(await HttpMultiServer.loopback(0));
|
| - return new BrowserServer(server, config, root: root);
|
| - }
|
| -
|
| - /// The underlying server.
|
| - final shelf.Server _server;
|
| -
|
| - /// A randomly-generated secret.
|
| - ///
|
| - /// This is used to ensure that other users on the same system can't snoop
|
| - /// on data being served through this server.
|
| - final _secret = randomBase64(24, urlSafe: true);
|
| -
|
| - /// The URL for this server.
|
| - Uri get url => _server.url.resolve(_secret + "/");
|
| -
|
| - /// The test runner configuration.
|
| - Configuration _config;
|
| -
|
| - /// 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();
|
| -
|
| - /// A [PathHandler] used to serve compiled JS.
|
| - final _jsHandler = new PathHandler();
|
| -
|
| - /// The [CompilerPool] managing active instances of `dart2js`.
|
| - ///
|
| - /// This is `null` if tests are loaded from `pub serve`.
|
| - final CompilerPool _compilers;
|
| -
|
| - /// The temporary directory in which compiled JS is emitted.
|
| - final String _compiledDir;
|
| -
|
| - /// The root directory served statically by this server.
|
| - final String _root;
|
| -
|
| - /// The pool of active `pub serve` compilations.
|
| - ///
|
| - /// Pub itself ensures that only one compilation runs at a time; we just use
|
| - /// this pool to make sure that the output is nice and linear.
|
| - final _pubServePool = new Pool(1);
|
| -
|
| - /// The HTTP client to use when caching JS files in `pub serve`.
|
| - final HttpClient _http;
|
| -
|
| - /// Whether [close] has been called.
|
| - bool get _closed => _closeMemo.hasRun;
|
| -
|
| - /// The memoizer for running [close] exactly once.
|
| - final _closeMemo = new AsyncMemoizer();
|
| -
|
| - /// A map from browser identifiers to futures that will complete to the
|
| - /// [BrowserManager]s for those browsers, or the errors that occurred when
|
| - /// trying to load those managers.
|
| - ///
|
| - /// This should only be accessed through [_browserManagerFor].
|
| - final _browserManagers =
|
| - new Map<TestPlatform, Future<Result<BrowserManager>>>();
|
| -
|
| - /// A map from test suite paths to Futures that will complete once those
|
| - /// suites are finished compiling.
|
| - ///
|
| - /// This is used to make sure that a given test suite is only compiled once
|
| - /// per run, rather than once per browser per run.
|
| - final _compileFutures = new Map<String, Future>();
|
| -
|
| - final _mappers = new Map<String, StackTraceMapper>();
|
| -
|
| - BrowserServer(this._server, Configuration config, {String root})
|
| - : _root = root == null ? p.current : root,
|
| - _config = config,
|
| - _compiledDir = config.pubServeUrl == null ? createTempDir() : null,
|
| - _http = config.pubServeUrl == null ? null : new HttpClient(),
|
| - _compilers = new CompilerPool(color: config.color) {
|
| - var cascade = new shelf.Cascade()
|
| - .add(_webSocketHandler.handler);
|
| -
|
| - if (_config.pubServeUrl == null) {
|
| - cascade = cascade
|
| - .add(_createPackagesHandler())
|
| - .add(_jsHandler.handler)
|
| - .add(createStaticHandler(_root))
|
| - .add(_wrapperHandler);
|
| - }
|
| -
|
| - var pipeline = new shelf.Pipeline()
|
| - .addMiddleware(nestingMiddleware(_secret))
|
| - .addHandler(cascade.handler);
|
| -
|
| - _server.mount(pipeline);
|
| - }
|
| -
|
| - /// Returns a handler that serves the contents of the "packages/" directory
|
| - /// for any URL that contains "packages/".
|
| - ///
|
| - /// This is a factory so it can wrap a static handler.
|
| - shelf.Handler _createPackagesHandler() {
|
| - var staticHandler =
|
| - createStaticHandler(_config.packageRoot, serveFilesOutsidePath: true);
|
| -
|
| - return (request) {
|
| - var segments = p.url.split(request.url.path);
|
| -
|
| - for (var i = 0; i < segments.length; i++) {
|
| - if (segments[i] != "packages") continue;
|
| - return staticHandler(
|
| - request.change(path: p.url.joinAll(segments.take(i + 1))));
|
| - }
|
| -
|
| - return new shelf.Response.notFound("Not found.");
|
| - };
|
| - }
|
| -
|
| - /// A handler that serves wrapper files used to bootstrap tests.
|
| - shelf.Response _wrapperHandler(shelf.Request request) {
|
| - var path = p.fromUri(request.url);
|
| -
|
| - if (path.endsWith(".browser_test.dart")) {
|
| - return new shelf.Response.ok('''
|
| -import "package:test/src/runner/browser/iframe_listener.dart";
|
| -
|
| -import "${p.basename(p.withoutExtension(p.withoutExtension(path)))}" as test;
|
| -
|
| -void main() {
|
| - IframeListener.start(() => test.main);
|
| -}
|
| -''', headers: {'Content-Type': 'application/dart'});
|
| - }
|
| -
|
| - if (path.endsWith(".html")) {
|
| - var test = p.withoutExtension(path) + ".dart";
|
| -
|
| - // Link to the Dart wrapper on Dartium and the compiled JS version
|
| - // elsewhere.
|
| - var scriptBase =
|
| - "${HTML_ESCAPE.convert(p.basename(test))}.browser_test.dart";
|
| - var script = request.headers['user-agent'].contains('(Dart)')
|
| - ? 'type="application/dart" src="$scriptBase"'
|
| - : 'src="$scriptBase.js"';
|
| -
|
| - return new shelf.Response.ok('''
|
| -<!DOCTYPE html>
|
| -<html>
|
| -<head>
|
| - <title>${HTML_ESCAPE.convert(test)} Test</title>
|
| - <script $script></script>
|
| -</head>
|
| -</html>
|
| -''', headers: {'Content-Type': 'text/html'});
|
| - }
|
| -
|
| - return new shelf.Response.notFound('Not found.');
|
| - }
|
| -
|
| - /// Loads the test suite at [path] on the browser [browser].
|
| - ///
|
| - /// This will start a browser to load the suite if one isn't already running.
|
| - /// Throws an [ArgumentError] if [browser] isn't a browser platform.
|
| - Future<Suite> loadSuite(String path, TestPlatform browser,
|
| - Metadata metadata) async {
|
| - if (!browser.isBrowser) {
|
| - throw new ArgumentError("$browser is not a browser.");
|
| - }
|
| -
|
| - var htmlPath = p.withoutExtension(path) + '.html';
|
| - if (new File(htmlPath).existsSync() &&
|
| - !new File(htmlPath).readAsStringSync()
|
| - .contains('packages/test/dart.js')) {
|
| - throw new LoadException(
|
| - path,
|
| - '"${htmlPath}" must contain <script src="packages/test/dart.js">'
|
| - '</script>.');
|
| - }
|
| -
|
| - var suiteUrl;
|
| - if (_config.pubServeUrl != null) {
|
| - var suitePrefix = p.toUri(p.withoutExtension(
|
| - p.relative(path, from: p.join(_root, 'test')))).path;
|
| -
|
| - var dartUrl;
|
| - // Polymer generates a bootstrap entrypoint that wraps the entrypoint we
|
| - // see on disk, and modifies the HTML file to point to the bootstrap
|
| - // instead. To make sure we get the right source maps and wait for the
|
| - // right file to compile, we have some Polymer-specific logic here to load
|
| - // the boostrap instead of the unwrapped file.
|
| - if (isPolymerEntrypoint(path)) {
|
| - dartUrl = _config.pubServeUrl.resolve(
|
| - "$suitePrefix.html.polymer.bootstrap.dart.browser_test.dart");
|
| - } else {
|
| - dartUrl = _config.pubServeUrl.resolve(
|
| - '$suitePrefix.dart.browser_test.dart');
|
| - }
|
| -
|
| - await _pubServeSuite(path, dartUrl, browser);
|
| - suiteUrl = _config.pubServeUrl.resolveUri(p.toUri('$suitePrefix.html'));
|
| - } else {
|
| - if (browser.isJS) await _compileSuite(path);
|
| - if (_closed) return null;
|
| - suiteUrl = url.resolveUri(p.toUri(
|
| - p.withoutExtension(p.relative(path, from: _root)) + ".html"));
|
| - }
|
| -
|
| - if (_closed) return null;
|
| -
|
| - // TODO(nweiz): Don't start the browser until all the suites are compiled.
|
| - var browserManager = await _browserManagerFor(browser);
|
| - if (_closed) return null;
|
| -
|
| - var suite = await browserManager.loadSuite(path, suiteUrl, metadata,
|
| - mapper: browser.isJS ? _mappers[path] : null);
|
| - if (_closed) return null;
|
| - return suite;
|
| - }
|
| -
|
| - /// Loads a test suite at [path] from the `pub serve` URL [dartUrl].
|
| - ///
|
| - /// This ensures that only one suite is loaded at a time, and that any errors
|
| - /// are exposed as [LoadException]s.
|
| - Future _pubServeSuite(String path, Uri dartUrl, TestPlatform browser) {
|
| - return _pubServePool.withResource(() async {
|
| - var timer = new Timer(new Duration(seconds: 1), () {
|
| - print('"pub serve" is compiling $path...');
|
| - });
|
| -
|
| - // For browsers that run Dart compiled to JavaScript, get the source map
|
| - // instead of the Dart code for two reasons. We want to verify that the
|
| - // server's dart2js compiler is running on the Dart code, and also load
|
| - // the StackTraceMapper.
|
| - var getSourceMap = browser.isJS;
|
| -
|
| - var url = getSourceMap
|
| - ? dartUrl.replace(path: dartUrl.path + '.js.map')
|
| - : dartUrl;
|
| -
|
| - var response;
|
| - try {
|
| - var request = await _http.getUrl(url);
|
| - response = await request.close();
|
| -
|
| - if (response.statusCode != 200) {
|
| - // We don't care about the response body, but we have to drain it or
|
| - // else the process can't exit.
|
| - response.listen((_) {});
|
| -
|
| - throw new LoadException(path,
|
| - "Error getting $url: ${response.statusCode} "
|
| - "${response.reasonPhrase}\n"
|
| - 'Make sure "pub serve" is serving the test/ directory.');
|
| - }
|
| -
|
| - if (getSourceMap && !_config.jsTrace) {
|
| - _mappers[path] = new StackTraceMapper(
|
| - await UTF8.decodeStream(response),
|
| - mapUrl: url,
|
| - packageRoot: _config.pubServeUrl.resolve('packages'),
|
| - sdkRoot: _config.pubServeUrl.resolve('packages/\$sdk'));
|
| - return;
|
| - }
|
| -
|
| - // Drain the response stream.
|
| - response.listen((_) {});
|
| - } on IOException catch (error) {
|
| - var message = getErrorMessage(error);
|
| - if (error is SocketException) {
|
| - message = "${error.osError.message} "
|
| - "(errno ${error.osError.errorCode})";
|
| - }
|
| -
|
| - throw new LoadException(path,
|
| - "Error getting $url: $message\n"
|
| - 'Make sure "pub serve" is running.');
|
| - } finally {
|
| - timer.cancel();
|
| - }
|
| - });
|
| - }
|
| -
|
| - /// Compile the test suite at [dartPath] to JavaScript.
|
| - ///
|
| - /// Once the suite has been compiled, it's added to [_jsHandler] so it can be
|
| - /// served.
|
| - Future _compileSuite(String dartPath) {
|
| - return _compileFutures.putIfAbsent(dartPath, () async {
|
| - var dir = new Directory(_compiledDir).createTempSync('test_').path;
|
| - var jsPath = p.join(dir, p.basename(dartPath) + ".browser_test.dart.js");
|
| -
|
| - await _compilers.compile(dartPath, jsPath,
|
| - packageRoot: _config.packageRoot);
|
| - if (_closed) return;
|
| -
|
| - var jsUrl = p.toUri(p.relative(dartPath, from: _root)).path +
|
| - '.browser_test.dart.js';
|
| - _jsHandler.add(jsUrl, (request) {
|
| - return new shelf.Response.ok(new File(jsPath).readAsStringSync(),
|
| - headers: {'Content-Type': 'application/javascript'});
|
| - });
|
| -
|
| - var mapUrl = p.toUri(p.relative(dartPath, from: _root)).path +
|
| - '.browser_test.dart.js.map';
|
| - _jsHandler.add(mapUrl, (request) {
|
| - return new shelf.Response.ok(
|
| - new File(jsPath + '.map').readAsStringSync(),
|
| - headers: {'Content-Type': 'application/json'});
|
| - });
|
| -
|
| - if (_config.jsTrace) return;
|
| - var mapPath = jsPath + '.map';
|
| - _mappers[dartPath] = new StackTraceMapper(
|
| - new File(mapPath).readAsStringSync(),
|
| - mapUrl: p.toUri(mapPath),
|
| - packageRoot: p.toUri(_config.packageRoot),
|
| - sdkRoot: p.toUri(sdkDir));
|
| - });
|
| - }
|
| -
|
| - /// Returns the [BrowserManager] for [platform], which should be a browser.
|
| - ///
|
| - /// If no browser manager is running yet, starts one.
|
| - Future<BrowserManager> _browserManagerFor(TestPlatform platform) {
|
| - var manager = _browserManagers[platform];
|
| - if (manager != null) return Result.release(manager);
|
| -
|
| - var completer = new Completer.sync();
|
| - var path = _webSocketHandler.create(webSocketHandler(completer.complete));
|
| - var webSocketUrl = url.replace(scheme: 'ws').resolve(path);
|
| - var hostUrl = (_config.pubServeUrl == null ? url : _config.pubServeUrl)
|
| - .resolve('packages/test/src/runner/browser/static/index.html')
|
| - .replace(queryParameters: {'managerUrl': webSocketUrl.toString()});
|
| -
|
| - var future = BrowserManager.start(platform, hostUrl, completer.future,
|
| - debug: _config.pauseAfterLoad);
|
| -
|
| - // Capture errors and release them later to avoid Zone issues. This call to
|
| - // [_browserManagerFor] is running in a different [LoadSuite] than future
|
| - // calls, which means they're also running in different error zones so
|
| - // errors can't be freely passed between them. Storing the error or value as
|
| - // an explicit [Result] fixes that.
|
| - _browserManagers[platform] = Result.capture(future);
|
| -
|
| - return future;
|
| - }
|
| -
|
| - /// Close all the browsers that the server currently has open.
|
| - ///
|
| - /// Note that this doesn't close the server itself. Browser tests can still be
|
| - /// loaded, they'll just spawn new browsers.
|
| - Future closeBrowsers() {
|
| - var managers = _browserManagers.values.toList();
|
| - _browserManagers.clear();
|
| - return Future.wait(managers.map((manager) async {
|
| - var result = await manager;
|
| - if (result.isError) return;
|
| - await result.asValue.value.close();
|
| - }));
|
| - }
|
| -
|
| - /// 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() {
|
| - return _closeMemo.runOnce(() async {
|
| - var futures = _browserManagers.values.map((future) async {
|
| - var result = await future;
|
| - if (result.isError) return;
|
| -
|
| - await result.asValue.value.close();
|
| - }).toList();
|
| -
|
| - futures.add(_server.close());
|
| - futures.add(_compilers.close());
|
| -
|
| - await Future.wait(futures);
|
| -
|
| - if (_config.pubServeUrl == null) {
|
| - new Directory(_compiledDir).deleteSync(recursive: true);
|
| - } else {
|
| - _http.close();
|
| - }
|
| - });
|
| - }
|
| -}
|
|
|