OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, 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 library test.runner.browser.server; | 5 library test.runner.browser.server; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:convert'; | 8 import 'dart:convert'; |
9 import 'dart:io'; | 9 import 'dart:io'; |
10 | 10 |
11 import 'package:async/async.dart'; | 11 import 'package:async/async.dart'; |
12 import 'package:http_multi_server/http_multi_server.dart'; | 12 import 'package:http_multi_server/http_multi_server.dart'; |
13 import 'package:path/path.dart' as p; | 13 import 'package:path/path.dart' as p; |
14 import 'package:pool/pool.dart'; | 14 import 'package:pool/pool.dart'; |
15 import 'package:shelf/shelf.dart' as shelf; | 15 import 'package:shelf/shelf.dart' as shelf; |
16 import 'package:shelf/shelf_io.dart' as shelf_io; | 16 import 'package:shelf/shelf_io.dart' as shelf_io; |
17 import 'package:shelf_static/shelf_static.dart'; | 17 import 'package:shelf_static/shelf_static.dart'; |
18 import 'package:shelf_web_socket/shelf_web_socket.dart'; | 18 import 'package:shelf_web_socket/shelf_web_socket.dart'; |
19 | 19 |
20 import '../../backend/metadata.dart'; | 20 import '../../backend/metadata.dart'; |
21 import '../../backend/suite.dart'; | 21 import '../../backend/suite.dart'; |
22 import '../../backend/test_platform.dart'; | 22 import '../../backend/test_platform.dart'; |
23 import '../../util/io.dart'; | 23 import '../../util/io.dart'; |
24 import '../../util/one_off_handler.dart'; | 24 import '../../util/one_off_handler.dart'; |
25 import '../../util/path_handler.dart'; | 25 import '../../util/path_handler.dart'; |
26 import '../../util/stack_trace_mapper.dart'; | 26 import '../../util/stack_trace_mapper.dart'; |
27 import '../../utils.dart'; | 27 import '../../utils.dart'; |
28 import '../application_exception.dart'; | 28 import '../application_exception.dart'; |
29 import '../load_exception.dart'; | 29 import '../load_exception.dart'; |
30 import 'browser.dart'; | |
31 import 'browser_manager.dart'; | 30 import 'browser_manager.dart'; |
32 import 'compiler_pool.dart'; | 31 import 'compiler_pool.dart'; |
33 import 'chrome.dart'; | |
34 import 'content_shell.dart'; | |
35 import 'dartium.dart'; | |
36 import 'firefox.dart'; | |
37 import 'internet_explorer.dart'; | |
38 import 'phantom_js.dart'; | |
39 import 'polymer.dart'; | 32 import 'polymer.dart'; |
40 import 'safari.dart'; | |
41 | 33 |
42 /// A server that serves JS-compiled tests to browsers. | 34 /// A server that serves JS-compiled tests to browsers. |
43 /// | 35 /// |
44 /// A test suite may be loaded for a given file using [loadSuite]. | 36 /// A test suite may be loaded for a given file using [loadSuite]. |
45 class BrowserServer { | 37 class BrowserServer { |
46 /// Starts the server. | 38 /// Starts the server. |
47 /// | 39 /// |
48 /// [root] is the root directory that the server should serve. It defaults to | 40 /// [root] is the root directory that the server should serve. It defaults to |
49 /// the working directory. | 41 /// the working directory. |
50 /// | 42 /// |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
121 | 113 |
122 /// The HTTP client to use when caching JS files in `pub serve`. | 114 /// The HTTP client to use when caching JS files in `pub serve`. |
123 final HttpClient _http; | 115 final HttpClient _http; |
124 | 116 |
125 /// Whether [close] has been called. | 117 /// Whether [close] has been called. |
126 bool get _closed => _closeMemo.hasRun; | 118 bool get _closed => _closeMemo.hasRun; |
127 | 119 |
128 /// The memoizer for running [close] exactly once. | 120 /// The memoizer for running [close] exactly once. |
129 final _closeMemo = new AsyncMemoizer(); | 121 final _closeMemo = new AsyncMemoizer(); |
130 | 122 |
131 /// All currently-running browsers. | |
132 /// | |
133 /// These are controlled by [_browserManager]s. | |
134 final _browsers = new Map<TestPlatform, Browser>(); | |
135 | |
136 /// A map from browser identifiers to futures that will complete to the | 123 /// A map from browser identifiers to futures that will complete to the |
137 /// [BrowserManager]s for those browsers, or the errors that occurred when | 124 /// [BrowserManager]s for those browsers, or the errors that occurred when |
138 /// trying to load those managers. | 125 /// trying to load those managers. |
139 /// | 126 /// |
140 /// This should only be accessed through [_browserManagerFor]. | 127 /// This should only be accessed through [_browserManagerFor]. |
141 final _browserManagers = | 128 final _browserManagers = |
142 new Map<TestPlatform, Future<Result<BrowserManager>>>(); | 129 new Map<TestPlatform, Future<Result<BrowserManager>>>(); |
143 | 130 |
144 /// A map from test suite paths to Futures that will complete once those | 131 /// A map from test suite paths to Futures that will complete once those |
145 /// suites are finished compiling. | 132 /// suites are finished compiling. |
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
289 suiteUrl = url.resolveUri(p.toUri( | 276 suiteUrl = url.resolveUri(p.toUri( |
290 p.withoutExtension(p.relative(path, from: _root)) + ".html")); | 277 p.withoutExtension(p.relative(path, from: _root)) + ".html")); |
291 } | 278 } |
292 | 279 |
293 if (_closed) return null; | 280 if (_closed) return null; |
294 | 281 |
295 // TODO(nweiz): Don't start the browser until all the suites are compiled. | 282 // TODO(nweiz): Don't start the browser until all the suites are compiled. |
296 var browserManager = await _browserManagerFor(browser); | 283 var browserManager = await _browserManagerFor(browser); |
297 if (_closed) return null; | 284 if (_closed) return null; |
298 | 285 |
299 if (browserManager != null) { | 286 var suite = await browserManager.loadSuite(path, suiteUrl, metadata, |
300 var suite = await browserManager.loadSuite(path, suiteUrl, metadata, | 287 mapper: browser.isJS ? _mappers[path] : null); |
301 mapper: browser.isJS ? _mappers[path] : null); | 288 if (_closed) return null; |
302 if (_closed) return null; | 289 return suite; |
303 if (suite != null) return suite; | |
304 } | |
305 | |
306 // If the browser manager fails to load a suite and the server isn't | |
307 // closed, it's probably because the browser failed. We emit the failure | |
308 // here to ensure that it gets surfaced. | |
309 return _browsers[browser].onExit; | |
310 } | 290 } |
311 | 291 |
312 /// Loads a test suite at [path] from the `pub serve` URL [jsUrl]. | 292 /// Loads a test suite at [path] from the `pub serve` URL [jsUrl]. |
313 /// | 293 /// |
314 /// This ensures that only one suite is loaded at a time, and that any errors | 294 /// This ensures that only one suite is loaded at a time, and that any errors |
315 /// are exposed as [LoadException]s. | 295 /// are exposed as [LoadException]s. |
316 Future _pubServeSuite(String path, Uri jsUrl) { | 296 Future _pubServeSuite(String path, Uri jsUrl) { |
317 return _pubServePool.withResource(() async { | 297 return _pubServePool.withResource(() async { |
318 var timer = new Timer(new Duration(seconds: 1), () { | 298 var timer = new Timer(new Duration(seconds: 1), () { |
319 print('"pub serve" is compiling $path...'); | 299 print('"pub serve" is compiling $path...'); |
(...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
403 }); | 383 }); |
404 } | 384 } |
405 | 385 |
406 /// Returns the [BrowserManager] for [platform], which should be a browser. | 386 /// Returns the [BrowserManager] for [platform], which should be a browser. |
407 /// | 387 /// |
408 /// If no browser manager is running yet, starts one. | 388 /// If no browser manager is running yet, starts one. |
409 Future<BrowserManager> _browserManagerFor(TestPlatform platform) { | 389 Future<BrowserManager> _browserManagerFor(TestPlatform platform) { |
410 var manager = _browserManagers[platform]; | 390 var manager = _browserManagers[platform]; |
411 if (manager != null) return Result.release(manager); | 391 if (manager != null) return Result.release(manager); |
412 | 392 |
413 var completer = new Completer(); | 393 var completer = new Completer.sync(); |
| 394 var path = _webSocketHandler.create(webSocketHandler(completer.complete)); |
| 395 var webSocketUrl = url.replace(scheme: 'ws').resolve(path); |
| 396 var hostUrl = (_pubServeUrl == null ? url : _pubServeUrl) |
| 397 .resolve('packages/test/src/runner/browser/static/index.html') |
| 398 .replace(queryParameters: {'managerUrl': webSocketUrl.toString()}); |
| 399 |
| 400 var future = BrowserManager.start(platform, hostUrl, completer.future); |
414 | 401 |
415 // Capture errors and release them later to avoid Zone issues. This call to | 402 // Capture errors and release them later to avoid Zone issues. This call to |
416 // [_browserManagerFor] is running in a different [LoadSuite] than future | 403 // [_browserManagerFor] is running in a different [LoadSuite] than future |
417 // calls, which means they're also running in different error zones so | 404 // calls, which means they're also running in different error zones so |
418 // errors can't be freely passed between them. Storing the error or value as | 405 // errors can't be freely passed between them. Storing the error or value as |
419 // an explicit [Result] fixes that. | 406 // an explicit [Result] fixes that. |
420 _browserManagers[platform] = Result.capture(completer.future); | 407 _browserManagers[platform] = Result.capture(future); |
421 var path = _webSocketHandler.create(webSocketHandler((webSocket) { | |
422 completer.complete(new BrowserManager(platform, webSocket)); | |
423 })); | |
424 | 408 |
425 var webSocketUrl = url.replace(scheme: 'ws').resolve(path); | 409 return future; |
426 | |
427 var hostUrl = (_pubServeUrl == null ? url : _pubServeUrl) | |
428 .resolve('packages/test/src/runner/browser/static/index.html'); | |
429 | |
430 var browser = _newBrowser(hostUrl.replace(queryParameters: { | |
431 'managerUrl': webSocketUrl.toString() | |
432 }), platform); | |
433 _browsers[platform] = browser; | |
434 | |
435 // TODO(nweiz): Gracefully handle the browser being killed before the | |
436 // tests complete. | |
437 browser.onExit.then((_) { | |
438 if (completer.isCompleted) return; | |
439 if (!_closed) return; | |
440 completer.complete(null); | |
441 }).catchError((error, stackTrace) { | |
442 if (completer.isCompleted) return; | |
443 completer.completeError(error, stackTrace); | |
444 }); | |
445 | |
446 return completer.future.timeout(new Duration(seconds: 30), onTimeout: () { | |
447 throw new ApplicationException( | |
448 "Timed out waiting for ${platform.name} to connect."); | |
449 }); | |
450 } | |
451 | |
452 /// Starts the browser identified by [browser] and has it load [url]. | |
453 Browser _newBrowser(Uri url, TestPlatform browser) { | |
454 switch (browser) { | |
455 case TestPlatform.dartium: return new Dartium(url); | |
456 case TestPlatform.contentShell: return new ContentShell(url); | |
457 case TestPlatform.chrome: return new Chrome(url); | |
458 case TestPlatform.phantomJS: return new PhantomJS(url); | |
459 case TestPlatform.firefox: return new Firefox(url); | |
460 case TestPlatform.safari: return new Safari(url); | |
461 case TestPlatform.internetExplorer: return new InternetExplorer(url); | |
462 default: | |
463 throw new ArgumentError("$browser is not a browser."); | |
464 } | |
465 } | 410 } |
466 | 411 |
467 /// Closes the server and releases all its resources. | 412 /// Closes the server and releases all its resources. |
468 /// | 413 /// |
469 /// Returns a [Future] that completes once the server is closed and its | 414 /// Returns a [Future] that completes once the server is closed and its |
470 /// resources have been fully released. | 415 /// resources have been fully released. |
471 Future close() { | 416 Future close() { |
472 return _closeMemo.runOnce(() async { | 417 return _closeMemo.runOnce(() async { |
473 var futures = _browsers.values.map((browser) => browser.close()).toList(); | 418 var futures = _browserManagers.values.map((future) async { |
| 419 var result = await future; |
| 420 if (result.isError) return; |
| 421 |
| 422 await result.asValue.value.close(); |
| 423 }).toList(); |
474 | 424 |
475 futures.add(_server.close()); | 425 futures.add(_server.close()); |
476 futures.add(_compilers.close()); | 426 futures.add(_compilers.close()); |
477 | 427 |
478 await Future.wait(futures); | 428 await Future.wait(futures); |
479 | 429 |
480 if (_pubServeUrl == null) { | 430 if (_pubServeUrl == null) { |
481 new Directory(_compiledDir).deleteSync(recursive: true); | 431 new Directory(_compiledDir).deleteSync(recursive: true); |
482 } else { | 432 } else { |
483 _http.close(); | 433 _http.close(); |
484 } | 434 } |
485 }); | 435 }); |
486 } | 436 } |
487 } | 437 } |
OLD | NEW |