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(); |
- } |
- }); |
- } |
-} |