285f7ddb1b
Change-Id: I4fd1b4e192ec1d0e53a4b3d81e5fdb8c1bb40208 Reviewed-on: https://dart-review.googlesource.com/c/85380 Commit-Queue: Danny Tuppeny <dantup@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
485 lines
15 KiB
Dart
485 lines
15 KiB
Dart
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS 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:analysis_server/lsp_protocol/protocol_generated.dart' as lsp;
|
|
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
|
|
import 'package:analysis_server/lsp_protocol/protocol_special.dart' as lsp;
|
|
import 'package:analysis_server/protocol/protocol.dart';
|
|
import 'package:analysis_server/protocol/protocol_generated.dart';
|
|
import 'package:analysis_server/src/analysis_server.dart';
|
|
import 'package:analysis_server/src/channel/channel.dart';
|
|
import 'package:analysis_server/src/lsp/channel/lsp_channel.dart';
|
|
import 'package:analyzer/src/generated/source.dart';
|
|
import 'package:analyzer/src/generated/timestamped_data.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
/**
|
|
* Answer the absolute path the SDK relative to the currently running
|
|
* script or throw an exception if it cannot be found.
|
|
*/
|
|
String get sdkPath {
|
|
Uri sdkUri = Platform.script.resolve('../../../sdk/');
|
|
|
|
// Verify the directory exists
|
|
Directory sdkDir = new Directory.fromUri(sdkUri);
|
|
if (!sdkDir.existsSync()) {
|
|
throw 'Specified Dart SDK does not exist: $sdkDir';
|
|
}
|
|
|
|
return sdkDir.path;
|
|
}
|
|
|
|
/**
|
|
* A [Matcher] that check that the given [Response] has an expected identifier
|
|
* and has an error. The error code may optionally be checked.
|
|
*/
|
|
Matcher isResponseFailure(String id, [RequestErrorCode code]) =>
|
|
new _IsResponseFailure(id, code);
|
|
|
|
/**
|
|
* A [Matcher] that check that the given [Response] has an expected identifier
|
|
* and no error.
|
|
*/
|
|
Matcher isResponseSuccess(String id) => new _IsResponseSuccess(id);
|
|
|
|
/**
|
|
* A mock [LspServerCommunicationChannel] for testing [LspAnalysisServer].
|
|
*/
|
|
class MockLspServerChannel implements LspServerCommunicationChannel {
|
|
// TODO(dantup): Support server-to-client requests. We'll probably need to
|
|
// change IncomingMessage to just Message for both directions?
|
|
final StreamController<lsp.IncomingMessage> _clientToServer =
|
|
new StreamController<lsp.IncomingMessage>.broadcast();
|
|
final StreamController<lsp.Message> _serverToClient =
|
|
new StreamController<lsp.Message>.broadcast();
|
|
|
|
bool _closed = false;
|
|
|
|
String name;
|
|
|
|
MockLspServerChannel(bool _printMessages) {
|
|
if (_printMessages) {
|
|
_serverToClient.stream
|
|
.listen((message) => print('<== ' + jsonEncode(message)));
|
|
_clientToServer.stream
|
|
.listen((message) => print('==> ' + jsonEncode(message)));
|
|
}
|
|
}
|
|
|
|
Stream<lsp.Message> get serverToClient => _serverToClient.stream;
|
|
|
|
@override
|
|
void close() {
|
|
_closed = true;
|
|
}
|
|
|
|
/**
|
|
* A stream of [NotificationMessage]s from the server that may be errors.
|
|
*/
|
|
Stream<lsp.NotificationMessage> get errorNotificationsFromServer {
|
|
return notificationsFromServer.where(_isErrorNotification);
|
|
}
|
|
|
|
@override
|
|
void listen(void Function(lsp.IncomingMessage message) onMessage,
|
|
{Function onError, void Function() onDone}) {
|
|
_clientToServer.stream.listen(onMessage, onError: onError, onDone: onDone);
|
|
}
|
|
|
|
/**
|
|
* A stream of [NotificationMessage]s from the server.
|
|
*/
|
|
Stream<lsp.NotificationMessage> get notificationsFromServer {
|
|
return _serverToClient.stream
|
|
.where((m) => m is lsp.NotificationMessage)
|
|
.cast<lsp.NotificationMessage>();
|
|
}
|
|
|
|
@override
|
|
void sendNotification(lsp.NotificationMessage notification) {
|
|
// Don't deliver notifications after the connection is closed.
|
|
if (_closed) {
|
|
return;
|
|
}
|
|
_serverToClient.add(notification);
|
|
}
|
|
|
|
void sendNotificationToServer(lsp.NotificationMessage notification) {
|
|
// Don't deliver notifications after the connection is closed.
|
|
if (_closed) {
|
|
return;
|
|
}
|
|
notification = _convertJson(notification, lsp.NotificationMessage.fromJson);
|
|
_clientToServer.add(notification);
|
|
}
|
|
|
|
/**
|
|
* Send the given [request] to the server and return a future that will
|
|
* complete when a response associated with the [request] has been received.
|
|
* The value of the future will be the received response.
|
|
*/
|
|
Future<lsp.ResponseMessage> sendRequestToServer(lsp.RequestMessage request) {
|
|
// No further requests should be sent after the connection is closed.
|
|
if (_closed) {
|
|
throw new Exception('sendLspRequest after connection closed');
|
|
}
|
|
request = _convertJson(request, lsp.RequestMessage.fromJson);
|
|
// Wrap send request in future to simulate WebSocket.
|
|
new Future(() => _clientToServer.add(request));
|
|
return waitForResponse(request);
|
|
}
|
|
|
|
@override
|
|
void sendResponse(lsp.ResponseMessage response) {
|
|
// Don't deliver responses after the connection is closed.
|
|
if (_closed) {
|
|
return;
|
|
}
|
|
// Wrap send response in future to simulate WebSocket.
|
|
new Future(() => _serverToClient.add(response));
|
|
}
|
|
|
|
/**
|
|
* Return a future that will complete when a response associated with the
|
|
* given [request] has been received. The value of the future will be the
|
|
* received response. The returned future will throw an exception if a server
|
|
* error is reported before the response has been received.
|
|
*
|
|
* Unlike [sendLspRequest], this method assumes that the [request] has already
|
|
* been sent to the server.
|
|
*/
|
|
Future<lsp.ResponseMessage> waitForResponse(
|
|
lsp.RequestMessage request) async {
|
|
// TODO(dantup): Make this throw if an error notifications comes in (as
|
|
// described above).
|
|
final response = await _serverToClient.stream.firstWhere((response) =>
|
|
response is lsp.ResponseMessage && response.id == request.id);
|
|
return response;
|
|
}
|
|
|
|
/// Round trips the object to JSON and back to ensure it behaves the same as
|
|
/// when running over the real STDIO server. Without this, the object passed
|
|
/// to the handlers will have concrete types as constructed in tests rather
|
|
/// than the maps as they would be (the server expects to do the conversion).
|
|
T _convertJson<T>(
|
|
lsp.ToJsonable message, T Function(Map<String, dynamic>) constructor) {
|
|
return constructor(jsonDecode(jsonEncode(message.toJson())));
|
|
}
|
|
|
|
/// Checks whether a notification is likely an error from the server (for
|
|
/// example a window/showMessage). This is useful for tests that want to
|
|
/// ensure no errors come from the server in response to notifications (which
|
|
/// don't have their own responses).
|
|
bool _isErrorNotification(lsp.NotificationMessage notification) {
|
|
return notification.method == Method.window_logMessage ||
|
|
notification.method == Method.window_showMessage;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A mock [ServerCommunicationChannel] for testing [AnalysisServer].
|
|
*/
|
|
class MockServerChannel implements ServerCommunicationChannel {
|
|
StreamController<Request> requestController = new StreamController<Request>();
|
|
StreamController<Response> responseController =
|
|
new StreamController<Response>.broadcast();
|
|
StreamController<Notification> notificationController =
|
|
new StreamController<Notification>(sync: true);
|
|
Completer<Response> errorCompleter;
|
|
|
|
List<Response> responsesReceived = [];
|
|
List<Notification> notificationsReceived = [];
|
|
|
|
bool _closed = false;
|
|
|
|
String name;
|
|
|
|
MockServerChannel();
|
|
|
|
@override
|
|
void close() {
|
|
_closed = true;
|
|
}
|
|
|
|
void expectMsgCount({responseCount: 0, notificationCount: 0}) {
|
|
expect(responsesReceived, hasLength(responseCount));
|
|
expect(notificationsReceived, hasLength(notificationCount));
|
|
}
|
|
|
|
@override
|
|
void listen(void onRequest(Request request),
|
|
{Function onError, void onDone()}) {
|
|
requestController.stream
|
|
.listen(onRequest, onError: onError, onDone: onDone);
|
|
}
|
|
|
|
@override
|
|
void sendNotification(Notification notification) {
|
|
// Don't deliver notifications after the connection is closed.
|
|
if (_closed) {
|
|
return;
|
|
}
|
|
notificationsReceived.add(notification);
|
|
if (errorCompleter != null && notification.event == 'server.error') {
|
|
print(
|
|
'[server.error] test: $name message: ${notification.params['message']}');
|
|
errorCompleter.completeError(
|
|
new ServerError(notification.params['message']),
|
|
new StackTrace.fromString(notification.params['stackTrace']));
|
|
}
|
|
// Wrap send notification in future to simulate websocket
|
|
// TODO(scheglov) ask Dan why and decide what to do
|
|
// new Future(() => notificationController.add(notification));
|
|
notificationController.add(notification);
|
|
}
|
|
|
|
/**
|
|
* Send the given [request] to the server and return a future that will
|
|
* complete when a response associated with the [request] has been received.
|
|
* The value of the future will be the received response. If [throwOnError] is
|
|
* `true` (the default) then the returned future will throw an exception if a
|
|
* server error is reported before the response has been received.
|
|
*/
|
|
Future<Response> sendRequest(Request request, {bool throwOnError = true}) {
|
|
// TODO(brianwilkerson) Attempt to remove the `throwOnError` parameter and
|
|
// have the default behavior be the only behavior.
|
|
// No further requests should be sent after the connection is closed.
|
|
if (_closed) {
|
|
throw new Exception('sendRequest after connection closed');
|
|
}
|
|
// Wrap send request in future to simulate WebSocket.
|
|
new Future(() => requestController.add(request));
|
|
return waitForResponse(request, throwOnError: throwOnError);
|
|
}
|
|
|
|
@override
|
|
void sendResponse(Response response) {
|
|
// Don't deliver responses after the connection is closed.
|
|
if (_closed) {
|
|
return;
|
|
}
|
|
responsesReceived.add(response);
|
|
// Wrap send response in future to simulate WebSocket.
|
|
new Future(() => responseController.add(response));
|
|
}
|
|
|
|
/**
|
|
* Return a future that will complete when a response associated with the
|
|
* given [request] has been received. The value of the future will be the
|
|
* received response. If [throwOnError] is `true` (the default) then the
|
|
* returned future will throw an exception if a server error is reported
|
|
* before the response has been received.
|
|
*
|
|
* Unlike [sendRequest], this method assumes that the [request] has already
|
|
* been sent to the server.
|
|
*/
|
|
Future<Response> waitForResponse(Request request,
|
|
{bool throwOnError = true}) {
|
|
// TODO(brianwilkerson) Attempt to remove the `throwOnError` parameter and
|
|
// have the default behavior be the only behavior.
|
|
String id = request.id;
|
|
Future<Response> response =
|
|
responseController.stream.firstWhere((response) => response.id == id);
|
|
if (throwOnError) {
|
|
errorCompleter = new Completer<Response>();
|
|
try {
|
|
return Future.any([response, errorCompleter.future]);
|
|
} finally {
|
|
errorCompleter = null;
|
|
}
|
|
}
|
|
return response;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A mock [WebSocket] for testing.
|
|
*/
|
|
class MockSocket<T> implements WebSocket {
|
|
StreamController<T> controller = new StreamController<T>();
|
|
MockSocket<T> twin;
|
|
Stream<T> stream;
|
|
|
|
MockSocket();
|
|
|
|
factory MockSocket.pair() {
|
|
MockSocket<T> socket1 = new MockSocket<T>();
|
|
MockSocket<T> socket2 = new MockSocket<T>();
|
|
socket1.twin = socket2;
|
|
socket2.twin = socket1;
|
|
socket1.stream = socket2.controller.stream;
|
|
socket2.stream = socket1.controller.stream;
|
|
return socket1;
|
|
}
|
|
|
|
void add(dynamic text) => controller.add(text as T);
|
|
|
|
void allowMultipleListeners() {
|
|
stream = stream.asBroadcastStream();
|
|
}
|
|
|
|
Future close([int code, String reason]) =>
|
|
controller.close().then((_) => twin.controller.close());
|
|
|
|
StreamSubscription<T> listen(void onData(dynamic event),
|
|
{Function onError, void onDone(), bool cancelOnError}) =>
|
|
stream.listen((T data) => onData(data),
|
|
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
|
|
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
|
|
Stream<T> where(bool test(dynamic t)) => stream.where((T data) => test(data));
|
|
}
|
|
|
|
class MockSource extends StringTypedMock implements Source {
|
|
@override
|
|
TimestampedData<String> contents = null;
|
|
|
|
@override
|
|
String encoding = null;
|
|
|
|
@override
|
|
String fullName = null;
|
|
|
|
@override
|
|
bool isInSystemLibrary = null;
|
|
|
|
@override
|
|
Source librarySource = null;
|
|
|
|
@override
|
|
int modificationStamp = null;
|
|
|
|
@override
|
|
String shortName = null;
|
|
|
|
@override
|
|
Source source = null;
|
|
|
|
@override
|
|
Uri uri = null;
|
|
|
|
@override
|
|
UriKind uriKind = null;
|
|
|
|
MockSource([String name = 'mocked.dart']) : super(name);
|
|
|
|
@override
|
|
bool exists() => null;
|
|
}
|
|
|
|
class ServerError implements Exception {
|
|
final message;
|
|
|
|
ServerError(this.message);
|
|
|
|
String toString() {
|
|
return "Server Error: $message";
|
|
}
|
|
}
|
|
|
|
class StringTypedMock {
|
|
String _toString;
|
|
|
|
StringTypedMock(this._toString);
|
|
|
|
@override
|
|
String toString() {
|
|
if (_toString != null) {
|
|
return _toString;
|
|
}
|
|
return super.toString();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A [Matcher] that check that there are no `error` in a given [Response].
|
|
*/
|
|
class _IsResponseFailure extends Matcher {
|
|
final String _id;
|
|
final RequestErrorCode _code;
|
|
|
|
_IsResponseFailure(this._id, this._code);
|
|
|
|
@override
|
|
Description describe(Description description) {
|
|
description =
|
|
description.add('response with identifier "$_id" and an error');
|
|
if (_code != null) {
|
|
description = description.add(' with code ${this._code.name}');
|
|
}
|
|
return description;
|
|
}
|
|
|
|
@override
|
|
Description describeMismatch(
|
|
item, Description mismatchDescription, Map matchState, bool verbose) {
|
|
Response response = item;
|
|
var id = response.id;
|
|
RequestError error = response.error;
|
|
mismatchDescription.add('has identifier "$id"');
|
|
if (error == null) {
|
|
mismatchDescription.add(' and has no error');
|
|
} else {
|
|
mismatchDescription
|
|
.add(' and has error code ${response.error.code.name}');
|
|
}
|
|
return mismatchDescription;
|
|
}
|
|
|
|
@override
|
|
bool matches(item, Map matchState) {
|
|
Response response = item;
|
|
if (response.id != _id || response.error == null) {
|
|
return false;
|
|
}
|
|
if (_code != null && response.error.code != _code) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A [Matcher] that check that there are no `error` in a given [Response].
|
|
*/
|
|
class _IsResponseSuccess extends Matcher {
|
|
final String _id;
|
|
|
|
_IsResponseSuccess(this._id);
|
|
|
|
@override
|
|
Description describe(Description description) {
|
|
return description
|
|
.addDescriptionOf('response with identifier "$_id" and without error');
|
|
}
|
|
|
|
@override
|
|
Description describeMismatch(
|
|
item, Description mismatchDescription, Map matchState, bool verbose) {
|
|
Response response = item;
|
|
if (response == null) {
|
|
mismatchDescription.add('is null response');
|
|
} else {
|
|
var id = response.id;
|
|
RequestError error = response.error;
|
|
mismatchDescription.add('has identifier "$id"');
|
|
if (error != null) {
|
|
mismatchDescription.add(' and has error $error');
|
|
}
|
|
}
|
|
return mismatchDescription;
|
|
}
|
|
|
|
@override
|
|
bool matches(item, Map matchState) {
|
|
Response response = item;
|
|
return response != null && response.id == _id && response.error == null;
|
|
}
|
|
}
|