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, bool createWebDir: true}) | |
153 { | |
154 var pubArgs = ["serve", "--port=0", // Use port 0 to get an ephemeral port. | |
155 "--force-poll", "--log-admin-url"]; | |
156 | |
157 if (args != null) pubArgs.addAll(args); | |
158 | |
159 // Dart2js can take a long time to compile dart code, so we increase the | |
160 // timeout to cope with that. | |
161 currentSchedule.timeout *= 1.5; | |
162 | |
163 if (createWebDir) d.dir(appPath, [d.dir("web")]).create(); | |
164 return startPub(args: pubArgs); | |
165 } | |
166 | |
167 /// Schedules starting the "pub serve" process and records its port number for | |
168 /// future requests. | |
169 /// | |
170 /// If [shouldGetFirst] is `true`, validates that pub get is run first. | |
171 /// | |
172 /// If [createWebDir] is `true`, creates a `web/` directory if one doesn't exist | |
173 /// so pub doesn't complain about having nothing to serve. | |
174 /// | |
175 /// Returns the `pub serve` process. | |
176 ScheduledProcess pubServe({bool shouldGetFirst: false, bool createWebDir: true, | |
177 Iterable<String> args}) { | |
178 _pubServer = startPubServe(args: args, createWebDir: createWebDir); | |
179 _portsCompleter = new Completer(); | |
180 | |
181 currentSchedule.onComplete.schedule(() { | |
182 _portsCompleter = null; | |
183 _ports.clear(); | |
184 | |
185 if (_webSocket != null) { | |
186 _webSocket.close(); | |
187 _webSocket = null; | |
188 _webSocketBroadcastStream = null; | |
189 } | |
190 }); | |
191 | |
192 if (shouldGetFirst) { | |
193 _pubServer.stdout.expect( | |
194 consumeThrough( | |
195 anyOf(["Got dependencies!", matches(new RegExp(r"^Changed \d+ depend
enc"))]))); | |
196 } | |
197 | |
198 _pubServer.stdout.expect(startsWith("Loading source assets...")); | |
199 _pubServer.stdout.expect(consumeWhile(matches("Loading .* transformers..."))); | |
200 | |
201 _pubServer.stdout.expect(predicate(_parseAdminPort)); | |
202 | |
203 // The server should emit one or more ports. | |
204 _pubServer.stdout.expect( | |
205 consumeWhile(predicate(_parsePort, 'emits server url'))); | |
206 schedule(() { | |
207 expect(_ports, isNot(isEmpty)); | |
208 _portsCompleter.complete(); | |
209 }); | |
210 | |
211 return _pubServer; | |
212 } | |
213 | |
214 /// The regular expression for parsing pub's output line describing the URL for | |
215 /// the server. | |
216 final _parsePortRegExp = new RegExp(r"([^ ]+) +on http://localhost:(\d+)"); | |
217 | |
218 /// Parses the port number from the "Running admin server on localhost:1234" | |
219 /// line printed by pub serve. | |
220 bool _parseAdminPort(String line) { | |
221 var match = _parsePortRegExp.firstMatch(line); | |
222 if (match == null) return false; | |
223 _adminPort = int.parse(match[2]); | |
224 return true; | |
225 } | |
226 | |
227 /// Parses the port number from the "Serving blah on localhost:1234" line | |
228 /// printed by pub serve. | |
229 bool _parsePort(String line) { | |
230 var match = _parsePortRegExp.firstMatch(line); | |
231 if (match == null) return false; | |
232 _ports[match[1]] = int.parse(match[2]); | |
233 return true; | |
234 } | |
235 | |
236 void endPubServe() { | |
237 _pubServer.kill(); | |
238 } | |
239 | |
240 /// Schedules an HTTP request to the running pub server with [urlPath] and | |
241 /// invokes [callback] with the response. | |
242 /// | |
243 /// [root] indicates which server should be accessed, and defaults to "web". | |
244 Future<http.Response> scheduleRequest(String urlPath, {String root}) { | |
245 return schedule(() { | |
246 return http.get(_getServerUrlSync(root, urlPath)); | |
247 }, "request $urlPath"); | |
248 } | |
249 | |
250 /// Schedules an HTTP request to the running pub server with [urlPath] and | |
251 /// verifies that it responds with a body that matches [expectation]. | |
252 /// | |
253 /// [expectation] may either be a [Matcher] or a string to match an exact body. | |
254 /// [root] indicates which server should be accessed, and defaults to "web". | |
255 /// [headers] may be either a [Matcher] or a map to match an exact headers map. | |
256 void requestShouldSucceed(String urlPath, expectation, {String root, headers}) { | |
257 scheduleRequest(urlPath, root: root).then((response) { | |
258 expect(response.statusCode, equals(200)); | |
259 if (expectation != null) expect(response.body, expectation); | |
260 if (headers != null) expect(response.headers, headers); | |
261 }); | |
262 } | |
263 | |
264 /// Schedules an HTTP request to the running pub server with [urlPath] and | |
265 /// verifies that it responds with a 404. | |
266 /// | |
267 /// [root] indicates which server should be accessed, and defaults to "web". | |
268 void requestShould404(String urlPath, {String root}) { | |
269 scheduleRequest(urlPath, root: root).then((response) { | |
270 expect(response.statusCode, equals(404)); | |
271 }); | |
272 } | |
273 | |
274 /// Schedules an HTTP request to the running pub server with [urlPath] and | |
275 /// verifies that it responds with a redirect to the given [redirectTarget]. | |
276 /// | |
277 /// [redirectTarget] may be either a [Matcher] or a string to match an exact | |
278 /// URL. [root] indicates which server should be accessed, and defaults to | |
279 /// "web". | |
280 void requestShouldRedirect(String urlPath, redirectTarget, {String root}) { | |
281 schedule(() { | |
282 var request = | |
283 new http.Request("GET", Uri.parse(_getServerUrlSync(root, urlPath))); | |
284 request.followRedirects = false; | |
285 return request.send().then((response) { | |
286 expect(response.statusCode ~/ 100, equals(3)); | |
287 expect(response.headers, containsPair('location', redirectTarget)); | |
288 }); | |
289 }, "request $urlPath"); | |
290 } | |
291 | |
292 /// Schedules an HTTP POST to the running pub server with [urlPath] and verifies | |
293 /// that it responds with a 405. | |
294 /// | |
295 /// [root] indicates which server should be accessed, and defaults to "web". | |
296 void postShould405(String urlPath, {String root}) { | |
297 schedule(() { | |
298 return http.post(_getServerUrlSync(root, urlPath)).then((response) { | |
299 expect(response.statusCode, equals(405)); | |
300 }); | |
301 }, "request $urlPath"); | |
302 } | |
303 | |
304 /// Schedules an HTTP request to the (theoretically) running pub server with | |
305 /// [urlPath] and verifies that it cannot be connected to. | |
306 /// | |
307 /// [root] indicates which server should be accessed, and defaults to "web". | |
308 void requestShouldNotConnect(String urlPath, {String root}) { | |
309 schedule(() { | |
310 return expect( | |
311 http.get(_getServerUrlSync(root, urlPath)), | |
312 throwsA(new isInstanceOf<SocketException>())); | |
313 }, "request $urlPath"); | |
314 } | |
315 | |
316 /// Reads lines from pub serve's stdout until it prints the build success | |
317 /// message. | |
318 /// | |
319 /// The schedule will not proceed until the output is found. If not found, it | |
320 /// will eventually time out. | |
321 void waitForBuildSuccess() => | |
322 _pubServer.stdout.expect(consumeThrough(contains("successfully"))); | |
323 | |
324 /// Schedules opening a web socket connection to the currently running pub | |
325 /// serve. | |
326 Future _ensureWebSocket() { | |
327 // Use the existing one if already connected. | |
328 if (_webSocket != null) return new Future.value(); | |
329 | |
330 // Server should already be running. | |
331 expect(_pubServer, isNotNull); | |
332 expect(_adminPort, isNotNull); | |
333 | |
334 return WebSocket.connect("ws://localhost:$_adminPort").then((socket) { | |
335 _webSocket = socket; | |
336 // TODO(rnystrom): Works around #13913. | |
337 _webSocketBroadcastStream = _webSocket.map(JSON.decode).asBroadcastStream(); | |
338 }); | |
339 } | |
340 | |
341 /// Schedules closing the web socket connection to the currently-running pub | |
342 /// serve. | |
343 void closeWebSocket() { | |
344 schedule(() { | |
345 return _ensureWebSocket().then((_) => _webSocket.close()).then((_) => _webSo
cket = | |
346 null); | |
347 }, "closing web socket"); | |
348 } | |
349 | |
350 /// Sends a JSON RPC 2.0 request to the running pub serve's web socket | |
351 /// connection. | |
352 /// | |
353 /// This calls a method named [method] with the given [params] (or no | |
354 /// parameters, if it's not passed). [params] may contain Futures, in which case | |
355 /// this will wait until they've completed before sending the request. | |
356 /// | |
357 /// This schedules the request, but doesn't block the schedule on the response. | |
358 /// It returns the response as a [Future]. | |
359 Future<Map> webSocketRequest(String method, [Map params]) { | |
360 var completer = new Completer(); | |
361 schedule(() { | |
362 return Future.wait( | |
363 [_ensureWebSocket(), awaitObject(params),]).then((results) { | |
364 var resolvedParams = results[1]; | |
365 chainToCompleter( | |
366 currentSchedule.wrapFuture(_jsonRpcRequest(method, resolvedParams)), | |
367 completer); | |
368 }); | |
369 }, "send $method with $params to web socket"); | |
370 return completer.future; | |
371 } | |
372 | |
373 /// Sends a JSON RPC 2.0 request to the running pub serve's web socket | |
374 /// connection, waits for a reply, then verifies the result. | |
375 /// | |
376 /// This calls a method named [method] with the given [params]. [params] may | |
377 /// contain Futures, in which case this will wait until they've completed before | |
378 /// sending the request. | |
379 /// | |
380 /// The result is validated using [result], which may be a [Matcher] or a [Map] | |
381 /// containing [Matcher]s and [Future]s. This will wait until any futures are | |
382 /// completed before sending the request. | |
383 /// | |
384 /// Returns a [Future] that completes to the call's result. | |
385 Future<Map> expectWebSocketResult(String method, Map params, result) { | |
386 return schedule(() { | |
387 return Future.wait( | |
388 [webSocketRequest(method, params), awaitObject(result)]).then((results)
{ | |
389 var response = results[0]; | |
390 var resolvedResult = results[1]; | |
391 expect(response["result"], resolvedResult); | |
392 return response["result"]; | |
393 }); | |
394 }, "send $method with $params to web socket and expect $result"); | |
395 } | |
396 | |
397 /// Sends a JSON RPC 2.0 request to the running pub serve's web socket | |
398 /// connection, waits for a reply, then verifies the error response. | |
399 /// | |
400 /// This calls a method named [method] with the given [params]. [params] may | |
401 /// contain Futures, in which case this will wait until they've completed before | |
402 /// sending the request. | |
403 /// | |
404 /// The error response is validated using [errorCode] and [errorMessage]. Both | |
405 /// of these must be provided. The error code is checked against [errorCode] and | |
406 /// the error message is checked against [errorMessage]. Either of these may be | |
407 /// matchers. | |
408 /// | |
409 /// If [data] is provided, it is a JSON value or matcher used to validate the | |
410 /// "data" value of the error response. | |
411 /// | |
412 /// Returns a [Future] that completes to the error's [data] field. | |
413 Future expectWebSocketError(String method, Map params, errorCode, errorMessage, | |
414 {data}) { | |
415 return schedule(() { | |
416 return webSocketRequest(method, params).then((response) { | |
417 expect(response["error"]["code"], errorCode); | |
418 expect(response["error"]["message"], errorMessage); | |
419 | |
420 if (data != null) { | |
421 expect(response["error"]["data"], data); | |
422 } | |
423 | |
424 return response["error"]["data"]; | |
425 }); | |
426 }, "send $method with $params to web socket and expect error $errorCode"); | |
427 } | |
428 | |
429 /// Validates that [root] was not bound to a port when pub serve started. | |
430 Future expectNotServed(String root) { | |
431 return schedule(() { | |
432 expect(_ports.containsKey(root), isFalse); | |
433 }); | |
434 } | |
435 | |
436 /// The next id to use for a JSON-RPC 2.0 request. | |
437 var _rpcId = 0; | |
438 | |
439 /// Sends a JSON-RPC 2.0 request calling [method] with [params]. | |
440 /// | |
441 /// Returns the response object. | |
442 Future<Map> _jsonRpcRequest(String method, [Map params]) { | |
443 var id = _rpcId++; | |
444 var message = { | |
445 "jsonrpc": "2.0", | |
446 "method": method, | |
447 "id": id | |
448 }; | |
449 if (params != null) message["params"] = params; | |
450 _webSocket.add(JSON.encode(message)); | |
451 | |
452 return _webSocketBroadcastStream.firstWhere( | |
453 (response) => response["id"] == id).then((value) { | |
454 currentSchedule.addDebugInfo( | |
455 "Web Socket request $method with params $params\n" "Result: $value"); | |
456 | |
457 expect(value["id"], equals(id)); | |
458 return value; | |
459 }); | |
460 } | |
461 | |
462 /// Returns a [Future] that completes to a URL string for the server serving | |
463 /// [path] from [root]. | |
464 /// | |
465 /// If [root] is omitted, defaults to "web". If [path] is omitted, no path is | |
466 /// included. The Future will complete once the server is up and running and | |
467 /// the bound ports are known. | |
468 Future<String> getServerUrl([String root, String path]) => | |
469 _portsCompleter.future.then((_) => _getServerUrlSync(root, path)); | |
470 | |
471 /// Records that [root] has been bound to [port]. | |
472 /// | |
473 /// Used for testing the Web Socket API for binding new root directories to | |
474 /// ports after pub serve has been started. | |
475 registerServerPort(String root, int port) { | |
476 _ports[root] = port; | |
477 } | |
478 | |
479 /// Returns a URL string for the server serving [path] from [root]. | |
480 /// | |
481 /// If [root] is omitted, defaults to "web". If [path] is omitted, no path is | |
482 /// included. Unlike [getServerUrl], this should only be called after the ports | |
483 /// are known. | |
484 String _getServerUrlSync([String root, String path]) { | |
485 if (root == null) root = 'web'; | |
486 expect(_ports, contains(root)); | |
487 var url = "http://localhost:${_ports[root]}"; | |
488 if (path != null) url = "$url/$path"; | |
489 return url; | |
490 } | |
491 | |
OLD | NEW |