Index: mojo/public/dart/third_party/test/lib/src/runner/browser/server.dart |
diff --git a/mojo/public/dart/third_party/test/lib/src/runner/browser/server.dart b/mojo/public/dart/third_party/test/lib/src/runner/browser/server.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..658236b08c649369c23433b65174ba6360828b4c |
--- /dev/null |
+++ b/mojo/public/dart/third_party/test/lib/src/runner/browser/server.dart |
@@ -0,0 +1,415 @@ |
+// 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.server; |
+ |
+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 BrowserServer._(root, config); |
+ await server._load(); |
+ return server; |
+ } |
+ |
+ /// The underlying HTTP server. |
+ HttpServer _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 => baseUrlForAddress(_server.address, _server.port) |
+ .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._(String root, Configuration config) |
+ : _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); |
+ |
+ /// Starts the underlying server. |
+ Future _load() async { |
+ 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 = await HttpMultiServer.loopback(0); |
+ shelf_io.serveRequests(_server, 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.withoutExtension( |
+ p.relative(path, from: p.join(_root, 'test'))); |
+ |
+ var jsUrl; |
+ // 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)) { |
+ jsUrl = _config.pubServeUrl.resolve( |
+ "$suitePrefix.html.polymer.bootstrap.dart.browser_test.dart.js"); |
+ } else { |
+ jsUrl = _config.pubServeUrl.resolve( |
+ '$suitePrefix.dart.browser_test.dart.js'); |
+ } |
+ |
+ await _pubServeSuite(path, jsUrl); |
+ 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 [jsUrl]. |
+ /// |
+ /// 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 jsUrl) { |
+ return _pubServePool.withResource(() async { |
+ var timer = new Timer(new Duration(seconds: 1), () { |
+ print('"pub serve" is compiling $path...'); |
+ }); |
+ |
+ var mapUrl = jsUrl.replace(path: jsUrl.path + '.map'); |
+ var response; |
+ try { |
+ // Get the source map here 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 request = await _http.getUrl(mapUrl); |
+ 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 $mapUrl: ${response.statusCode} " |
+ "${response.reasonPhrase}\n" |
+ 'Make sure "pub serve" is serving the test/ directory.'); |
+ } |
+ |
+ if (_config.jsTrace) { |
+ // Drain the response stream. |
+ response.listen((_) {}); |
+ return; |
+ } |
+ |
+ _mappers[path] = new StackTraceMapper( |
+ await UTF8.decodeStream(response), |
+ mapUrl: mapUrl, |
+ packageRoot: _config.pubServeUrl.resolve('packages'), |
+ sdkRoot: _config.pubServeUrl.resolve('packages/\$sdk')); |
+ } 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 $mapUrl: $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; |
+ } |
+ |
+ /// 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(); |
+ } |
+ }); |
+ } |
+} |