blob: 983bf1cc8a101415fb1cba1a703fd6257f6ec30f [file] [log] [blame]
// 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.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';
import 'package:pub/src/compiler.dart';
import '../descriptor.dart' as d;
import '../test_pub.dart';
/// The pub process running "pub serve".
TestProcess _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>();
/// 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.
///
/// This transformer takes one configuration field: "addition". This is
/// concatenated to its TOKEN value before adding it to the output library.
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
import 'dart:io';
const TOKEN = "$id";
final _tokenRegExp = new RegExp(r'^const TOKEN = "(.*?)";\$', multiLine: true);
class DartTransformer extends Transformer {
final BarbackSettings _settings;
DartTransformer.asPlugin(this._settings);
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) {
var token = TOKEN;
var addition = _settings.configuration["addition"];
if (addition != null) token += addition;
return 'const TOKEN = "(\${match[1]}, \$token)";';
})));
});
}
}
""";
}
/// Starts 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.
Future<TestProcess> startPubServe(
{Iterable<String> args,
bool createWebDir: true,
Compiler compiler,
bool forcePoll}) async {
forcePoll ??= true;
var pubArgs = [
"serve",
"--port=0", // Use port 0 to get an ephemeral port.
"--admin-port=0", // Use port 0 to get an ephemeral port.
"--log-admin-url",
];
if (forcePoll) pubArgs.add("--force-poll");
if (compiler != null) {
pubArgs.add("--web-compiler=${compiler.name}");
}
if (args != null) pubArgs.addAll(args);
if (createWebDir) await d.dir(appPath, [d.dir("web")]).create();
return await startPub(args: pubArgs);
}
/// Starts the "pub serve" process and records its port number for future
/// requests.
///
/// The port can be retrieved by calling [getServerUrl].
///
/// 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.
Future<TestProcess> pubServe(
{bool createWebDir: true,
Iterable<String> args,
Compiler compiler,
bool forcePoll}) async {
_pubServer = await startPubServe(
args: args,
createWebDir: createWebDir,
compiler: compiler,
forcePoll: forcePoll);
addTearDown(() {
_ports.clear();
if (_webSocket != null) {
_webSocket.close();
_webSocket = null;
_webSocketBroadcastStream = null;
}
});
await expectLater(
_pubServer.stdout, emits(startsWith("Loading source assets...")));
await expectLater(_pubServer.stdout,
mayEmitMultiple(matches("Loading .* transformers...")));
_adminPort = _parseAdminPort(await _pubServer.stdout.next);
// The server should emit one or more ports.
while (_parsePort(await _pubServer.stdout.peek)) {
await _pubServer.stdout.next;
}
expect(_ports, isNotEmpty);
return _pubServer;
}
/// The regular expression for parsing pub's output line describing the URL for
/// the server.
final _parsePortRegExp = new RegExp(r"([^ ]+) +on http://localhost:(\d+)");
/// Parses the port number from the "Running admin server on localhost:1234"
/// line printed by pub serve.
int _parseAdminPort(String line) {
expect(line, startsWith('Running admin server on'));
expect(line, contains(_parsePortRegExp));
var match = _parsePortRegExp.firstMatch(line);
return int.parse(match[2]);
}
/// Parses the port number from the "Serving blah on localhost:1234" line
/// printed by pub serve and adds it to [_ports].
///
/// Returns whether parsing succeeded.
bool _parsePort(String line) {
var match = _parsePortRegExp.firstMatch(line);
if (match == null) return false;
_ports[match[1]] = int.parse(match[2]);
return true;
}
Future endPubServe() => _pubServer.kill();
/// Makes an HTTP request to the running pub server with [urlPath] and returns
/// the response.
///
/// [root] indicates which server should be accessed, and defaults to "web".
Future<http.Response> requestFromPub(String urlPath,
{String root, Map<String, String> headers}) =>
http.get(getServerUrl(root, urlPath), headers: headers);
/// Makes 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.
Future requestShouldSucceed(String urlPath, expectation,
{String root, headers}) async {
var response = await requestFromPub(urlPath, root: root);
expect(response.statusCode, equals(200));
if (expectation != null) expect(response.body, expectation);
if (headers != null) expect(response.headers, headers);
}
/// Makes 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".
Future requestShould404(String urlPath, {String root}) async {
var response = await requestFromPub(urlPath, root: root);
expect(response.statusCode, equals(404));
}
/// Makes 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".
Future requestShouldRedirect(String urlPath, redirectTarget,
{String root}) async {
var request = new http.Request("GET", Uri.parse(getServerUrl(root, urlPath)));
request.followRedirects = false;
var response = await request.send();
expect(response.statusCode ~/ 100, equals(3));
expect(response.headers, containsPair('location', redirectTarget));
}
/// Makes 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".
Future postShould405(String urlPath, {String root}) async {
var response = await http.post(getServerUrl(root, urlPath));
expect(response.statusCode, equals(405));
}
/// Makes 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".
Future requestShouldNotConnect(String urlPath, {String root}) {
return expectLater(http.get(getServerUrl(root, urlPath)),
throwsA(new isInstanceOf<SocketException>()));
}
/// 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.
Future waitForBuildSuccess() =>
expectLater(_pubServer.stdout, emitsThrough(contains("successfully")));
/// Opening a web socket connection to the currently running pub serve.
Future _ensureWebSocket() async {
// Use the existing one if already connected.
if (_webSocket != null) return;
// Server should already be running.
expect(_pubServer, isNotNull);
expect(_adminPort, isNotNull);
var socket = await WebSocket.connect("ws://localhost:$_adminPort");
_webSocket = socket;
// TODO(rnystrom): Works around #13913.
_webSocketBroadcastStream = _webSocket.map(JSON.decode).asBroadcastStream();
}
/// Closes the web socket connection to the currently-running pub serve.
Future closeWebSocket() async {
await _ensureWebSocket();
await _webSocket.close();
_webSocket = null;
}
/// 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] (or no
/// parameters, if it's not passed).
///
/// Returns the result of the RPC call.
Future<Map> webSocketRequest(String method, [Map params]) async {
await _ensureWebSocket();
return await _jsonRpcRequest(method, params);
}
/// 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].
///
/// The result is validated using [result], which may be a [Matcher] or a [Map]
/// containing [Matcher]s.
///
/// Returns the result of the RPC call.
Future<Map> expectWebSocketResult(String method, Map params, result) async {
var response = await webSocketRequest(method, params);
expect(response["result"], result);
return response["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].
///
/// 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 the result of the RPC call.
Future expectWebSocketError(String method, Map params, errorCode, errorMessage,
{data}) async {
var response = await webSocketRequest(method, params);
expect(response["error"]["code"], errorCode);
expect(response["error"]["message"], errorMessage);
if (data != null) {
expect(response["error"]["data"], data);
}
return response["error"]["data"];
}
/// Validates that [root] was not bound to a port when pub serve started.
void expectNotServed(String root) {
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]) async {
var id = _rpcId++;
var message = {"jsonrpc": "2.0", "method": method, "id": id};
if (params != null) message["params"] = params;
_webSocket.add(JSON.encode(message));
var value = await _webSocketBroadcastStream
.firstWhere((response) => response["id"] == id);
printOnFailure("Web Socket request $method with params $params\n"
"Result: $value");
expect(value["id"], equals(id));
return value;
}
/// Returns the URL 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.
String getServerUrl([String root, String path]) {
if (root == null) root = 'web';
expect(_ports, contains(root));
var url = "http://localhost:${_ports[root]}";
if (path != null) url = "$url/$path";
return url;
}
/// 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.
void registerServerPort(String root, int port) {
_ports[root] = port;
}