| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS d.file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library pub_tests; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:http/http.dart' as http; |
| import 'package:scheduled_test/scheduled_process.dart'; |
| import 'package:scheduled_test/scheduled_stream.dart'; |
| import 'package:scheduled_test/scheduled_test.dart'; |
| |
| import '../../lib/src/utils.dart'; |
| import '../descriptor.dart' as d; |
| import '../test_pub.dart'; |
| |
| /// The pub process running "pub serve". |
| ScheduledProcess _pubServer; |
| |
| /// The ephemeral port assign to the running admin server. |
| int _adminPort; |
| |
| /// The ephemeral ports assigned to the running servers, associated with the |
| /// directories they're serving. |
| final _ports = new Map<String, int>(); |
| |
| /// A completer that completes when the server has been started and the served |
| /// ports are known. |
| Completer _portsCompleter; |
| |
| /// The web socket connection to the running pub process, or `null` if no |
| /// connection has been made. |
| WebSocket _webSocket; |
| Stream _webSocketBroadcastStream; |
| |
| /// The code for a transformer that renames ".txt" files to ".out" and adds a |
| /// ".out" suffix. |
| const REWRITE_TRANSFORMER = """ |
| import 'dart:async'; |
| |
| import 'package:barback/barback.dart'; |
| |
| class RewriteTransformer extends Transformer { |
| RewriteTransformer.asPlugin(); |
| |
| String get allowedExtensions => '.txt'; |
| |
| Future apply(Transform transform) { |
| return transform.primaryInput.readAsString().then((contents) { |
| var id = transform.primaryInput.id.changeExtension(".out"); |
| transform.addOutput(new Asset.fromString(id, "\$contents.out")); |
| }); |
| } |
| } |
| """; |
| |
| /// The code for a lazy version of [REWRITE_TRANSFORMER]. |
| const LAZY_TRANSFORMER = """ |
| import 'dart:async'; |
| |
| import 'package:barback/barback.dart'; |
| |
| class LazyRewriteTransformer extends Transformer implements LazyTransformer { |
| LazyRewriteTransformer.asPlugin(); |
| |
| String get allowedExtensions => '.txt'; |
| |
| Future apply(Transform transform) { |
| transform.logger.info('Rewriting \${transform.primaryInput.id}.'); |
| return transform.primaryInput.readAsString().then((contents) { |
| var id = transform.primaryInput.id.changeExtension(".out"); |
| transform.addOutput(new Asset.fromString(id, "\$contents.out")); |
| }); |
| } |
| |
| Future declareOutputs(DeclaringTransform transform) { |
| transform.declareOutput(transform.primaryId.changeExtension(".out")); |
| return new Future.value(); |
| } |
| } |
| """; |
| |
| /// The web socket error code for a directory not being served. |
| const NOT_SERVED = 1; |
| |
| /// Returns the source code for a Dart library defining a Transformer that |
| /// rewrites Dart files. |
| /// |
| /// The transformer defines a constant named TOKEN whose value is [id]. When the |
| /// transformer transforms another Dart file, it will look for a "TOKEN" |
| /// constant definition there and modify it to include *this* transformer's |
| /// TOKEN value as well. |
| /// |
| /// If [import] is passed, it should be the name of a package that defines its |
| /// own TOKEN constant. The primary library of that package will be imported |
| /// here and its TOKEN value will be added to this library's. |
| String dartTransformer(String id, {String import}) { |
| if (import != null) { |
| id = '$id imports \${$import.TOKEN}'; |
| import = 'import "package:$import/$import.dart" as $import;'; |
| } else { |
| import = ''; |
| } |
| |
| return """ |
| import 'dart:async'; |
| |
| import 'package:barback/barback.dart'; |
| $import |
| |
| const TOKEN = "$id"; |
| |
| final _tokenRegExp = new RegExp(r'^const TOKEN = "(.*?)";\$', multiLine: true); |
| |
| class DartTransformer extends Transformer { |
| DartTransformer.asPlugin(); |
| |
| String get allowedExtensions => '.dart'; |
| |
| Future apply(Transform transform) { |
| return transform.primaryInput.readAsString().then((contents) { |
| transform.addOutput(new Asset.fromString(transform.primaryInput.id, |
| contents.replaceAllMapped(_tokenRegExp, (match) { |
| return 'const TOKEN = "(\${match[1]}, \$TOKEN)";'; |
| }))); |
| }); |
| } |
| } |
| """; |
| } |
| |
| /// Schedules starting the `pub serve` process. |
| /// |
| /// Unlike [pubServe], this doesn't determine the port number of the server, and |
| /// so may be used to test for errors in the initialization process. |
| /// |
| /// Returns the `pub serve` process. |
| ScheduledProcess startPubServe({Iterable<String> args, |
| bool createWebDir: true}) { |
| var pubArgs = [ |
| "serve", |
| "--port=0", // Use port 0 to get an ephemeral port. |
| "--hostname=127.0.0.1", // Force IPv4 on bots. |
| "--force-poll", |
| "--log-admin-url" |
| ]; |
| |
| if (args != null) pubArgs.addAll(args); |
| |
| // Dart2js can take a long time to compile dart code, so we increase the |
| // timeout to cope with that. |
| currentSchedule.timeout *= 1.5; |
| |
| if (createWebDir) d.dir(appPath, [d.dir("web")]).create(); |
| return startPub(args: pubArgs); |
| } |
| |
| /// Schedules starting the "pub serve" process and records its port number for |
| /// future requests. |
| /// |
| /// If [shouldGetFirst] is `true`, validates that pub get is run first. |
| /// |
| /// If [createWebDir] is `true`, creates a `web/` directory if one doesn't exist |
| /// so pub doesn't complain about having nothing to serve. |
| /// |
| /// Returns the `pub serve` process. |
| ScheduledProcess pubServe({bool shouldGetFirst: false, bool createWebDir: true, |
| Iterable<String> args}) { |
| _pubServer = startPubServe(args: args, createWebDir: createWebDir); |
| _portsCompleter = new Completer(); |
| |
| currentSchedule.onComplete.schedule(() { |
| _portsCompleter = null; |
| _ports.clear(); |
| |
| if (_webSocket != null) { |
| _webSocket.close(); |
| _webSocket = null; |
| _webSocketBroadcastStream = null; |
| } |
| }); |
| |
| if (shouldGetFirst) { |
| _pubServer.stdout.expect(consumeThrough("Got dependencies!")); |
| } |
| |
| _pubServer.stdout.expect(startsWith("Loading source assets...")); |
| _pubServer.stdout.expect(consumeWhile(matches("Loading .* transformers..."))); |
| |
| _pubServer.stdout.expect(predicate(_parseAdminPort)); |
| |
| // The server should emit one or more ports. |
| _pubServer.stdout.expect( |
| consumeWhile(predicate(_parsePort, 'emits server url'))); |
| schedule(() { |
| expect(_ports, isNot(isEmpty)); |
| _portsCompleter.complete(); |
| }); |
| |
| return _pubServer; |
| } |
| |
| /// The regular expression for parsing pub's output line describing the URL for |
| /// the server. |
| final _parsePortRegExp = new RegExp(r"([^ ]+) +on http://127\.0\.0\.1:(\d+)"); |
| |
| /// Parses the port number from the "Running admin server on 127.0.0.1:1234" |
| /// line printed by pub serve. |
| bool _parseAdminPort(String line) { |
| var match = _parsePortRegExp.firstMatch(line); |
| if (match == null) return false; |
| _adminPort = int.parse(match[2]); |
| return true; |
| } |
| |
| /// Parses the port number from the "Serving blah on 127.0.0.1:1234" line |
| /// printed by pub serve. |
| bool _parsePort(String line) { |
| var match = _parsePortRegExp.firstMatch(line); |
| if (match == null) return false; |
| _ports[match[1]] = int.parse(match[2]); |
| return true; |
| } |
| |
| void endPubServe() { |
| _pubServer.kill(); |
| } |
| |
| /// Schedules an HTTP request to the running pub server with [urlPath] and |
| /// invokes [callback] with the response. |
| /// |
| /// [root] indicates which server should be accessed, and defaults to "web". |
| Future<http.Response> scheduleRequest(String urlPath, {String root}) { |
| return schedule(() { |
| return http.get(_getServerUrlSync(root, urlPath)); |
| }, "request $urlPath"); |
| } |
| |
| /// Schedules an HTTP request to the running pub server with [urlPath] and |
| /// verifies that it responds with a body that matches [expectation]. |
| /// |
| /// [expectation] may either be a [Matcher] or a string to match an exact body. |
| /// [root] indicates which server should be accessed, and defaults to "web". |
| /// [headers] may be either a [Matcher] or a map to match an exact headers map. |
| void requestShouldSucceed(String urlPath, expectation, {String root, headers}) { |
| scheduleRequest(urlPath, root: root).then((response) { |
| if (expectation != null) expect(response.body, expectation); |
| if (headers != null) expect(response.headers, headers); |
| }); |
| } |
| |
| /// Schedules an HTTP request to the running pub server with [urlPath] and |
| /// verifies that it responds with a 404. |
| /// |
| /// [root] indicates which server should be accessed, and defaults to "web". |
| void requestShould404(String urlPath, {String root}) { |
| scheduleRequest(urlPath, root: root).then((response) { |
| expect(response.statusCode, equals(404)); |
| }); |
| } |
| |
| /// Schedules an HTTP request to the running pub server with [urlPath] and |
| /// verifies that it responds with a redirect to the given [redirectTarget]. |
| /// |
| /// [redirectTarget] may be either a [Matcher] or a string to match an exact |
| /// URL. [root] indicates which server should be accessed, and defaults to |
| /// "web". |
| void requestShouldRedirect(String urlPath, redirectTarget, {String root}) { |
| schedule(() { |
| var request = new http.Request("GET", |
| Uri.parse(_getServerUrlSync(root, urlPath))); |
| request.followRedirects = false; |
| return request.send().then((response) { |
| expect(response.statusCode ~/ 100, equals(3)); |
| expect(response.headers, containsPair('location', redirectTarget)); |
| }); |
| }, "request $urlPath"); |
| } |
| |
| /// Schedules an HTTP POST to the running pub server with [urlPath] and verifies |
| /// that it responds with a 405. |
| /// |
| /// [root] indicates which server should be accessed, and defaults to "web". |
| void postShould405(String urlPath, {String root}) { |
| schedule(() { |
| return http.post(_getServerUrlSync(root, urlPath)).then((response) { |
| expect(response.statusCode, equals(405)); |
| }); |
| }, "request $urlPath"); |
| } |
| |
| /// Schedules an HTTP request to the (theoretically) running pub server with |
| /// [urlPath] and verifies that it cannot be connected to. |
| /// |
| /// [root] indicates which server should be accessed, and defaults to "web". |
| void requestShouldNotConnect(String urlPath, {String root}) { |
| schedule(() { |
| return expect(http.get(_getServerUrlSync(root, urlPath)), |
| throwsA(new isInstanceOf<SocketException>())); |
| }, "request $urlPath"); |
| } |
| |
| /// Reads lines from pub serve's stdout until it prints the build success |
| /// message. |
| /// |
| /// The schedule will not proceed until the output is found. If not found, it |
| /// will eventually time out. |
| void waitForBuildSuccess() => |
| _pubServer.stdout.expect(consumeThrough(contains("successfully"))); |
| |
| /// Schedules opening a web socket connection to the currently running pub |
| /// serve. |
| Future _ensureWebSocket() { |
| // Use the existing one if already connected. |
| if (_webSocket != null) return new Future.value(); |
| |
| // Server should already be running. |
| expect(_pubServer, isNotNull); |
| expect(_adminPort, isNotNull); |
| |
| return WebSocket.connect("ws://127.0.0.1:$_adminPort").then((socket) { |
| _webSocket = socket; |
| // TODO(rnystrom): Works around #13913. |
| _webSocketBroadcastStream = _webSocket.map(JSON.decode).asBroadcastStream(); |
| }); |
| } |
| |
| /// Sends a JSON RPC 2.0 request to the running pub serve's web socket |
| /// connection. |
| /// |
| /// This calls a method named [method] with the given [params]. [params] may |
| /// contain Futures, in which case this will wait until they've completed before |
| /// sending the request. |
| /// |
| /// This schedules the request, but doesn't block the schedule on the response. |
| /// It returns the response as a [Future]. |
| Future<Map> webSocketRequest(String method, Map params) { |
| var completer = new Completer(); |
| schedule(() { |
| return Future.wait([ |
| _ensureWebSocket(), |
| awaitObject(params), |
| ]).then((results) { |
| var resolvedParams = results[1]; |
| chainToCompleter( |
| currentSchedule.wrapFuture(_jsonRpcRequest(method, resolvedParams)), |
| completer); |
| }); |
| }, "send $method with $params to web socket"); |
| return completer.future; |
| } |
| |
| /// Sends a JSON RPC 2.0 request to the running pub serve's web socket |
| /// connection, waits for a reply, then verifies the result. |
| /// |
| /// This calls a method named [method] with the given [params]. [params] may |
| /// contain Futures, in which case this will wait until they've completed before |
| /// sending the request. |
| /// |
| /// The result is validated using [result], which may be a [Matcher] or a [Map] |
| /// containing [Matcher]s and [Future]s. This will wait until any futures are |
| /// completed before sending the request. |
| /// |
| /// Returns a [Future] that completes to the call's result. |
| Future<Map> expectWebSocketResult(String method, Map params, result) { |
| return schedule(() { |
| return Future.wait([ |
| webSocketRequest(method, params), |
| awaitObject(result) |
| ]).then((results) { |
| var response = results[0]; |
| var resolvedResult = results[1]; |
| expect(response["result"], resolvedResult); |
| return response["result"]; |
| }); |
| }, "send $method with $params to web socket and expect $result"); |
| } |
| |
| /// Sends a JSON RPC 2.0 request to the running pub serve's web socket |
| /// connection, waits for a reply, then verifies the error response. |
| /// |
| /// This calls a method named [method] with the given [params]. [params] may |
| /// contain Futures, in which case this will wait until they've completed before |
| /// sending the request. |
| /// |
| /// The error response is validated using [errorCode] and [errorMessage]. Both |
| /// of these must be provided. The error code is checked against [errorCode] and |
| /// the error message is checked against [errorMessage]. Either of these may be |
| /// matchers. |
| /// |
| /// If [data] is provided, it is a JSON value or matcher used to validate the |
| /// "data" value of the error response. |
| /// |
| /// Returns a [Future] that completes to the error's [data] field. |
| Future expectWebSocketError(String method, Map params, errorCode, |
| errorMessage, {data}) { |
| return schedule(() { |
| return webSocketRequest(method, params).then((response) { |
| expect(response["error"]["code"], errorCode); |
| expect(response["error"]["message"], errorMessage); |
| |
| if (data != null) { |
| expect(response["error"]["data"], data); |
| } |
| |
| return response["error"]["data"]; |
| }); |
| }, "send $method with $params to web socket and expect error $errorCode"); |
| } |
| |
| /// Validates that [root] was not bound to a port when pub serve started. |
| Future expectNotServed(String root) { |
| return schedule(() { |
| expect(_ports.containsKey(root), isFalse); |
| }); |
| } |
| |
| /// The next id to use for a JSON-RPC 2.0 request. |
| var _rpcId = 0; |
| |
| /// Sends a JSON-RPC 2.0 request calling [method] with [params]. |
| /// |
| /// Returns the response object. |
| Future<Map> _jsonRpcRequest(String method, Map params) { |
| var id = _rpcId++; |
| _webSocket.add(JSON.encode({ |
| "jsonrpc": "2.0", |
| "method": method, |
| "params": params, |
| "id": id |
| })); |
| |
| return _webSocketBroadcastStream |
| .firstWhere((response) => response["id"] == id).then((value) { |
| currentSchedule.addDebugInfo( |
| "Web Socket request $method with params $params\n" |
| "Result: $value"); |
| |
| expect(value["id"], equals(id)); |
| return value; |
| }); |
| } |
| |
| /// Returns a [Future] that completes to a URL string for the server serving |
| /// [path] from [root]. |
| /// |
| /// If [root] is omitted, defaults to "web". If [path] is omitted, no path is |
| /// included. The Future will complete once the server is up and running and |
| /// the bound ports are known. |
| Future<String> getServerUrl([String root, String path]) => |
| _portsCompleter.future.then((_) => _getServerUrlSync(root, path)); |
| |
| /// Records that [root] has been bound to [port]. |
| /// |
| /// Used for testing the Web Socket API for binding new root directories to |
| /// ports after pub serve has been started. |
| registerServerPort(String root, int port) { |
| _ports[root] = port; |
| } |
| |
| /// Returns a URL string for the server serving [path] from [root]. |
| /// |
| /// If [root] is omitted, defaults to "web". If [path] is omitted, no path is |
| /// included. Unlike [getServerUrl], this should only be called after the ports |
| /// are known. |
| String _getServerUrlSync([String root, String path]) { |
| if (root == null) root = 'web'; |
| expect(_ports, contains(root)); |
| var url = "http://127.0.0.1:${_ports[root]}"; |
| if (path != null) url = "$url/$path"; |
| return url; |
| } |
| |