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

Side by Side Diff: mojo/public/dart/third_party/test/lib/src/runner/browser/server.dart

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

Powered by Google App Engine
This is Rietveld 408576698