25952fd4a0
Change-Id: If623390580fd332affe6820a20b0706a7befccbb Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/392402 Reviewed-by: Ben Konyi <bkonyi@google.com> Commit-Queue: Devon Carew <devoncarew@google.com>
628 lines
19 KiB
Dart
628 lines
19 KiB
Dart
// Copyright (c) 2019, 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.
|
|
|
|
@TestOn('vm')
|
|
library;
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:async/async.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:vm_service/vm_service.dart';
|
|
import 'package:vm_service_interface/vm_service_interface.dart';
|
|
|
|
void main() {
|
|
group('method delegation', () {
|
|
late MockVmService serviceMock;
|
|
late StreamController<Map<String, Object>> requestsController;
|
|
late StreamController<Map<String, Object?>> responsesController;
|
|
late ServiceExtensionRegistry serviceRegistry;
|
|
|
|
setUp(() {
|
|
serviceMock = MockVmService();
|
|
requestsController = StreamController();
|
|
responsesController = StreamController();
|
|
serviceRegistry = ServiceExtensionRegistry();
|
|
|
|
VmServerConnection(
|
|
requestsController.stream,
|
|
responsesController.sink,
|
|
serviceRegistry,
|
|
serviceMock,
|
|
);
|
|
});
|
|
|
|
tearDown(() {
|
|
requestsController.close();
|
|
responsesController.close();
|
|
});
|
|
|
|
test('works for simple methods', () {
|
|
var request = rpcRequest('getVersion');
|
|
var version = Version(major: 1, minor: 0);
|
|
|
|
serviceMock.version = version;
|
|
|
|
expect(responsesController.stream, emits(rpcResponse(version)));
|
|
requestsController.add(request);
|
|
});
|
|
|
|
test('works for methods with parameters', () {
|
|
var isolate = Isolate(
|
|
name: 'isolate',
|
|
exceptionPauseMode: ExceptionPauseMode.kNone,
|
|
id: '123',
|
|
number: '0',
|
|
startTime: 1,
|
|
runnable: true,
|
|
livePorts: 2,
|
|
isolateFlags: [],
|
|
pauseOnExit: false,
|
|
pauseEvent: Event(
|
|
kind: EventKind.kResume,
|
|
timestamp: 3,
|
|
),
|
|
libraries: [],
|
|
breakpoints: [],
|
|
isSystemIsolate: false,
|
|
);
|
|
var request =
|
|
rpcRequest('getIsolate', params: {'isolateId': isolate.id!});
|
|
|
|
serviceMock.isolates[isolate.id!] = isolate;
|
|
|
|
expect(responsesController.stream, emits(rpcResponse(isolate)));
|
|
requestsController.add(request);
|
|
});
|
|
|
|
test('works for methods with list parameters', () {
|
|
var isolate = Isolate(
|
|
name: 'isolate',
|
|
exceptionPauseMode: ExceptionPauseMode.kNone,
|
|
id: '123',
|
|
number: '0',
|
|
startTime: 1,
|
|
runnable: true,
|
|
livePorts: 2,
|
|
isolateFlags: [],
|
|
pauseOnExit: false,
|
|
pauseEvent: Event(
|
|
kind: EventKind.kResume,
|
|
timestamp: 3,
|
|
),
|
|
libraries: [],
|
|
breakpoints: [],
|
|
isSystemIsolate: false,
|
|
);
|
|
var request = rpcRequest('setVMTimelineFlags', params: {
|
|
'isolateId': isolate.id!,
|
|
// Note: the dynamic list below is intentional in order to exercise the
|
|
// code under test.
|
|
'recordedStreams': <dynamic>['GC', 'Dart', 'Embedder'],
|
|
});
|
|
|
|
serviceMock.isolates[isolate.id!] = isolate;
|
|
var response = Success();
|
|
|
|
expect(responsesController.stream, emits(rpcResponse(response)));
|
|
requestsController.add(request);
|
|
});
|
|
});
|
|
|
|
group('custom service extensions', () {
|
|
late MockVmService serviceMock;
|
|
late StreamController<Map<String, Object>> requestsController;
|
|
late StreamController<Map<String, Object?>> responsesController;
|
|
late ServiceExtensionRegistry serviceRegistry;
|
|
|
|
setUp(() {
|
|
serviceMock = MockVmService();
|
|
requestsController = StreamController();
|
|
responsesController = StreamController();
|
|
serviceRegistry = ServiceExtensionRegistry();
|
|
|
|
VmServerConnection(
|
|
requestsController.stream,
|
|
responsesController.sink,
|
|
serviceRegistry,
|
|
serviceMock,
|
|
);
|
|
});
|
|
|
|
tearDown(() {
|
|
requestsController.close();
|
|
responsesController.close();
|
|
});
|
|
|
|
test('with no params or isolateId', () {
|
|
var extension = 'ext.cool';
|
|
var request = rpcRequest(extension, params: null);
|
|
var response = Response()..json = {'hello': 'world'};
|
|
|
|
serviceMock.serviceExtensionResponse = response;
|
|
requestsController.add(request);
|
|
|
|
expect(responsesController.stream, emits(rpcResponse(response)));
|
|
});
|
|
|
|
test('with isolateId and no other params', () {
|
|
var extension = 'ext.cool';
|
|
var request = rpcRequest(extension, params: {'isolateId': '1'});
|
|
var response = Response()..json = {'hello': 'world'};
|
|
|
|
serviceMock.serviceExtensionResponse = response;
|
|
requestsController.add(request);
|
|
|
|
expect(responsesController.stream, emits(rpcResponse(response)));
|
|
});
|
|
|
|
test('with params and no isolateId', () {
|
|
var extension = 'ext.cool';
|
|
var params = {'cool': 'option'};
|
|
var request = rpcRequest(extension, params: params);
|
|
var response = Response()..json = {'hello': 'world'};
|
|
|
|
serviceMock.serviceExtensionResponse = response;
|
|
requestsController.add(request);
|
|
|
|
expect(responsesController.stream, emits(rpcResponse(response)));
|
|
});
|
|
|
|
test('with params and isolateId', () {
|
|
var extension = 'ext.cool';
|
|
var params = {'cool': 'option'};
|
|
var request =
|
|
rpcRequest(extension, params: Map.of(params)..['isolateId'] = '1');
|
|
var response = Response()..json = {'hello': 'world'};
|
|
|
|
serviceMock.serviceExtensionResponse = response;
|
|
requestsController.add(request);
|
|
|
|
expect(responsesController.stream, emits(rpcResponse(response)));
|
|
});
|
|
});
|
|
|
|
group('error handling', () {
|
|
late MockVmService serviceMock;
|
|
late StreamController<Map<String, Object>> requestsController;
|
|
late StreamController<Map<String, Object?>> responsesController;
|
|
late ServiceExtensionRegistry serviceRegistry;
|
|
|
|
setUp(() {
|
|
serviceMock = MockVmService();
|
|
requestsController = StreamController();
|
|
responsesController = StreamController();
|
|
serviceRegistry = ServiceExtensionRegistry();
|
|
|
|
VmServerConnection(
|
|
requestsController.stream,
|
|
responsesController.sink,
|
|
serviceRegistry,
|
|
serviceMock,
|
|
);
|
|
});
|
|
|
|
tearDown(() {
|
|
requestsController.close();
|
|
responsesController.close();
|
|
});
|
|
|
|
test('special cases RPCError instances', () {
|
|
var request = rpcRequest('getVersion');
|
|
var error =
|
|
RPCError('getVersion', 1234, 'custom message', {'custom': 'data'});
|
|
|
|
serviceMock.versionError = error;
|
|
requestsController.add(request);
|
|
|
|
expect(responsesController.stream, emits(rpcErrorResponse(error)));
|
|
});
|
|
|
|
test('has a fallback for generic exceptions', () {
|
|
var request = rpcRequest('getVersion');
|
|
|
|
requestsController.add(request);
|
|
|
|
expect(
|
|
responsesController.stream.map((response) => '$response'),
|
|
emits(startsWith('{jsonrpc: 2.0, id: 1, '
|
|
'error: {code: -32603, message: getVersion: UnimplementedError')));
|
|
});
|
|
});
|
|
|
|
group('streams', () {
|
|
late MockVmService serviceMock;
|
|
late StreamController<Map<String, Object>> requestsController;
|
|
late StreamController<Map<String, Object?>> responsesController;
|
|
late ServiceExtensionRegistry serviceRegistry;
|
|
|
|
setUp(() {
|
|
serviceMock = MockVmService();
|
|
requestsController = StreamController();
|
|
responsesController = StreamController();
|
|
serviceRegistry = ServiceExtensionRegistry();
|
|
|
|
VmServerConnection(
|
|
requestsController.stream,
|
|
responsesController.sink,
|
|
serviceRegistry,
|
|
serviceMock,
|
|
);
|
|
});
|
|
|
|
tearDown(() {
|
|
requestsController.close();
|
|
responsesController.close();
|
|
});
|
|
|
|
test('can be listened to and canceled', () async {
|
|
StreamController<Event> eventController;
|
|
var responseQueue = StreamQueue(responsesController.stream);
|
|
|
|
{
|
|
var request =
|
|
rpcRequest('streamListen', params: {'streamId': 'Isolate'});
|
|
var response = Success();
|
|
requestsController.add(request);
|
|
|
|
await expectLater(responseQueue, emitsThrough(rpcResponse(response)));
|
|
|
|
eventController = serviceMock.streamControllers['Isolate']!;
|
|
|
|
var events = [
|
|
Event(
|
|
kind: EventKind.kIsolateStart,
|
|
timestamp: 0,
|
|
),
|
|
Event(
|
|
kind: EventKind.kIsolateExit,
|
|
timestamp: 1,
|
|
)
|
|
];
|
|
events.forEach(eventController.add);
|
|
await expectLater(
|
|
responseQueue,
|
|
emitsInOrder(
|
|
events.map((event) => streamNotifyResponse('Isolate', event))));
|
|
}
|
|
|
|
{
|
|
var request =
|
|
rpcRequest('streamCancel', params: {'streamId': 'Isolate'});
|
|
var response = Success();
|
|
requestsController.add(request);
|
|
|
|
await expectLater(responseQueue, emitsThrough(rpcResponse(response)));
|
|
|
|
var nextEvent = Event(
|
|
kind: EventKind.kIsolateReload,
|
|
timestamp: 2,
|
|
);
|
|
eventController.add(nextEvent);
|
|
expect(responseQueue,
|
|
neverEmits(streamNotifyResponse('Isolate', nextEvent)));
|
|
|
|
await pumpEventQueue();
|
|
await eventController.close();
|
|
await responsesController.close();
|
|
}
|
|
});
|
|
|
|
test("can't be listened to twice", () {
|
|
var responseQueue = StreamQueue(responsesController.stream);
|
|
|
|
{
|
|
var request =
|
|
rpcRequest('streamListen', params: {'streamId': 'Isolate'});
|
|
var response = Success();
|
|
requestsController.add(request);
|
|
|
|
expect(responseQueue, emitsThrough(rpcResponse(response)));
|
|
}
|
|
|
|
{
|
|
var request =
|
|
rpcRequest('streamListen', params: {'streamId': 'Isolate'});
|
|
requestsController.add(request);
|
|
|
|
expect(
|
|
responseQueue,
|
|
emitsThrough(rpcErrorResponse(
|
|
RPCError('streamSubscribe', 103, 'Stream already subscribed', {
|
|
'details': "The stream 'Isolate' is already subscribed",
|
|
}))),
|
|
);
|
|
}
|
|
});
|
|
|
|
test("can't cancel a stream that isn't being listened to", () {
|
|
var streamId = 'Isolate';
|
|
var responseQueue = StreamQueue(responsesController.stream);
|
|
|
|
var request = rpcRequest('streamCancel', params: {'streamId': streamId});
|
|
requestsController.add(request);
|
|
|
|
expect(
|
|
responseQueue,
|
|
emitsThrough(rpcErrorResponse(
|
|
RPCError('streamCancel', 104, 'Stream not subscribed', {
|
|
'details': "The stream '$streamId' is not subscribed",
|
|
}))),
|
|
);
|
|
});
|
|
|
|
test('gives register and unregister events', () async {
|
|
var serviceId = 'ext.test.service';
|
|
var serviceRegisteredEvent = streamNotifyResponse(
|
|
'Service',
|
|
Event(
|
|
kind: EventKind.kServiceRegistered,
|
|
timestamp: 0,
|
|
method: serviceId,
|
|
service: serviceId,
|
|
),
|
|
);
|
|
var serviceUnRegisteredEvent = streamNotifyResponse(
|
|
'Service',
|
|
Event(
|
|
kind: EventKind.kServiceUnregistered,
|
|
timestamp: 0,
|
|
method: serviceId,
|
|
service: serviceId,
|
|
),
|
|
);
|
|
|
|
requestsController
|
|
.add(rpcRequest('streamListen', params: {'streamId': 'Service'}));
|
|
requestsController
|
|
.add(rpcRequest('registerService', params: {'service': serviceId}));
|
|
await expectLater(
|
|
responsesController.stream
|
|
.map((Map response) => stripEventTimestamp(response)),
|
|
emitsThrough(serviceRegisteredEvent));
|
|
|
|
// Connect another client to get the previous register events and the
|
|
// unregister event.
|
|
var requestsController2 = StreamController<Map<String, Object>>();
|
|
var responsesController2 = StreamController<Map<String, Object?>>();
|
|
addTearDown(() {
|
|
requestsController2.close();
|
|
responsesController2.close();
|
|
});
|
|
|
|
VmServerConnection(
|
|
requestsController2.stream,
|
|
responsesController2.sink,
|
|
serviceRegistry,
|
|
MockVmService(),
|
|
);
|
|
|
|
expect(
|
|
responsesController2.stream
|
|
.map((Map response) => stripEventTimestamp(response)),
|
|
emitsThrough(emitsInOrder(
|
|
[serviceRegisteredEvent, serviceUnRegisteredEvent])));
|
|
|
|
// Should get the previously registered extension event, as well as
|
|
// the unregister event when the client disconnects.
|
|
requestsController2
|
|
.add(rpcRequest('streamListen', params: {'streamId': 'Service'}));
|
|
// Need to give the client a chance to subscribe.
|
|
await pumpEventQueue();
|
|
unawaited(requestsController.close());
|
|
// Give the old client a chance to shut down
|
|
await pumpEventQueue();
|
|
|
|
// Connect yet another client, it should get zero registration or
|
|
// unregistration events.
|
|
var requestsController3 = StreamController<Map<String, Object>>();
|
|
var responsesController3 = StreamController<Map<String, Object?>>();
|
|
|
|
VmServerConnection(
|
|
requestsController3.stream,
|
|
responsesController3.sink,
|
|
serviceRegistry,
|
|
MockVmService(),
|
|
);
|
|
|
|
expect(responsesController3.stream,
|
|
neverEmits(anyOf(serviceRegisteredEvent, serviceUnRegisteredEvent)));
|
|
// Give it a chance to deliver events.
|
|
await pumpEventQueue();
|
|
// Disconnect the client so the test can shut down.
|
|
unawaited(requestsController3.close());
|
|
unawaited(responsesController3.close());
|
|
});
|
|
});
|
|
|
|
group('registerService', () {
|
|
late MockVmService serviceMock;
|
|
late StreamController<Map<String, Object>> requestsController;
|
|
late StreamController<Map<String, Object?>> responsesController;
|
|
late ServiceExtensionRegistry serviceRegistry;
|
|
|
|
setUp(() {
|
|
serviceMock = MockVmService();
|
|
requestsController = StreamController();
|
|
responsesController = StreamController();
|
|
serviceRegistry = ServiceExtensionRegistry();
|
|
|
|
VmServerConnection(
|
|
requestsController.stream,
|
|
responsesController.sink,
|
|
serviceRegistry,
|
|
serviceMock,
|
|
);
|
|
});
|
|
|
|
tearDown(() {
|
|
requestsController.close();
|
|
responsesController.close();
|
|
});
|
|
|
|
test('registerService can delegate requests between clients', () async {
|
|
var serviceId = 'ext.test.service';
|
|
var responseQueue = StreamQueue(responsesController.stream);
|
|
|
|
var clientInputController =
|
|
StreamController<Map<String, Object?>>.broadcast();
|
|
var clientOutputController =
|
|
StreamController<Map<String, Object>>.broadcast();
|
|
var client = VmService(
|
|
clientInputController.stream.map(jsonEncode),
|
|
(String message) => clientOutputController
|
|
.add(jsonDecode(message).cast<String, Object>()),
|
|
disposeHandler: () async {
|
|
await clientInputController.close();
|
|
await clientOutputController.close();
|
|
});
|
|
|
|
var clientConnection = VmServerConnection(clientOutputController.stream,
|
|
clientInputController.sink, serviceRegistry, serviceMock);
|
|
|
|
var requestParams = {'foo': 'bar'};
|
|
var expectedResponse = Response()..json = {'zap': 'zip'};
|
|
await client.registerService(serviceId, 'service');
|
|
// Duplicate registrations should fail.
|
|
expect(client.registerService(serviceId, 'service'),
|
|
throwsA(const TypeMatcher<RPCError>()));
|
|
|
|
client.registerServiceCallback(serviceId, (request) async {
|
|
expect(request, equals(requestParams));
|
|
return {'result': expectedResponse.toJson()};
|
|
});
|
|
|
|
var serviceRequest = rpcRequest(serviceId, params: requestParams);
|
|
|
|
requestsController.add(serviceRequest);
|
|
expect(await responseQueue.next, rpcResponse(expectedResponse));
|
|
|
|
// Kill the client that registered the handler, it should now fall back
|
|
// on `callServiceExtension`.
|
|
await client.dispose();
|
|
// This should complete as well.
|
|
await clientConnection.done;
|
|
|
|
var mockResponse = Response()..json = {'mock': 'response'};
|
|
serviceMock.serviceExtensionResponse = mockResponse;
|
|
requestsController.add(serviceRequest);
|
|
|
|
expect(await responseQueue.next, rpcResponse(mockResponse));
|
|
});
|
|
});
|
|
}
|
|
|
|
Map<String, Object> rpcRequest(String method,
|
|
{Map<String, Object>? params = const {}, String id = '1'}) =>
|
|
{
|
|
'jsonrpc': '2.0',
|
|
'method': method,
|
|
if (params != null) 'params': params,
|
|
'id': id,
|
|
};
|
|
|
|
Map<String, Object> rpcResponse(Response response, {String id = '1'}) => {
|
|
'jsonrpc': '2.0',
|
|
'id': id,
|
|
'result': response.toJson(),
|
|
};
|
|
|
|
Map<String, Object> rpcErrorResponse(Object error, {String id = '1'}) {
|
|
Map<String, Object> errorJson;
|
|
if (error is RPCError) {
|
|
errorJson = {
|
|
'code': error.code,
|
|
'message': error.message,
|
|
};
|
|
if (error.data != null) {
|
|
errorJson['data'] = error.data!;
|
|
}
|
|
} else {
|
|
errorJson = {
|
|
'code': -32603,
|
|
'message': error.toString(),
|
|
};
|
|
}
|
|
return {
|
|
'jsonrpc': '2.0',
|
|
'id': id,
|
|
'error': errorJson,
|
|
};
|
|
}
|
|
|
|
Map<String, Object> streamNotifyResponse(String streamId, Event event) {
|
|
return {
|
|
'jsonrpc': '2.0',
|
|
'method': 'streamNotify',
|
|
'params': {
|
|
'streamId': streamId,
|
|
'event': event.toJson(),
|
|
},
|
|
};
|
|
}
|
|
|
|
Map<String, Object?> stripEventTimestamp(Map response) {
|
|
if (response.containsKey('params') &&
|
|
response['params'].containsKey('event')) {
|
|
response['params']['event']['timestamp'] = 0;
|
|
}
|
|
return response as Map<String, Object?>;
|
|
}
|
|
|
|
class MockVmService implements VmServiceInterface {
|
|
Version? version;
|
|
RPCError? versionError;
|
|
Response? serviceExtensionResponse;
|
|
|
|
final Map<String, Isolate> isolates = {};
|
|
final streamControllers = <String, StreamController<Event>>{};
|
|
|
|
// Override `noSuchMethod` to capture any [VmServiceInterface] method we don't
|
|
// explicitly override for testing purposes.
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) {
|
|
throw UnimplementedError('$invocation.memberName');
|
|
}
|
|
|
|
@override
|
|
Future<Response> callServiceExtension(String method,
|
|
{String? isolateId, Map<String, dynamic>? args}) {
|
|
return Future.value(serviceExtensionResponse!);
|
|
}
|
|
|
|
@override
|
|
Future<Isolate> getIsolate(String isolateId) {
|
|
return Future.sync(() => isolates[isolateId]!);
|
|
}
|
|
|
|
@override
|
|
Future<Version> getVersion() {
|
|
if (versionError != null) {
|
|
return Future.error(versionError!);
|
|
} else if (version != null) {
|
|
return Future.value(version!);
|
|
} else {
|
|
return Future.error(UnimplementedError('getVersion'));
|
|
}
|
|
}
|
|
|
|
@override
|
|
Stream<Event> onEvent(String streamId) {
|
|
return streamControllers
|
|
.putIfAbsent(streamId, () => StreamController<Event>())
|
|
.stream;
|
|
}
|
|
|
|
@override
|
|
Future<Success> setVMTimelineFlags(List<String> recordedStreams) {
|
|
return Future.value(Success());
|
|
}
|
|
|
|
@override
|
|
Future<Success> streamListen(String streamId) {
|
|
return Future.value(Success());
|
|
}
|
|
}
|