| OLD | NEW |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import 'dart:async'; | 5 import 'dart:async'; |
| 6 import 'dart:convert'; | 6 import 'dart:convert'; |
| 7 import 'dart:io'; | 7 import 'dart:io'; |
| 8 | 8 |
| 9 import 'package:async/async.dart'; | 9 import 'package:async/async.dart'; |
| 10 import 'package:http_multi_server/http_multi_server.dart'; | 10 import 'package:http_multi_server/http_multi_server.dart'; |
| 11 import 'package:path/path.dart' as p; | 11 import 'package:path/path.dart' as p; |
| 12 import 'package:pool/pool.dart'; | 12 import 'package:pool/pool.dart'; |
| 13 import 'package:shelf/shelf.dart' as shelf; | 13 import 'package:shelf/shelf.dart' as shelf; |
| 14 import 'package:shelf/shelf_io.dart' as shelf_io; | 14 import 'package:shelf/shelf_io.dart' as shelf_io; |
| 15 import 'package:shelf_static/shelf_static.dart'; | 15 import 'package:shelf_static/shelf_static.dart'; |
| 16 import 'package:shelf_web_socket/shelf_web_socket.dart'; | 16 import 'package:shelf_web_socket/shelf_web_socket.dart'; |
| 17 import 'package:stream_channel/stream_channel.dart'; |
| 17 | 18 |
| 18 import '../../backend/metadata.dart'; | 19 import '../../backend/metadata.dart'; |
| 19 import '../../backend/suite.dart'; | |
| 20 import '../../backend/test_platform.dart'; | 20 import '../../backend/test_platform.dart'; |
| 21 import '../../util/io.dart'; | 21 import '../../util/io.dart'; |
| 22 import '../../util/one_off_handler.dart'; | 22 import '../../util/one_off_handler.dart'; |
| 23 import '../../util/path_handler.dart'; | 23 import '../../util/path_handler.dart'; |
| 24 import '../../util/stack_trace_mapper.dart'; | 24 import '../../util/stack_trace_mapper.dart'; |
| 25 import '../../utils.dart'; | 25 import '../../utils.dart'; |
| 26 import '../configuration.dart'; | 26 import '../configuration.dart'; |
| 27 import '../load_exception.dart'; | 27 import '../load_exception.dart'; |
| 28 import '../plugin/platform.dart'; |
| 29 import '../runner_suite.dart'; |
| 28 import 'browser_manager.dart'; | 30 import 'browser_manager.dart'; |
| 29 import 'compiler_pool.dart'; | 31 import 'compiler_pool.dart'; |
| 30 import 'polymer.dart'; | 32 import 'polymer.dart'; |
| 31 | 33 |
| 32 /// A server that serves JS-compiled tests to browsers. | 34 class BrowserPlatform extends PlatformPlugin { |
| 33 /// | |
| 34 /// A test suite may be loaded for a given file using [loadSuite]. | |
| 35 class BrowserServer { | |
| 36 /// Starts the server. | 35 /// Starts the server. |
| 37 /// | 36 /// |
| 38 /// [root] is the root directory that the server should serve. It defaults to | 37 /// [root] is the root directory that the server should serve. It defaults to |
| 39 /// the working directory. | 38 /// the working directory. |
| 40 static Future<BrowserServer> start(Configuration config, {String root}) | 39 static Future<BrowserPlatform> start(Configuration config, {String root}) |
| 41 async { | 40 async { |
| 42 var server = new shelf_io.IOServer(await HttpMultiServer.loopback(0)); | 41 var server = new shelf_io.IOServer(await HttpMultiServer.loopback(0)); |
| 43 return new BrowserServer(server, config, root: root); | 42 return new BrowserPlatform._(server, config, root: root); |
| 44 } | 43 } |
| 45 | 44 |
| 46 /// The underlying server. | 45 /// The underlying server. |
| 47 final shelf.Server _server; | 46 final shelf.Server _server; |
| 48 | 47 |
| 49 /// A randomly-generated secret. | 48 /// A randomly-generated secret. |
| 50 /// | 49 /// |
| 51 /// This is used to ensure that other users on the same system can't snoop | 50 /// This is used to ensure that other users on the same system can't snoop |
| 52 /// on data being served through this server. | 51 /// on data being served through this server. |
| 53 final _secret = randomBase64(24, urlSafe: true); | 52 final _secret = randomBase64(24, urlSafe: true); |
| (...skipping 30 matching lines...) Expand all Loading... |
| 84 /// Pub itself ensures that only one compilation runs at a time; we just use | 83 /// Pub itself ensures that only one compilation runs at a time; we just use |
| 85 /// this pool to make sure that the output is nice and linear. | 84 /// this pool to make sure that the output is nice and linear. |
| 86 final _pubServePool = new Pool(1); | 85 final _pubServePool = new Pool(1); |
| 87 | 86 |
| 88 /// The HTTP client to use when caching JS files in `pub serve`. | 87 /// The HTTP client to use when caching JS files in `pub serve`. |
| 89 final HttpClient _http; | 88 final HttpClient _http; |
| 90 | 89 |
| 91 /// Whether [close] has been called. | 90 /// Whether [close] has been called. |
| 92 bool get _closed => _closeMemo.hasRun; | 91 bool get _closed => _closeMemo.hasRun; |
| 93 | 92 |
| 94 /// The memoizer for running [close] exactly once. | |
| 95 final _closeMemo = new AsyncMemoizer(); | |
| 96 | |
| 97 /// A map from browser identifiers to futures that will complete to the | 93 /// A map from browser identifiers to futures that will complete to the |
| 98 /// [BrowserManager]s for those browsers, or the errors that occurred when | 94 /// [BrowserManager]s for those browsers, or the errors that occurred when |
| 99 /// trying to load those managers. | 95 /// trying to load those managers. |
| 100 /// | 96 /// |
| 101 /// This should only be accessed through [_browserManagerFor]. | 97 /// This should only be accessed through [_browserManagerFor]. |
| 102 final _browserManagers = | 98 final _browserManagers = |
| 103 new Map<TestPlatform, Future<Result<BrowserManager>>>(); | 99 new Map<TestPlatform, Future<Result<BrowserManager>>>(); |
| 104 | 100 |
| 105 /// A map from test suite paths to Futures that will complete once those | 101 /// A map from test suite paths to Futures that will complete once those |
| 106 /// suites are finished compiling. | 102 /// suites are finished compiling. |
| 107 /// | 103 /// |
| 108 /// This is used to make sure that a given test suite is only compiled once | 104 /// This is used to make sure that a given test suite is only compiled once |
| 109 /// per run, rather than once per browser per run. | 105 /// per run, rather than once per browser per run. |
| 110 final _compileFutures = new Map<String, Future>(); | 106 final _compileFutures = new Map<String, Future>(); |
| 111 | 107 |
| 108 /// Mappers for Dartifying stack traces, indexed by test path. |
| 112 final _mappers = new Map<String, StackTraceMapper>(); | 109 final _mappers = new Map<String, StackTraceMapper>(); |
| 113 | 110 |
| 114 BrowserServer(this._server, Configuration config, {String root}) | 111 BrowserPlatform._(this._server, Configuration config, {String root}) |
| 115 : _root = root == null ? p.current : root, | 112 : _root = root == null ? p.current : root, |
| 116 _config = config, | 113 _config = config, |
| 117 _compiledDir = config.pubServeUrl == null ? createTempDir() : null, | 114 _compiledDir = config.pubServeUrl == null ? createTempDir() : null, |
| 118 _http = config.pubServeUrl == null ? null : new HttpClient(), | 115 _http = config.pubServeUrl == null ? null : new HttpClient(), |
| 119 _compilers = new CompilerPool(color: config.color) { | 116 _compilers = new CompilerPool(color: config.color) { |
| 120 var cascade = new shelf.Cascade() | 117 var cascade = new shelf.Cascade() |
| 121 .add(_webSocketHandler.handler); | 118 .add(_webSocketHandler.handler); |
| 122 | 119 |
| 123 if (_config.pubServeUrl == null) { | 120 if (_config.pubServeUrl == null) { |
| 124 cascade = cascade | 121 cascade = cascade |
| (...skipping 29 matching lines...) Expand all Loading... |
| 154 | 151 |
| 155 return new shelf.Response.notFound("Not found."); | 152 return new shelf.Response.notFound("Not found."); |
| 156 }; | 153 }; |
| 157 } | 154 } |
| 158 | 155 |
| 159 /// A handler that serves wrapper files used to bootstrap tests. | 156 /// A handler that serves wrapper files used to bootstrap tests. |
| 160 shelf.Response _wrapperHandler(shelf.Request request) { | 157 shelf.Response _wrapperHandler(shelf.Request request) { |
| 161 var path = p.fromUri(request.url); | 158 var path = p.fromUri(request.url); |
| 162 | 159 |
| 163 if (path.endsWith(".browser_test.dart")) { | 160 if (path.endsWith(".browser_test.dart")) { |
| 161 var testPath = p.basename(p.withoutExtension(p.withoutExtension(path))); |
| 164 return new shelf.Response.ok(''' | 162 return new shelf.Response.ok(''' |
| 165 import "package:test/src/runner/browser/iframe_listener.dart"; | 163 import "package:stream_channel/stream_channel.dart"; |
| 166 | 164 |
| 167 import "${p.basename(p.withoutExtension(p.withoutExtension(path)))}" as test; | 165 import "package:test/src/runner/plugin/remote_platform_helpers.dart"; |
| 166 import "package:test/src/runner/browser/post_message_channel.dart"; |
| 168 | 167 |
| 169 void main() { | 168 import "$testPath" as test; |
| 170 IframeListener.start(() => test.main); | 169 |
| 171 } | 170 void main() { |
| 172 ''', headers: {'Content-Type': 'application/dart'}); | 171 var channel = serializeSuite(() => test.main, hidePrints: false); |
| 172 postMessageChannel().pipe(channel); |
| 173 } |
| 174 ''', headers: {'Content-Type': 'application/dart'}); |
| 173 } | 175 } |
| 174 | 176 |
| 175 if (path.endsWith(".html")) { | 177 if (path.endsWith(".html")) { |
| 176 var test = p.withoutExtension(path) + ".dart"; | 178 var test = p.withoutExtension(path) + ".dart"; |
| 177 | 179 |
| 178 // Link to the Dart wrapper on Dartium and the compiled JS version | 180 // Link to the Dart wrapper on Dartium and the compiled JS version |
| 179 // elsewhere. | 181 // elsewhere. |
| 180 var scriptBase = | 182 var scriptBase = |
| 181 "${HTML_ESCAPE.convert(p.basename(test))}.browser_test.dart"; | 183 "${HTML_ESCAPE.convert(p.basename(test))}.browser_test.dart"; |
| 182 var script = request.headers['user-agent'].contains('(Dart)') | 184 var script = request.headers['user-agent'].contains('(Dart)') |
| 183 ? 'type="application/dart" src="$scriptBase"' | 185 ? 'type="application/dart" src="$scriptBase"' |
| 184 : 'src="$scriptBase.js"'; | 186 : 'src="$scriptBase.js"'; |
| 185 | 187 |
| 186 return new shelf.Response.ok(''' | 188 return new shelf.Response.ok(''' |
| 187 <!DOCTYPE html> | 189 <!DOCTYPE html> |
| 188 <html> | 190 <html> |
| 189 <head> | 191 <head> |
| 190 <title>${HTML_ESCAPE.convert(test)} Test</title> | 192 <title>${HTML_ESCAPE.convert(test)} Test</title> |
| 191 <script $script></script> | 193 <script $script></script> |
| 192 </head> | 194 </head> |
| 193 </html> | 195 </html> |
| 194 ''', headers: {'Content-Type': 'text/html'}); | 196 ''', headers: {'Content-Type': 'text/html'}); |
| 195 } | 197 } |
| 196 | 198 |
| 197 return new shelf.Response.notFound('Not found.'); | 199 return new shelf.Response.notFound('Not found.'); |
| 198 } | 200 } |
| 199 | 201 |
| 200 /// Loads the test suite at [path] on the browser [browser]. | 202 /// Loads the test suite at [path] on the browser [browser]. |
| 201 /// | 203 /// |
| 202 /// This will start a browser to load the suite if one isn't already running. | 204 /// This will start a browser to load the suite if one isn't already running. |
| 203 /// Throws an [ArgumentError] if [browser] isn't a browser platform. | 205 /// Throws an [ArgumentError] if [browser] isn't a browser platform. |
| 204 Future<Suite> loadSuite(String path, TestPlatform browser, | 206 Future<RunnerSuite> load(String path, TestPlatform browser, |
| 205 Metadata metadata) async { | 207 Metadata metadata) async { |
| 206 if (!browser.isBrowser) { | 208 if (!browser.isBrowser) { |
| 207 throw new ArgumentError("$browser is not a browser."); | 209 throw new ArgumentError("$browser is not a browser."); |
| 208 } | 210 } |
| 209 | 211 |
| 210 var htmlPath = p.withoutExtension(path) + '.html'; | 212 var htmlPath = p.withoutExtension(path) + '.html'; |
| 211 if (new File(htmlPath).existsSync() && | 213 if (new File(htmlPath).existsSync() && |
| 212 !new File(htmlPath).readAsStringSync() | 214 !new File(htmlPath).readAsStringSync() |
| 213 .contains('packages/test/dart.js')) { | 215 .contains('packages/test/dart.js')) { |
| 214 throw new LoadException( | 216 throw new LoadException( |
| (...skipping 29 matching lines...) Expand all Loading... |
| 244 suiteUrl = url.resolveUri(p.toUri( | 246 suiteUrl = url.resolveUri(p.toUri( |
| 245 p.withoutExtension(p.relative(path, from: _root)) + ".html")); | 247 p.withoutExtension(p.relative(path, from: _root)) + ".html")); |
| 246 } | 248 } |
| 247 | 249 |
| 248 if (_closed) return null; | 250 if (_closed) return null; |
| 249 | 251 |
| 250 // TODO(nweiz): Don't start the browser until all the suites are compiled. | 252 // TODO(nweiz): Don't start the browser until all the suites are compiled. |
| 251 var browserManager = await _browserManagerFor(browser); | 253 var browserManager = await _browserManagerFor(browser); |
| 252 if (_closed) return null; | 254 if (_closed) return null; |
| 253 | 255 |
| 254 var suite = await browserManager.loadSuite(path, suiteUrl, metadata, | 256 var suite = await browserManager.load(path, suiteUrl, metadata, |
| 255 mapper: browser.isJS ? _mappers[path] : null); | 257 mapper: browser.isJS ? _mappers[path] : null); |
| 256 if (_closed) return null; | 258 if (_closed) return null; |
| 257 return suite; | 259 return suite; |
| 258 } | 260 } |
| 259 | 261 |
| 262 StreamChannel loadChannel(String path, TestPlatform platform) => |
| 263 throw new UnimplementedError(); |
| 264 |
| 260 /// Loads a test suite at [path] from the `pub serve` URL [dartUrl]. | 265 /// Loads a test suite at [path] from the `pub serve` URL [dartUrl]. |
| 261 /// | 266 /// |
| 262 /// This ensures that only one suite is loaded at a time, and that any errors | 267 /// This ensures that only one suite is loaded at a time, and that any errors |
| 263 /// are exposed as [LoadException]s. | 268 /// are exposed as [LoadException]s. |
| 264 Future _pubServeSuite(String path, Uri dartUrl, TestPlatform browser) { | 269 Future _pubServeSuite(String path, Uri dartUrl, TestPlatform browser) { |
| 265 return _pubServePool.withResource(() async { | 270 return _pubServePool.withResource(() async { |
| 266 var timer = new Timer(new Duration(seconds: 1), () { | 271 var timer = new Timer(new Duration(seconds: 1), () { |
| 267 print('"pub serve" is compiling $path...'); | 272 print('"pub serve" is compiling $path...'); |
| 268 }); | 273 }); |
| 269 | 274 |
| (...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 382 // an explicit [Result] fixes that. | 387 // an explicit [Result] fixes that. |
| 383 _browserManagers[platform] = Result.capture(future); | 388 _browserManagers[platform] = Result.capture(future); |
| 384 | 389 |
| 385 return future; | 390 return future; |
| 386 } | 391 } |
| 387 | 392 |
| 388 /// Close all the browsers that the server currently has open. | 393 /// Close all the browsers that the server currently has open. |
| 389 /// | 394 /// |
| 390 /// Note that this doesn't close the server itself. Browser tests can still be | 395 /// Note that this doesn't close the server itself. Browser tests can still be |
| 391 /// loaded, they'll just spawn new browsers. | 396 /// loaded, they'll just spawn new browsers. |
| 392 Future closeBrowsers() { | 397 Future closeEphemeral() { |
| 393 var managers = _browserManagers.values.toList(); | 398 var managers = _browserManagers.values.toList(); |
| 394 _browserManagers.clear(); | 399 _browserManagers.clear(); |
| 395 return Future.wait(managers.map((manager) async { | 400 return Future.wait(managers.map((manager) async { |
| 396 var result = await manager; | 401 var result = await manager; |
| 397 if (result.isError) return; | 402 if (result.isError) return; |
| 398 await result.asValue.value.close(); | 403 await result.asValue.value.close(); |
| 399 })); | 404 })); |
| 400 } | 405 } |
| 401 | 406 |
| 402 /// Closes the server and releases all its resources. | 407 /// Closes the server and releases all its resources. |
| 403 /// | 408 /// |
| 404 /// Returns a [Future] that completes once the server is closed and its | 409 /// Returns a [Future] that completes once the server is closed and its |
| 405 /// resources have been fully released. | 410 /// resources have been fully released. |
| 406 Future close() { | 411 Future close() => _closeMemo.runOnce(() async { |
| 407 return _closeMemo.runOnce(() async { | 412 var futures = _browserManagers.values.map((future) async { |
| 408 var futures = _browserManagers.values.map((future) async { | 413 var result = await future; |
| 409 var result = await future; | 414 if (result.isError) return; |
| 410 if (result.isError) return; | |
| 411 | 415 |
| 412 await result.asValue.value.close(); | 416 await result.asValue.value.close(); |
| 413 }).toList(); | 417 }).toList(); |
| 414 | 418 |
| 415 futures.add(_server.close()); | 419 futures.add(_server.close()); |
| 416 futures.add(_compilers.close()); | 420 futures.add(_compilers.close()); |
| 417 | 421 |
| 418 await Future.wait(futures); | 422 await Future.wait(futures); |
| 419 | 423 |
| 420 if (_config.pubServeUrl == null) { | 424 if (_config.pubServeUrl == null) { |
| 421 new Directory(_compiledDir).deleteSync(recursive: true); | 425 new Directory(_compiledDir).deleteSync(recursive: true); |
| 422 } else { | 426 } else { |
| 423 _http.close(); | 427 _http.close(); |
| 424 } | 428 } |
| 425 }); | 429 }); |
| 426 } | 430 final _closeMemo = new AsyncMemoizer(); |
| 427 } | 431 } |
| OLD | NEW |