Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1415)

Side by Side Diff: lib/src/runner/browser/server.dart

Issue 1704773002: Load web tests using the plugin infrastructure. (Closed) Base URL: git@github.com:dart-lang/test@master
Patch Set: Created 4 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 import 'dart:async';
6 import 'dart:convert';
7 import 'dart:io';
8
9 import 'package:async/async.dart';
10 import 'package:http_multi_server/http_multi_server.dart';
11 import 'package:path/path.dart' as p;
12 import 'package:pool/pool.dart';
13 import 'package:shelf/shelf.dart' as shelf;
14 import 'package:shelf/shelf_io.dart' as shelf_io;
15 import 'package:shelf_static/shelf_static.dart';
16 import 'package:shelf_web_socket/shelf_web_socket.dart';
17
18 import '../../backend/metadata.dart';
19 import '../../backend/suite.dart';
20 import '../../backend/test_platform.dart';
21 import '../../util/io.dart';
22 import '../../util/one_off_handler.dart';
23 import '../../util/path_handler.dart';
24 import '../../util/stack_trace_mapper.dart';
25 import '../../utils.dart';
26 import '../configuration.dart';
27 import '../load_exception.dart';
28 import 'browser_manager.dart';
29 import 'compiler_pool.dart';
30 import 'polymer.dart';
31
32 /// A server that serves JS-compiled tests to browsers.
33 ///
34 /// A test suite may be loaded for a given file using [loadSuite].
35 class BrowserServer {
36 /// Starts the server.
37 ///
38 /// [root] is the root directory that the server should serve. It defaults to
39 /// the working directory.
40 static Future<BrowserServer> start(Configuration config, {String root})
41 async {
42 var server = new shelf_io.IOServer(await HttpMultiServer.loopback(0));
43 return new BrowserServer(server, config, root: root);
44 }
45
46 /// The underlying server.
47 final shelf.Server _server;
48
49 /// A randomly-generated secret.
50 ///
51 /// This is used to ensure that other users on the same system can't snoop
52 /// on data being served through this server.
53 final _secret = randomBase64(24, urlSafe: true);
54
55 /// The URL for this server.
56 Uri get url => _server.url.resolve(_secret + "/");
57
58 /// The test runner configuration.
59 Configuration _config;
60
61 /// A [OneOffHandler] for servicing WebSocket connections for
62 /// [BrowserManager]s.
63 ///
64 /// This is one-off because each [BrowserManager] can only connect to a single
65 /// WebSocket,
66 final _webSocketHandler = new OneOffHandler();
67
68 /// A [PathHandler] used to serve compiled JS.
69 final _jsHandler = new PathHandler();
70
71 /// The [CompilerPool] managing active instances of `dart2js`.
72 ///
73 /// This is `null` if tests are loaded from `pub serve`.
74 final CompilerPool _compilers;
75
76 /// The temporary directory in which compiled JS is emitted.
77 final String _compiledDir;
78
79 /// The root directory served statically by this server.
80 final String _root;
81
82 /// The pool of active `pub serve` compilations.
83 ///
84 /// 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.
86 final _pubServePool = new Pool(1);
87
88 /// The HTTP client to use when caching JS files in `pub serve`.
89 final HttpClient _http;
90
91 /// Whether [close] has been called.
92 bool get _closed => _closeMemo.hasRun;
93
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
98 /// [BrowserManager]s for those browsers, or the errors that occurred when
99 /// trying to load those managers.
100 ///
101 /// This should only be accessed through [_browserManagerFor].
102 final _browserManagers =
103 new Map<TestPlatform, Future<Result<BrowserManager>>>();
104
105 /// A map from test suite paths to Futures that will complete once those
106 /// suites are finished compiling.
107 ///
108 /// 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.
110 final _compileFutures = new Map<String, Future>();
111
112 final _mappers = new Map<String, StackTraceMapper>();
113
114 BrowserServer(this._server, Configuration config, {String root})
115 : _root = root == null ? p.current : root,
116 _config = config,
117 _compiledDir = config.pubServeUrl == null ? createTempDir() : null,
118 _http = config.pubServeUrl == null ? null : new HttpClient(),
119 _compilers = new CompilerPool(color: config.color) {
120 var cascade = new shelf.Cascade()
121 .add(_webSocketHandler.handler);
122
123 if (_config.pubServeUrl == null) {
124 cascade = cascade
125 .add(_createPackagesHandler())
126 .add(_jsHandler.handler)
127 .add(createStaticHandler(_root))
128 .add(_wrapperHandler);
129 }
130
131 var pipeline = new shelf.Pipeline()
132 .addMiddleware(nestingMiddleware(_secret))
133 .addHandler(cascade.handler);
134
135 _server.mount(pipeline);
136 }
137
138 /// Returns a handler that serves the contents of the "packages/" directory
139 /// for any URL that contains "packages/".
140 ///
141 /// This is a factory so it can wrap a static handler.
142 shelf.Handler _createPackagesHandler() {
143 var staticHandler =
144 createStaticHandler(_config.packageRoot, serveFilesOutsidePath: true);
145
146 return (request) {
147 var segments = p.url.split(request.url.path);
148
149 for (var i = 0; i < segments.length; i++) {
150 if (segments[i] != "packages") continue;
151 return staticHandler(
152 request.change(path: p.url.joinAll(segments.take(i + 1))));
153 }
154
155 return new shelf.Response.notFound("Not found.");
156 };
157 }
158
159 /// A handler that serves wrapper files used to bootstrap tests.
160 shelf.Response _wrapperHandler(shelf.Request request) {
161 var path = p.fromUri(request.url);
162
163 if (path.endsWith(".browser_test.dart")) {
164 return new shelf.Response.ok('''
165 import "package:test/src/runner/browser/iframe_listener.dart";
166
167 import "${p.basename(p.withoutExtension(p.withoutExtension(path)))}" as test;
168
169 void main() {
170 IframeListener.start(() => test.main);
171 }
172 ''', headers: {'Content-Type': 'application/dart'});
173 }
174
175 if (path.endsWith(".html")) {
176 var test = p.withoutExtension(path) + ".dart";
177
178 // Link to the Dart wrapper on Dartium and the compiled JS version
179 // elsewhere.
180 var scriptBase =
181 "${HTML_ESCAPE.convert(p.basename(test))}.browser_test.dart";
182 var script = request.headers['user-agent'].contains('(Dart)')
183 ? 'type="application/dart" src="$scriptBase"'
184 : 'src="$scriptBase.js"';
185
186 return new shelf.Response.ok('''
187 <!DOCTYPE html>
188 <html>
189 <head>
190 <title>${HTML_ESCAPE.convert(test)} Test</title>
191 <script $script></script>
192 </head>
193 </html>
194 ''', headers: {'Content-Type': 'text/html'});
195 }
196
197 return new shelf.Response.notFound('Not found.');
198 }
199
200 /// Loads the test suite at [path] on the browser [browser].
201 ///
202 /// 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.
204 Future<Suite> loadSuite(String path, TestPlatform browser,
205 Metadata metadata) async {
206 if (!browser.isBrowser) {
207 throw new ArgumentError("$browser is not a browser.");
208 }
209
210 var htmlPath = p.withoutExtension(path) + '.html';
211 if (new File(htmlPath).existsSync() &&
212 !new File(htmlPath).readAsStringSync()
213 .contains('packages/test/dart.js')) {
214 throw new LoadException(
215 path,
216 '"${htmlPath}" must contain <script src="packages/test/dart.js">'
217 '</script>.');
218 }
219
220 var suiteUrl;
221 if (_config.pubServeUrl != null) {
222 var suitePrefix = p.toUri(p.withoutExtension(
223 p.relative(path, from: p.join(_root, 'test')))).path;
224
225 var dartUrl;
226 // Polymer generates a bootstrap entrypoint that wraps the entrypoint we
227 // see on disk, and modifies the HTML file to point to the bootstrap
228 // instead. To make sure we get the right source maps and wait for the
229 // right file to compile, we have some Polymer-specific logic here to load
230 // the boostrap instead of the unwrapped file.
231 if (isPolymerEntrypoint(path)) {
232 dartUrl = _config.pubServeUrl.resolve(
233 "$suitePrefix.html.polymer.bootstrap.dart.browser_test.dart");
234 } else {
235 dartUrl = _config.pubServeUrl.resolve(
236 '$suitePrefix.dart.browser_test.dart');
237 }
238
239 await _pubServeSuite(path, dartUrl, browser);
240 suiteUrl = _config.pubServeUrl.resolveUri(p.toUri('$suitePrefix.html'));
241 } else {
242 if (browser.isJS) await _compileSuite(path);
243 if (_closed) return null;
244 suiteUrl = url.resolveUri(p.toUri(
245 p.withoutExtension(p.relative(path, from: _root)) + ".html"));
246 }
247
248 if (_closed) return null;
249
250 // TODO(nweiz): Don't start the browser until all the suites are compiled.
251 var browserManager = await _browserManagerFor(browser);
252 if (_closed) return null;
253
254 var suite = await browserManager.loadSuite(path, suiteUrl, metadata,
255 mapper: browser.isJS ? _mappers[path] : null);
256 if (_closed) return null;
257 return suite;
258 }
259
260 /// Loads a test suite at [path] from the `pub serve` URL [dartUrl].
261 ///
262 /// This ensures that only one suite is loaded at a time, and that any errors
263 /// are exposed as [LoadException]s.
264 Future _pubServeSuite(String path, Uri dartUrl, TestPlatform browser) {
265 return _pubServePool.withResource(() async {
266 var timer = new Timer(new Duration(seconds: 1), () {
267 print('"pub serve" is compiling $path...');
268 });
269
270 // For browsers that run Dart compiled to JavaScript, get the source map
271 // instead of the Dart code for two reasons. We want to verify that the
272 // server's dart2js compiler is running on the Dart code, and also load
273 // the StackTraceMapper.
274 var getSourceMap = browser.isJS;
275
276 var url = getSourceMap
277 ? dartUrl.replace(path: dartUrl.path + '.js.map')
278 : dartUrl;
279
280 var response;
281 try {
282 var request = await _http.getUrl(url);
283 response = await request.close();
284
285 if (response.statusCode != 200) {
286 // We don't care about the response body, but we have to drain it or
287 // else the process can't exit.
288 response.listen((_) {});
289
290 throw new LoadException(path,
291 "Error getting $url: ${response.statusCode} "
292 "${response.reasonPhrase}\n"
293 'Make sure "pub serve" is serving the test/ directory.');
294 }
295
296 if (getSourceMap && !_config.jsTrace) {
297 _mappers[path] = new StackTraceMapper(
298 await UTF8.decodeStream(response),
299 mapUrl: url,
300 packageRoot: _config.pubServeUrl.resolve('packages'),
301 sdkRoot: _config.pubServeUrl.resolve('packages/\$sdk'));
302 return;
303 }
304
305 // Drain the response stream.
306 response.listen((_) {});
307 } on IOException catch (error) {
308 var message = getErrorMessage(error);
309 if (error is SocketException) {
310 message = "${error.osError.message} "
311 "(errno ${error.osError.errorCode})";
312 }
313
314 throw new LoadException(path,
315 "Error getting $url: $message\n"
316 'Make sure "pub serve" is running.');
317 } finally {
318 timer.cancel();
319 }
320 });
321 }
322
323 /// Compile the test suite at [dartPath] to JavaScript.
324 ///
325 /// Once the suite has been compiled, it's added to [_jsHandler] so it can be
326 /// served.
327 Future _compileSuite(String dartPath) {
328 return _compileFutures.putIfAbsent(dartPath, () async {
329 var dir = new Directory(_compiledDir).createTempSync('test_').path;
330 var jsPath = p.join(dir, p.basename(dartPath) + ".browser_test.dart.js");
331
332 await _compilers.compile(dartPath, jsPath,
333 packageRoot: _config.packageRoot);
334 if (_closed) return;
335
336 var jsUrl = p.toUri(p.relative(dartPath, from: _root)).path +
337 '.browser_test.dart.js';
338 _jsHandler.add(jsUrl, (request) {
339 return new shelf.Response.ok(new File(jsPath).readAsStringSync(),
340 headers: {'Content-Type': 'application/javascript'});
341 });
342
343 var mapUrl = p.toUri(p.relative(dartPath, from: _root)).path +
344 '.browser_test.dart.js.map';
345 _jsHandler.add(mapUrl, (request) {
346 return new shelf.Response.ok(
347 new File(jsPath + '.map').readAsStringSync(),
348 headers: {'Content-Type': 'application/json'});
349 });
350
351 if (_config.jsTrace) return;
352 var mapPath = jsPath + '.map';
353 _mappers[dartPath] = new StackTraceMapper(
354 new File(mapPath).readAsStringSync(),
355 mapUrl: p.toUri(mapPath),
356 packageRoot: p.toUri(_config.packageRoot),
357 sdkRoot: p.toUri(sdkDir));
358 });
359 }
360
361 /// Returns the [BrowserManager] for [platform], which should be a browser.
362 ///
363 /// If no browser manager is running yet, starts one.
364 Future<BrowserManager> _browserManagerFor(TestPlatform platform) {
365 var manager = _browserManagers[platform];
366 if (manager != null) return Result.release(manager);
367
368 var completer = new Completer.sync();
369 var path = _webSocketHandler.create(webSocketHandler(completer.complete));
370 var webSocketUrl = url.replace(scheme: 'ws').resolve(path);
371 var hostUrl = (_config.pubServeUrl == null ? url : _config.pubServeUrl)
372 .resolve('packages/test/src/runner/browser/static/index.html')
373 .replace(queryParameters: {'managerUrl': webSocketUrl.toString()});
374
375 var future = BrowserManager.start(platform, hostUrl, completer.future,
376 debug: _config.pauseAfterLoad);
377
378 // Capture errors and release them later to avoid Zone issues. This call to
379 // [_browserManagerFor] is running in a different [LoadSuite] than future
380 // calls, which means they're also running in different error zones so
381 // errors can't be freely passed between them. Storing the error or value as
382 // an explicit [Result] fixes that.
383 _browserManagers[platform] = Result.capture(future);
384
385 return future;
386 }
387
388 /// Close all the browsers that the server currently has open.
389 ///
390 /// Note that this doesn't close the server itself. Browser tests can still be
391 /// loaded, they'll just spawn new browsers.
392 Future closeBrowsers() {
393 var managers = _browserManagers.values.toList();
394 _browserManagers.clear();
395 return Future.wait(managers.map((manager) async {
396 var result = await manager;
397 if (result.isError) return;
398 await result.asValue.value.close();
399 }));
400 }
401
402 /// Closes the server and releases all its resources.
403 ///
404 /// Returns a [Future] that completes once the server is closed and its
405 /// resources have been fully released.
406 Future close() {
407 return _closeMemo.runOnce(() async {
408 var futures = _browserManagers.values.map((future) async {
409 var result = await future;
410 if (result.isError) return;
411
412 await result.asValue.value.close();
413 }).toList();
414
415 futures.add(_server.close());
416 futures.add(_compilers.close());
417
418 await Future.wait(futures);
419
420 if (_config.pubServeUrl == null) {
421 new Directory(_compiledDir).deleteSync(recursive: true);
422 } else {
423 _http.close();
424 }
425 });
426 }
427 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698