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 |