OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS d.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 pub_tests; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:convert'; | |
9 import 'dart:io'; | |
10 | |
11 import 'package:http/http.dart' as http; | |
12 import 'package:scheduled_test/scheduled_process.dart'; | |
13 import 'package:scheduled_test/scheduled_stream.dart'; | |
14 import 'package:scheduled_test/scheduled_test.dart'; | |
15 import 'package:stack_trace/stack_trace.dart'; | |
16 | |
17 import '../../lib/src/utils.dart'; | |
18 import '../descriptor.dart' as d; | |
19 import '../test_pub.dart'; | |
20 | |
21 /// The pub process running "pub serve". | |
22 ScheduledProcess _pubServer; | |
23 | |
24 /// The ephemeral port assign to the running admin server. | |
25 int _adminPort; | |
26 | |
27 /// The ephemeral ports assigned to the running servers, associated with the | |
28 /// directories they're serving. | |
29 final _ports = new Map<String, int>(); | |
30 | |
31 /// A completer that completes when the server has been started and the served | |
32 /// ports are known. | |
33 Completer _portsCompleter; | |
34 | |
35 /// The web socket connection to the running pub process, or `null` if no | |
36 /// connection has been made. | |
37 WebSocket _webSocket; | |
38 Stream _webSocketBroadcastStream; | |
39 | |
40 /// The code for a transformer that renames ".txt" files to ".out" and adds a | |
41 /// ".out" suffix. | |
42 const REWRITE_TRANSFORMER = """ | |
43 import 'dart:async'; | |
44 | |
45 import 'package:barback/barback.dart'; | |
46 | |
47 class RewriteTransformer extends Transformer { | |
48 RewriteTransformer.asPlugin(); | |
49 | |
50 String get allowedExtensions => '.txt'; | |
51 | |
52 Future apply(Transform transform) { | |
53 return transform.primaryInput.readAsString().then((contents) { | |
54 var id = transform.primaryInput.id.changeExtension(".out"); | |
55 transform.addOutput(new Asset.fromString(id, "\$contents.out")); | |
56 }); | |
57 } | |
58 } | |
59 """; | |
60 | |
61 /// The code for a lazy version of [REWRITE_TRANSFORMER]. | |
62 const LAZY_TRANSFORMER = """ | |
63 import 'dart:async'; | |
64 | |
65 import 'package:barback/barback.dart'; | |
66 | |
67 class LazyRewriteTransformer extends Transformer implements LazyTransformer { | |
68 LazyRewriteTransformer.asPlugin(); | |
69 | |
70 String get allowedExtensions => '.txt'; | |
71 | |
72 Future apply(Transform transform) { | |
73 transform.logger.info('Rewriting \${transform.primaryInput.id}.'); | |
74 return transform.primaryInput.readAsString().then((contents) { | |
75 var id = transform.primaryInput.id.changeExtension(".out"); | |
76 transform.addOutput(new Asset.fromString(id, "\$contents.out")); | |
77 }); | |
78 } | |
79 | |
80 Future declareOutputs(DeclaringTransform transform) { | |
81 transform.declareOutput(transform.primaryId.changeExtension(".out")); | |
82 return new Future.value(); | |
83 } | |
84 } | |
85 """; | |
86 | |
87 /// The web socket error code for a directory not being served. | |
88 const NOT_SERVED = 1; | |
89 | |
90 /// Returns the source code for a Dart library defining a Transformer that | |
91 /// rewrites Dart files. | |
92 /// | |
93 /// The transformer defines a constant named TOKEN whose value is [id]. When the | |
94 /// transformer transforms another Dart file, it will look for a "TOKEN" | |
95 /// constant definition there and modify it to include *this* transformer's | |
96 /// TOKEN value as well. | |
97 /// | |
98 /// If [import] is passed, it should be the name of a package that defines its | |
99 /// own TOKEN constant. The primary library of that package will be imported | |
100 /// here and its TOKEN value will be added to this library's. | |
101 /// | |
102 /// This transformer takes one configuration field: "addition". This is | |
103 /// concatenated to its TOKEN value before adding it to the output library. | |
104 String dartTransformer(String id, {String import}) { | |
105 if (import != null) { | |
106 id = '$id imports \${$import.TOKEN}'; | |
107 import = 'import "package:$import/$import.dart" as $import;'; | |
108 } else { | |
109 import = ''; | |
110 } | |
111 | |
112 return """ | |
113 import 'dart:async'; | |
114 | |
115 import 'package:barback/barback.dart'; | |
116 $import | |
117 | |
118 import 'dart:io'; | |
119 | |
120 const TOKEN = "$id"; | |
121 | |
122 final _tokenRegExp = new RegExp(r'^const TOKEN = "(.*?)";\$', multiLine: true); | |
123 | |
124 class DartTransformer extends Transformer { | |
125 final BarbackSettings _settings; | |
126 | |
127 DartTransformer.asPlugin(this._settings); | |
128 | |
129 String get allowedExtensions => '.dart'; | |
130 | |
131 Future apply(Transform transform) { | |
132 return transform.primaryInput.readAsString().then((contents) { | |
133 transform.addOutput(new Asset.fromString(transform.primaryInput.id, | |
134 contents.replaceAllMapped(_tokenRegExp, (match) { | |
135 var token = TOKEN; | |
136 var addition = _settings.configuration["addition"]; | |
137 if (addition != null) token += addition; | |
138 return 'const TOKEN = "(\${match[1]}, \$token)";'; | |
139 }))); | |
140 }); | |
141 } | |
142 } | |
143 """; | |
144 } | |
145 | |
146 /// Schedules starting the `pub serve` process. | |
147 /// | |
148 /// Unlike [pubServe], this doesn't determine the port number of the server, and | |
149 /// so may be used to test for errors in the initialization process. | |
150 /// | |
151 /// Returns the `pub serve` process. | |
152 ScheduledProcess startPubServe({Iterable<String> args, | |
153 bool createWebDir: true}) { | |
154 var pubArgs = [ | |
155 "serve", | |
156 "--port=0", // Use port 0 to get an ephemeral port. | |
157 "--force-poll", | |
158 "--admin-port=0", // Use port 0 to get an ephemeral port. | |
159 "--log-admin-url" | |
160 ]; | |
161 | |
162 if (args != null) pubArgs.addAll(args); | |
163 | |
164 // Dart2js can take a long time to compile dart code, so we increase the | |
165 // timeout to cope with that. | |
166 currentSchedule.timeout *= 1.5; | |
167 | |
168 if (createWebDir) d.dir(appPath, [d.dir("web")]).create(); | |
169 return startPub(args: pubArgs); | |
170 } | |
171 | |
172 /// Schedules starting the "pub serve" process and records its port number for | |
173 /// future requests. | |
174 /// | |
175 /// If [shouldGetFirst] is `true`, validates that pub get is run first. | |
176 /// | |
177 /// If [createWebDir] is `true`, creates a `web/` directory if one doesn't exist | |
178 /// so pub doesn't complain about having nothing to serve. | |
179 /// | |
180 /// Returns the `pub serve` process. | |
181 ScheduledProcess pubServe({bool shouldGetFirst: false, bool createWebDir: true, | |
182 Iterable<String> args}) { | |
183 _pubServer = startPubServe(args: args, createWebDir: createWebDir); | |
184 _portsCompleter = new Completer(); | |
185 | |
186 currentSchedule.onComplete.schedule(() { | |
187 _portsCompleter = null; | |
188 _ports.clear(); | |
189 | |
190 if (_webSocket != null) { | |
191 _webSocket.close(); | |
192 _webSocket = null; | |
193 _webSocketBroadcastStream = null; | |
194 } | |
195 }); | |
196 | |
197 if (shouldGetFirst) { | |
198 _pubServer.stdout.expect(consumeThrough(anyOf([ | |
199 "Got dependencies!", | |
200 matches(new RegExp(r"^Changed \d+ dependenc")) | |
201 ]))); | |
202 } | |
203 | |
204 _pubServer.stdout.expect(startsWith("Loading source assets...")); | |
205 _pubServer.stdout.expect(consumeWhile(matches("Loading .* transformers..."))); | |
206 | |
207 _pubServer.stdout.expect(predicate(_parseAdminPort)); | |
208 | |
209 // The server should emit one or more ports. | |
210 _pubServer.stdout.expect( | |
211 consumeWhile(predicate(_parsePort, 'emits server url'))); | |
212 schedule(() { | |
213 expect(_ports, isNot(isEmpty)); | |
214 _portsCompleter.complete(); | |
215 }); | |
216 | |
217 return _pubServer; | |
218 } | |
219 | |
220 /// The regular expression for parsing pub's output line describing the URL for | |
221 /// the server. | |
222 final _parsePortRegExp = new RegExp(r"([^ ]+) +on http://localhost:(\d+)"); | |
223 | |
224 /// Parses the port number from the "Running admin server on localhost:1234" | |
225 /// line printed by pub serve. | |
226 bool _parseAdminPort(String line) { | |
227 expect(line, startsWith('Running admin server on')); | |
228 var match = _parsePortRegExp.firstMatch(line); | |
229 if (match == null) return false; | |
230 _adminPort = int.parse(match[2]); | |
231 return true; | |
232 } | |
233 | |
234 /// Parses the port number from the "Serving blah on localhost:1234" line | |
235 /// printed by pub serve. | |
236 bool _parsePort(String line) { | |
237 var match = _parsePortRegExp.firstMatch(line); | |
238 if (match == null) return false; | |
239 _ports[match[1]] = int.parse(match[2]); | |
240 return true; | |
241 } | |
242 | |
243 void endPubServe() { | |
244 _pubServer.kill(); | |
245 } | |
246 | |
247 /// Schedules an HTTP request to the running pub server with [urlPath] and | |
248 /// invokes [callback] with the response. | |
249 /// | |
250 /// [root] indicates which server should be accessed, and defaults to "web". | |
251 Future<http.Response> scheduleRequest(String urlPath, {String root}) { | |
252 return schedule(() { | |
253 return http.get(_getServerUrlSync(root, urlPath)); | |
254 }, "request $urlPath"); | |
255 } | |
256 | |
257 /// Schedules an HTTP request to the running pub server with [urlPath] and | |
258 /// verifies that it responds with a body that matches [expectation]. | |
259 /// | |
260 /// [expectation] may either be a [Matcher] or a string to match an exact body. | |
261 /// [root] indicates which server should be accessed, and defaults to "web". | |
262 /// [headers] may be either a [Matcher] or a map to match an exact headers map. | |
263 void requestShouldSucceed(String urlPath, expectation, {String root, headers}) { | |
264 scheduleRequest(urlPath, root: root).then((response) { | |
265 expect(response.statusCode, equals(200)); | |
266 if (expectation != null) expect(response.body, expectation); | |
267 if (headers != null) expect(response.headers, headers); | |
268 }); | |
269 } | |
270 | |
271 /// Schedules an HTTP request to the running pub server with [urlPath] and | |
272 /// verifies that it responds with a 404. | |
273 /// | |
274 /// [root] indicates which server should be accessed, and defaults to "web". | |
275 void requestShould404(String urlPath, {String root}) { | |
276 scheduleRequest(urlPath, root: root).then((response) { | |
277 expect(response.statusCode, equals(404)); | |
278 }); | |
279 } | |
280 | |
281 /// Schedules an HTTP request to the running pub server with [urlPath] and | |
282 /// verifies that it responds with a redirect to the given [redirectTarget]. | |
283 /// | |
284 /// [redirectTarget] may be either a [Matcher] or a string to match an exact | |
285 /// URL. [root] indicates which server should be accessed, and defaults to | |
286 /// "web". | |
287 void requestShouldRedirect(String urlPath, redirectTarget, {String root}) { | |
288 schedule(() { | |
289 var request = new http.Request("GET", | |
290 Uri.parse(_getServerUrlSync(root, urlPath))); | |
291 request.followRedirects = false; | |
292 return request.send().then((response) { | |
293 expect(response.statusCode ~/ 100, equals(3)); | |
294 expect(response.headers, containsPair('location', redirectTarget)); | |
295 }); | |
296 }, "request $urlPath"); | |
297 } | |
298 | |
299 /// Schedules an HTTP POST to the running pub server with [urlPath] and verifies | |
300 /// that it responds with a 405. | |
301 /// | |
302 /// [root] indicates which server should be accessed, and defaults to "web". | |
303 void postShould405(String urlPath, {String root}) { | |
304 schedule(() { | |
305 return http.post(_getServerUrlSync(root, urlPath)).then((response) { | |
306 expect(response.statusCode, equals(405)); | |
307 }); | |
308 }, "request $urlPath"); | |
309 } | |
310 | |
311 /// Schedules an HTTP request to the (theoretically) running pub server with | |
312 /// [urlPath] and verifies that it cannot be connected to. | |
313 /// | |
314 /// [root] indicates which server should be accessed, and defaults to "web". | |
315 void requestShouldNotConnect(String urlPath, {String root}) { | |
316 schedule(() { | |
317 return expect(http.get(_getServerUrlSync(root, urlPath)), | |
318 throwsA(new isInstanceOf<SocketException>())); | |
319 }, "request $urlPath"); | |
320 } | |
321 | |
322 /// Reads lines from pub serve's stdout until it prints the build success | |
323 /// message. | |
324 /// | |
325 /// The schedule will not proceed until the output is found. If not found, it | |
326 /// will eventually time out. | |
327 void waitForBuildSuccess() => | |
328 _pubServer.stdout.expect(consumeThrough(contains("successfully"))); | |
329 | |
330 /// Schedules opening a web socket connection to the currently running pub | |
331 /// serve. | |
332 Future _ensureWebSocket() { | |
333 // Use the existing one if already connected. | |
334 if (_webSocket != null) return new Future.value(); | |
335 | |
336 // Server should already be running. | |
337 expect(_pubServer, isNotNull); | |
338 expect(_adminPort, isNotNull); | |
339 | |
340 return WebSocket.connect("ws://localhost:$_adminPort").then((socket) { | |
341 _webSocket = socket; | |
342 // TODO(rnystrom): Works around #13913. | |
343 _webSocketBroadcastStream = _webSocket.map(JSON.decode).asBroadcastStream(); | |
344 }); | |
345 } | |
346 | |
347 /// Schedules closing the web socket connection to the currently-running pub | |
348 /// serve. | |
349 void closeWebSocket() { | |
350 schedule(() { | |
351 return _ensureWebSocket().then((_) => _webSocket.close()) | |
352 .then((_) => _webSocket = null); | |
353 }, "closing web socket"); | |
354 } | |
355 | |
356 /// Sends a JSON RPC 2.0 request to the running pub serve's web socket | |
357 /// connection. | |
358 /// | |
359 /// This calls a method named [method] with the given [params] (or no | |
360 /// parameters, if it's not passed). [params] may contain Futures, in which case | |
361 /// this will wait until they've completed before sending the request. | |
362 /// | |
363 /// This schedules the request, but doesn't block the schedule on the response. | |
364 /// It returns the response as a [Future]. | |
365 Future<Map> webSocketRequest(String method, [Map params]) { | |
366 var completer = new Completer(); | |
367 schedule(() { | |
368 return Future.wait([ | |
369 _ensureWebSocket(), | |
370 awaitObject(params), | |
371 ]).then((results) { | |
372 var resolvedParams = results[1]; | |
373 chainToCompleter( | |
374 currentSchedule.wrapFuture(_jsonRpcRequest(method, resolvedParams)), | |
375 completer); | |
376 }); | |
377 }, "send $method with $params to web socket"); | |
378 return completer.future; | |
379 } | |
380 | |
381 /// Sends a JSON RPC 2.0 request to the running pub serve's web socket | |
382 /// connection, waits for a reply, then verifies the result. | |
383 /// | |
384 /// This calls a method named [method] with the given [params]. [params] may | |
385 /// contain Futures, in which case this will wait until they've completed before | |
386 /// sending the request. | |
387 /// | |
388 /// The result is validated using [result], which may be a [Matcher] or a [Map] | |
389 /// containing [Matcher]s and [Future]s. This will wait until any futures are | |
390 /// completed before sending the request. | |
391 /// | |
392 /// Returns a [Future] that completes to the call's result. | |
393 Future<Map> expectWebSocketResult(String method, Map params, result) { | |
394 return schedule(() { | |
395 return Future.wait([ | |
396 webSocketRequest(method, params), | |
397 awaitObject(result) | |
398 ]).then((results) { | |
399 var response = results[0]; | |
400 var resolvedResult = results[1]; | |
401 expect(response["result"], resolvedResult); | |
402 return response["result"]; | |
403 }); | |
404 }, "send $method with $params to web socket and expect $result"); | |
405 } | |
406 | |
407 /// Sends a JSON RPC 2.0 request to the running pub serve's web socket | |
408 /// connection, waits for a reply, then verifies the error response. | |
409 /// | |
410 /// This calls a method named [method] with the given [params]. [params] may | |
411 /// contain Futures, in which case this will wait until they've completed before | |
412 /// sending the request. | |
413 /// | |
414 /// The error response is validated using [errorCode] and [errorMessage]. Both | |
415 /// of these must be provided. The error code is checked against [errorCode] and | |
416 /// the error message is checked against [errorMessage]. Either of these may be | |
417 /// matchers. | |
418 /// | |
419 /// If [data] is provided, it is a JSON value or matcher used to validate the | |
420 /// "data" value of the error response. | |
421 /// | |
422 /// Returns a [Future] that completes to the error's [data] field. | |
423 Future expectWebSocketError(String method, Map params, errorCode, | |
424 errorMessage, {data}) { | |
425 return schedule(() { | |
426 return webSocketRequest(method, params).then((response) { | |
427 expect(response["error"]["code"], errorCode); | |
428 expect(response["error"]["message"], errorMessage); | |
429 | |
430 if (data != null) { | |
431 expect(response["error"]["data"], data); | |
432 } | |
433 | |
434 return response["error"]["data"]; | |
435 }); | |
436 }, "send $method with $params to web socket and expect error $errorCode"); | |
437 } | |
438 | |
439 /// Validates that [root] was not bound to a port when pub serve started. | |
440 Future expectNotServed(String root) { | |
441 return schedule(() { | |
442 expect(_ports.containsKey(root), isFalse); | |
443 }); | |
444 } | |
445 | |
446 /// The next id to use for a JSON-RPC 2.0 request. | |
447 var _rpcId = 0; | |
448 | |
449 /// Sends a JSON-RPC 2.0 request calling [method] with [params]. | |
450 /// | |
451 /// Returns the response object. | |
452 Future<Map> _jsonRpcRequest(String method, [Map params]) { | |
453 var id = _rpcId++; | |
454 var message = { | |
455 "jsonrpc": "2.0", | |
456 "method": method, | |
457 "id": id | |
458 }; | |
459 if (params != null) message["params"] = params; | |
460 _webSocket.add(JSON.encode(message)); | |
461 | |
462 return _webSocketBroadcastStream | |
463 .firstWhere((response) => response["id"] == id).then((value) { | |
464 currentSchedule.addDebugInfo( | |
465 "Web Socket request $method with params $params\n" | |
466 "Result: $value"); | |
467 | |
468 expect(value["id"], equals(id)); | |
469 return value; | |
470 }); | |
471 } | |
472 | |
473 /// Returns a [Future] that completes to a URL string for the server serving | |
474 /// [path] from [root]. | |
475 /// | |
476 /// If [root] is omitted, defaults to "web". If [path] is omitted, no path is | |
477 /// included. The Future will complete once the server is up and running and | |
478 /// the bound ports are known. | |
479 Future<String> getServerUrl([String root, String path]) => | |
480 _portsCompleter.future.then((_) => _getServerUrlSync(root, path)); | |
481 | |
482 /// Records that [root] has been bound to [port]. | |
483 /// | |
484 /// Used for testing the Web Socket API for binding new root directories to | |
485 /// ports after pub serve has been started. | |
486 registerServerPort(String root, int port) { | |
487 _ports[root] = port; | |
488 } | |
489 | |
490 /// Returns a URL string for the server serving [path] from [root]. | |
491 /// | |
492 /// If [root] is omitted, defaults to "web". If [path] is omitted, no path is | |
493 /// included. Unlike [getServerUrl], this should only be called after the ports | |
494 /// are known. | |
495 String _getServerUrlSync([String root, String path]) { | |
496 if (root == null) root = 'web'; | |
497 expect(_ports, contains(root)); | |
498 var url = "http://localhost:${_ports[root]}"; | |
499 if (path != null) url = "$url/$path"; | |
500 return url; | |
501 } | |
502 | |
OLD | NEW |