Add a DTD method to get all registered services.
This CL contains breaking changes for package:dtd and prepares both package:dart_service_protocol_shared and package:dtd for publish. This CL also fixes https://github.com/dart-lang/sdk/issues/60757 so that DTD-registered services are sent over the `Service` stream upon initial subscription like what is done for client-registered services. Change-Id: I619af816e64af01864c7ed9b98743c6691bf7e0b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/429161 Reviewed-by: Ben Konyi <bkonyi@google.com> Commit-Queue: Kenzie Davisson <kenzieschmoll@google.com>
This commit is contained in:
committed by
Commit Queue
parent
f4c87f2ae8
commit
10dd59c039
@@ -64,6 +64,14 @@ To run just the analysis server integration tests:
|
||||
./tools/test.py -mrelease pkg/analysis_server/test/integration/
|
||||
```
|
||||
|
||||
To run a single test:
|
||||
|
||||
```
|
||||
dart test pkg/analysis_server/test/some_test.dart
|
||||
```
|
||||
|
||||
> Note: `dart` may need to point to a Dart SDK built from source
|
||||
depending on the changes you are testing.
|
||||
|
||||
[building]: https://github.com/dart-lang/sdk/wiki/Building
|
||||
[contributing]: https://github.com/dart-lang/sdk/blob/master/CONTRIBUTING.md
|
||||
|
||||
@@ -29,10 +29,10 @@ class ExperimentalEchoHandler extends SharedMessageHandler<Object?, Object?> {
|
||||
MessageInfo message,
|
||||
CancellationToken token,
|
||||
) async {
|
||||
// The DTD client automatically converts `null` params to an empty map, but
|
||||
// (because of a previous bug) we want to test null results. So if the
|
||||
// params are an empty map, return null. This is tested by
|
||||
// `test_service_success_echo_nullResponse` in `SharedDtdTests`.
|
||||
// The DTD client may send an empty map as params. Return null in this case.
|
||||
// This is tested by
|
||||
//`test_service_success_echo_nullResponse_with_empty_params` in
|
||||
// `SharedDtdTests`.
|
||||
if (params is Map && params.isEmpty) {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ class DtdServices {
|
||||
var message = IncomingMessage(
|
||||
jsonrpc: jsonRpcVersion,
|
||||
method: method,
|
||||
params: params.asMap,
|
||||
params: params.value,
|
||||
);
|
||||
var scheduler = _server.messageScheduler;
|
||||
var completer = Completer<Map<String, Object?>>();
|
||||
|
||||
@@ -88,7 +88,10 @@ mixin SharedDtdTests
|
||||
|
||||
/// A list of service/methods that the test client has seen registered (and
|
||||
/// not yet unregistered) over the DTD connection.
|
||||
final availableMethods = <(String, Method)>[];
|
||||
///
|
||||
/// The service name is a nullable String because DTD-internal methods and
|
||||
/// services do not have a service name.
|
||||
final availableMethods = <(String?, Method)>[];
|
||||
|
||||
/// An invalid DTD URI used for testing connection failures.
|
||||
final invalidUri = Uri.parse('ws://invalid:345/invalid');
|
||||
@@ -207,12 +210,12 @@ mixin SharedDtdTests
|
||||
switch (e.kind) {
|
||||
case 'ServiceRegistered':
|
||||
availableMethods.add((
|
||||
e.data['service'] as String,
|
||||
e.data['service'] as String?,
|
||||
Method(e.data['method'] as String),
|
||||
));
|
||||
case 'ServiceUnregistered':
|
||||
availableMethods.remove((
|
||||
e.data['service'] as String,
|
||||
e.data['service'] as String?,
|
||||
Method(e.data['method'] as String),
|
||||
));
|
||||
}
|
||||
@@ -470,7 +473,24 @@ FutureOr<void>? a;
|
||||
expect(result, equals({'a': 'b'}));
|
||||
}
|
||||
|
||||
Future<void> test_service_success_echo_nullResponse() async {
|
||||
Future<void>
|
||||
test_service_success_echo_nullResponse_with_empty_params() async {
|
||||
await initializeServer();
|
||||
await sendConnectToDtdRequest(registerExperimentalHandlers: true);
|
||||
|
||||
var response = await dtd.connection.call(
|
||||
lspServiceName,
|
||||
CustomMethods.experimentalEcho.toString(),
|
||||
params: const <String, Object?>{},
|
||||
);
|
||||
|
||||
var result = response.result['result'] as Map<String, Object?>?;
|
||||
|
||||
expect(response.type, 'Null');
|
||||
expect(result, isNull);
|
||||
}
|
||||
|
||||
Future<void> test_service_success_echo_nullResponse_with_null_params() async {
|
||||
await initializeServer();
|
||||
await sendConnectToDtdRequest(registerExperimentalHandlers: true);
|
||||
|
||||
@@ -512,7 +532,10 @@ void [!myFun^ction!]() {}
|
||||
await initializeServer();
|
||||
await sendConnectToDtdRequest();
|
||||
|
||||
expect(availableMethods, isNotEmpty);
|
||||
var lspMethods = availableMethods.where(
|
||||
(serviceMethod) => serviceMethod.$1 == 'Lsp',
|
||||
);
|
||||
expect(lspMethods, isNotEmpty);
|
||||
|
||||
// Send a request to the server to connect to DTD. This will only complete
|
||||
// once all services are registered, however there's no guarantee about the
|
||||
@@ -521,9 +544,9 @@ void [!myFun^ction!]() {}
|
||||
await shutdownServer();
|
||||
|
||||
// Wait for the services to be unregistered.
|
||||
while (availableMethods.isNotEmpty) {
|
||||
while (lspMethods.isNotEmpty) {
|
||||
await pumpEventQueue(times: 5000);
|
||||
}
|
||||
expect(availableMethods, isEmpty);
|
||||
expect(lspMethods, isEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
## 0.0.3-wip
|
||||
## 0.0.3
|
||||
- Update sdk constraint to '^3.5.0'
|
||||
- Added `ClientServiceInfo.fromJson` and `ClientServiceInfo.toJson` methods.
|
||||
- Added `ClientServiceMethodInfo.fromJson` and `ClientServiceMethodInfo.toJson`
|
||||
methods.
|
||||
|
||||
## 0.0.2
|
||||
- Fixed an issue with streamNotify data type being too specific.
|
||||
|
||||
@@ -48,6 +48,26 @@ class ClientServiceInfo {
|
||||
ClientServiceInfo(this.name, [Map<String, ClientServiceMethodInfo>? methods])
|
||||
: methods = methods ?? {};
|
||||
|
||||
/// Deserializes a [json] object to create a [ClientServiceInfo] object.
|
||||
static ClientServiceInfo fromJson(Map<String, Object?> json) {
|
||||
if (json case {_kName: final String name, _kMethods: final List methods}) {
|
||||
return ClientServiceInfo(
|
||||
name,
|
||||
<String, ClientServiceMethodInfo>{
|
||||
for (final method in methods
|
||||
.cast<Map<String, Object?>>()
|
||||
.map(ClientServiceMethodInfo.fromJson))
|
||||
method.name: method
|
||||
},
|
||||
);
|
||||
}
|
||||
throw ArgumentError('Unexpected JSON format: $json');
|
||||
}
|
||||
|
||||
static const _kName = 'name';
|
||||
|
||||
static const _kMethods = 'methods';
|
||||
|
||||
/// The name of the service.
|
||||
///
|
||||
/// A client can register multiple services each with multiple methods.
|
||||
@@ -58,6 +78,12 @@ class ClientServiceInfo {
|
||||
|
||||
/// The service methods registered for this service.
|
||||
final Map<String, ClientServiceMethodInfo> methods;
|
||||
|
||||
/// Serializes this [ClientServiceInfo] object to JSON.
|
||||
Map<String, Object?> toJson() => {
|
||||
_kName: name,
|
||||
_kMethods: methods.values.map((m) => m.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Information about an individual method of a service provided by a
|
||||
@@ -65,6 +91,22 @@ class ClientServiceInfo {
|
||||
class ClientServiceMethodInfo {
|
||||
ClientServiceMethodInfo(this.name, [this.capabilities]);
|
||||
|
||||
/// Deserializes a [json] object to create a [ClientServiceMethodInfo] object.
|
||||
static ClientServiceMethodInfo fromJson(Map<String, Object?> json) {
|
||||
try {
|
||||
return ClientServiceMethodInfo(
|
||||
json[_kName] as String,
|
||||
json[_kCapabilities] as Map<String, Object?>?,
|
||||
);
|
||||
} catch (e) {
|
||||
throw ArgumentError('Unexpected JSON format: $json');
|
||||
}
|
||||
}
|
||||
|
||||
static const _kName = 'name';
|
||||
|
||||
static const _kCapabilities = 'capabilities';
|
||||
|
||||
/// The name of the method.
|
||||
///
|
||||
/// A client can register multiple methods for each service but can only use
|
||||
@@ -74,6 +116,12 @@ class ClientServiceMethodInfo {
|
||||
|
||||
/// Optional capabilities of this service method provided by the client.
|
||||
final Map<String, Object?>? capabilities;
|
||||
|
||||
/// Serializes this [ClientServiceMethodInfo] object to JSON.
|
||||
Map<String, Object?> toJson() => {
|
||||
_kName: name,
|
||||
if (capabilities != null) _kCapabilities: capabilities,
|
||||
};
|
||||
}
|
||||
|
||||
/// Used for keeping track and managing clients that are connected to a given
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: dart_service_protocol_shared
|
||||
description: A package that implements service extensions and stream managers.
|
||||
|
||||
version: 0.0.3-wip
|
||||
version: 0.0.3
|
||||
repository: https://github.com/dart-lang/sdk/tree/main/pkg/dart_service_protocol_shared
|
||||
|
||||
environment:
|
||||
|
||||
@@ -128,4 +128,82 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('ClientServiceInfo', () {
|
||||
test('toJson and fromJson', () {
|
||||
final serviceInfo = ClientServiceInfo(
|
||||
'testService',
|
||||
{
|
||||
'method1': ClientServiceMethodInfo('method1', {'capability': true}),
|
||||
'method2': ClientServiceMethodInfo('method2'),
|
||||
},
|
||||
);
|
||||
|
||||
final json = serviceInfo.toJson();
|
||||
final deserialized = ClientServiceInfo.fromJson(json);
|
||||
|
||||
expect(deserialized.name, 'testService');
|
||||
expect(deserialized.methods.length, 2);
|
||||
expect(deserialized.methods['method1']?.name, 'method1');
|
||||
expect(
|
||||
deserialized.methods['method1']?.capabilities,
|
||||
{'capability': true},
|
||||
);
|
||||
expect(deserialized.methods['method2']?.name, 'method2');
|
||||
expect(deserialized.methods['method2']?.capabilities, isNull);
|
||||
});
|
||||
|
||||
test('fromJson throws with invalid json', () {
|
||||
expect(
|
||||
() => ClientServiceInfo.fromJson({
|
||||
'name': 'testService',
|
||||
'methods': [
|
||||
{'bad': 'format'}
|
||||
],
|
||||
}),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
);
|
||||
expect(
|
||||
() => ClientServiceInfo.fromJson({
|
||||
'methods': [
|
||||
{'name': 'method1'}
|
||||
],
|
||||
}),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
reason: 'Missing "name" field in top-level map',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('ClientServiceMethodInfo', () {
|
||||
test('toJson and parse', () {
|
||||
final methodInfo =
|
||||
ClientServiceMethodInfo('testMethod', {'capability': 123});
|
||||
|
||||
final json = methodInfo.toJson();
|
||||
final deserialized = ClientServiceMethodInfo.fromJson(json);
|
||||
|
||||
expect(deserialized.name, 'testMethod');
|
||||
expect(deserialized.capabilities, {'capability': 123});
|
||||
});
|
||||
|
||||
test('toJson and parse without capabilities', () {
|
||||
final methodInfo = ClientServiceMethodInfo('testMethod');
|
||||
|
||||
final json = methodInfo.toJson();
|
||||
final deserialized = ClientServiceMethodInfo.fromJson(json);
|
||||
|
||||
expect(deserialized.name, 'testMethod');
|
||||
expect(deserialized.capabilities, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('ClientServiceMethodInfo throws with invalid json', () {
|
||||
expect(
|
||||
() => ClientServiceMethodInfo.fromJson({
|
||||
'bad': 'format',
|
||||
}),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
## 2.6.0
|
||||
## 3.0.0
|
||||
|
||||
- Add `ConnectedAppService` to store the connections to Dart and Flutter
|
||||
- Added `ConnectedAppService` to store the connections to Dart and Flutter
|
||||
applications that DTD is aware of.
|
||||
- Log exceptions from invalid `streamNotify` events.
|
||||
- Added `getRegisteredServices` API.
|
||||
- Added a new response type `RegisteredServicesResponse`.
|
||||
- **Breaking Change**: Changed the `serviceName` parameter for the
|
||||
`DartToolingDaemon.call` method to have type `String?` instead of `String.`
|
||||
- **Breaking Change**: When the `params` parameter for the
|
||||
`DartToolingDaemon.call` method is null, pass the null value along to the client
|
||||
peer request instead of sending an empty Map value.
|
||||
|
||||
## 2.5.1
|
||||
|
||||
|
||||
@@ -34,9 +34,13 @@ void main(List<String> args) async {
|
||||
clientB.onEvent('Service').listen((e) {
|
||||
switch (e.kind) {
|
||||
case 'ServiceRegistered':
|
||||
serviceRegisteredCompleted.complete();
|
||||
if (e.data['service'] == 'ExampleServer') {
|
||||
serviceRegisteredCompleted.complete();
|
||||
}
|
||||
case 'ServiceUnregistered':
|
||||
serviceUnregisteredCompleted.complete();
|
||||
if (e.data['service'] == 'ExampleServer') {
|
||||
serviceUnregisteredCompleted.complete();
|
||||
}
|
||||
}
|
||||
print(jsonEncode({'stream': e.stream, 'kind': e.kind, 'data': e.data}));
|
||||
});
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
/// Support for communicating with the Dart Tooling Daemon.
|
||||
library;
|
||||
|
||||
export 'src/connected_app_service.dart';
|
||||
export 'src/constants.dart';
|
||||
export 'src/dart_tooling_daemon.dart';
|
||||
export 'src/file_system/file_system_service.dart';
|
||||
export 'src/file_system/types.dart';
|
||||
export 'src/response_types.dart';
|
||||
export 'src/response_types/response_types.dart';
|
||||
export 'src/rpc_error_codes.dart';
|
||||
export 'src/services/connected_app_service.dart';
|
||||
export 'src/services/file_system_service.dart';
|
||||
|
||||
@@ -117,6 +117,17 @@ class DartToolingDaemon {
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a structured response with all the currently registered services
|
||||
/// available on this DTD instance.
|
||||
Future<RegisteredServicesResponse> getRegisteredServices() async {
|
||||
final json = await _clientPeer.sendRequest(
|
||||
'getRegisteredServices',
|
||||
) as Map<String, Object?>;
|
||||
|
||||
final dtdResponse = _dtdResponseFromJson(json);
|
||||
return RegisteredServicesResponse.fromDTDResponse(dtdResponse);
|
||||
}
|
||||
|
||||
/// Subscribes this client to events posted on [streamId].
|
||||
///
|
||||
/// Once called, the Dart Tooling Daemon will then send any events on the
|
||||
@@ -185,26 +196,35 @@ class DartToolingDaemon {
|
||||
}
|
||||
|
||||
/// Invokes the service method registered with the name
|
||||
/// `[serviceName].[methodName]`.
|
||||
/// `[serviceName].[methodName]`, or with `[methodName]` when [serviceName] is
|
||||
/// null.
|
||||
///
|
||||
/// [serviceName] may be null if the service method is a first party service
|
||||
/// method registered by DTD or by an internal service.
|
||||
///
|
||||
/// If provided, [params] will be sent as the set of parameters used when
|
||||
/// invoking the service.
|
||||
///
|
||||
/// If `[serviceName].[methodName]` is not a registered service method, an
|
||||
/// [RpcException] will be thrown with [RpcErrorCodes.kMethodNotFound].
|
||||
/// If `[serviceName].[methodName]`, or `[methodName]` when [serviceName] is
|
||||
/// null, is not a registered service method, an [RpcException] will be thrown
|
||||
/// with [RpcErrorCodes.kMethodNotFound].
|
||||
///
|
||||
/// If the parameters included in [params] are invalid, an [RpcException] will
|
||||
/// be thrown with [RpcErrorCodes.kInvalidParams].
|
||||
Future<DTDResponse> call(
|
||||
String serviceName,
|
||||
String? serviceName,
|
||||
String methodName, {
|
||||
Map<String, Object?>? params,
|
||||
}) async {
|
||||
final combinedName = [serviceName, methodName].nonNulls.join('.');
|
||||
final json = await _clientPeer.sendRequest(
|
||||
'$serviceName.$methodName',
|
||||
params ?? <String, Object?>{},
|
||||
combinedName,
|
||||
params,
|
||||
) as Map<String, Object?>;
|
||||
return _dtdResponseFromJson(json);
|
||||
}
|
||||
|
||||
DTDResponse _dtdResponseFromJson(Map<String, Object?> json) {
|
||||
final type = json['type'] as String?;
|
||||
if (type == null) {
|
||||
throw DartToolingDaemonConnectionException.callResponseMissingType(json);
|
||||
|
||||
+1
-3
@@ -2,9 +2,7 @@
|
||||
// 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 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
|
||||
|
||||
import '../../dtd.dart';
|
||||
part of 'response_types.dart';
|
||||
|
||||
/// A list or [uris] on the system where the Dart Tooling Daemon is running.
|
||||
class UriList {
|
||||
+81
-1
@@ -2,9 +2,12 @@
|
||||
// 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 'package:dart_service_protocol_shared/dart_service_protocol_shared.dart';
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
|
||||
|
||||
import 'dart_tooling_daemon.dart';
|
||||
import '../dart_tooling_daemon.dart';
|
||||
|
||||
part '_file_system.dart';
|
||||
|
||||
/// A DTD response that indicates success.
|
||||
class Success extends _SuccessResponse<Null> {
|
||||
@@ -107,3 +110,80 @@ abstract class _SuccessResponse<T> {
|
||||
@override
|
||||
String toString() => '[$type value: $value]';
|
||||
}
|
||||
|
||||
/// A DTD response that contains information about all the registered services
|
||||
/// available on the Dart Tooling Daemon, including services provided by DTD
|
||||
/// itself as well as services registered by DTD clients.
|
||||
class RegisteredServicesResponse {
|
||||
const RegisteredServicesResponse({
|
||||
required this.dtdServices,
|
||||
required this.clientServices,
|
||||
});
|
||||
|
||||
factory RegisteredServicesResponse.fromDTDResponse(DTDResponse response) {
|
||||
if (response.result[_kType] != type) {
|
||||
throw json_rpc.RpcException.invalidParams(
|
||||
'Expected $_kType param to be $type, got: ${response.result[_kType]}',
|
||||
);
|
||||
}
|
||||
return RegisteredServicesResponse._fromDTDResponse(response);
|
||||
}
|
||||
|
||||
RegisteredServicesResponse._fromDTDResponse(DTDResponse response)
|
||||
: dtdServices = List<String>.from(
|
||||
(response.result[_kDtdServices] as List).cast<String>(),
|
||||
),
|
||||
clientServices = List<Map<String, Object?>>.from(
|
||||
(response.result[_kClientServices] as List)
|
||||
.cast<Map<String, Object?>>(),
|
||||
).map(ClientServiceInfo.fromJson).toList();
|
||||
|
||||
/// The key for the type parameter.
|
||||
static const String _kType = 'type';
|
||||
|
||||
/// The key for the DTD services parameter.
|
||||
static const String _kDtdServices = 'dtdServices';
|
||||
|
||||
/// The key for the client services parameter.
|
||||
static const String _kClientServices = 'clientServices';
|
||||
|
||||
/// A list of DTD services.
|
||||
final List<String> dtdServices;
|
||||
|
||||
/// A list of DTD client services.
|
||||
final List<ClientServiceInfo> clientServices;
|
||||
|
||||
static String get type => 'RegisteredServicesResponse';
|
||||
|
||||
Map<String, Object?> toJson() => <String, Object?>{
|
||||
_kType: type,
|
||||
_kDtdServices: dtdServices,
|
||||
_kClientServices:
|
||||
clientServices.map((service) => service.toJson()).toList(),
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() => '['
|
||||
'$type '
|
||||
'dtdServices: ${dtdServices.toString()}, '
|
||||
'clientServices: '
|
||||
'${clientServices.map((service) => service.display).toList().toString()}'
|
||||
']';
|
||||
}
|
||||
|
||||
extension on ClientServiceInfo {
|
||||
String get display {
|
||||
final sb = StringBuffer()
|
||||
..write('$name (')
|
||||
..write(
|
||||
methods.values.map((method) {
|
||||
final capabilities = method.capabilities != null
|
||||
? ' ${method.capabilities.toString()}'
|
||||
: '';
|
||||
return '${method.name}$capabilities';
|
||||
}).join(', '),
|
||||
)
|
||||
..write(')');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -4,10 +4,10 @@
|
||||
|
||||
import 'package:json_rpc_2/json_rpc_2.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'dart_tooling_daemon.dart';
|
||||
import 'response_types.dart';
|
||||
import 'rpc_error_codes.dart';
|
||||
import '../constants.dart';
|
||||
import '../dart_tooling_daemon.dart';
|
||||
import '../response_types/response_types.dart';
|
||||
import '../rpc_error_codes.dart';
|
||||
|
||||
/// Extension methods on the [DartToolingDaemon] that call the ConnectedApps
|
||||
/// service.
|
||||
+1
-1
@@ -9,8 +9,8 @@ import 'package:json_rpc_2/json_rpc_2.dart' show RpcException;
|
||||
import '../constants.dart';
|
||||
import '../dart_tooling_daemon.dart';
|
||||
|
||||
import '../response_types/response_types.dart';
|
||||
import '../rpc_error_codes.dart' show RpcErrorCodes;
|
||||
import 'types.dart';
|
||||
|
||||
extension FileSystemService on DartToolingDaemon {
|
||||
/// Reads the file at [uri] from disk in the environment where the Dart
|
||||
+3
-3
@@ -4,9 +4,9 @@
|
||||
|
||||
import 'package:unified_analytics/unified_analytics.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'dart_tooling_daemon.dart';
|
||||
import 'response_types.dart';
|
||||
import '../constants.dart';
|
||||
import '../dart_tooling_daemon.dart';
|
||||
import '../response_types/response_types.dart';
|
||||
|
||||
/// Extension methods on the [DartToolingDaemon] that call the UnifiedAnalytics
|
||||
/// service.
|
||||
@@ -1,5 +1,5 @@
|
||||
name: dtd
|
||||
version: 2.6.0
|
||||
version: 3.0.0
|
||||
description: A package for communicating with the Dart Tooling Daemon.
|
||||
repository: https://github.com/dart-lang/sdk/tree/main/pkg/dtd
|
||||
|
||||
@@ -9,6 +9,7 @@ environment:
|
||||
resolution: workspace
|
||||
|
||||
dependencies:
|
||||
dart_service_protocol_shared: ^0.0.3
|
||||
json_rpc_2: '>=3.0.2 <5.0.0'
|
||||
stream_channel: ^2.1.2
|
||||
unified_analytics: '>=7.0.0 <9.0.0'
|
||||
|
||||
@@ -98,6 +98,106 @@ void main() {
|
||||
{'type': 'test', 'data': data, 'params': params},
|
||||
);
|
||||
});
|
||||
|
||||
test('getRegisteredServices', () async {
|
||||
await clientA.registerService(
|
||||
'TestService',
|
||||
'foo',
|
||||
(Parameters params) async {
|
||||
return {
|
||||
'type': 'test',
|
||||
'data': data,
|
||||
'params': params.asMap,
|
||||
};
|
||||
},
|
||||
);
|
||||
await clientA.registerService(
|
||||
'TestService',
|
||||
'bar',
|
||||
(Parameters params) async {
|
||||
return {
|
||||
'type': 'test',
|
||||
'data': data,
|
||||
'params': params.asMap,
|
||||
};
|
||||
},
|
||||
capabilities: {
|
||||
'language': 'french',
|
||||
},
|
||||
);
|
||||
await clientB.registerService(
|
||||
'OtherService',
|
||||
'foo',
|
||||
(Parameters params) async {
|
||||
return {
|
||||
'type': 'other',
|
||||
'data': data,
|
||||
'params': params.asMap,
|
||||
};
|
||||
},
|
||||
capabilities: {
|
||||
'skills': 'baking',
|
||||
},
|
||||
);
|
||||
|
||||
final response = await clientA.getRegisteredServices();
|
||||
expect(
|
||||
response.toJson(),
|
||||
{
|
||||
'type': 'RegisteredServicesResponse',
|
||||
'dtdServices': [
|
||||
'streamListen',
|
||||
'streamCancel',
|
||||
'postEvent',
|
||||
'registerService',
|
||||
'getRegisteredServices',
|
||||
'ConnectedApp.registerVmService',
|
||||
'ConnectedApp.unregisterVmService',
|
||||
'ConnectedApp.getVmServiceUris',
|
||||
'FileSystem.readFileAsString',
|
||||
'FileSystem.writeFileAsString',
|
||||
'FileSystem.listDirectoryContents',
|
||||
'FileSystem.setIDEWorkspaceRoots',
|
||||
'FileSystem.getIDEWorkspaceRoots',
|
||||
'FileSystem.getProjectRoots',
|
||||
'UnifiedAnalytics.getConsentMessage',
|
||||
'UnifiedAnalytics.shouldShowMessage',
|
||||
'UnifiedAnalytics.clientShowedMessage',
|
||||
'UnifiedAnalytics.telemetryEnabled',
|
||||
'UnifiedAnalytics.setTelemetry',
|
||||
'UnifiedAnalytics.send',
|
||||
'UnifiedAnalytics.listFakeAnalyticsSentEvents',
|
||||
],
|
||||
'clientServices': [
|
||||
{
|
||||
'name': 'TestService',
|
||||
'methods': [
|
||||
{
|
||||
'name': 'foo',
|
||||
},
|
||||
{
|
||||
'name': 'bar',
|
||||
'capabilities': <String, Object?>{
|
||||
'language': 'french',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': 'OtherService',
|
||||
'methods': [
|
||||
{
|
||||
'name': 'foo',
|
||||
'capabilities': <String, Object?>{
|
||||
'skills': 'baking',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,12 +212,6 @@ void main() {
|
||||
'timestamp': 1,
|
||||
},
|
||||
};
|
||||
const exampleCallToReceive = {
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'foo.bar',
|
||||
'id': 0,
|
||||
'params': <String, Object?>{},
|
||||
};
|
||||
|
||||
final clientToServer = StreamController<String>();
|
||||
final serverToClient = StreamController<String>();
|
||||
@@ -129,12 +223,57 @@ void main() {
|
||||
final clientReceivedEvent = await client.onEvent('testStream').first;
|
||||
expect(clientReceivedEvent.data['foo'], 'bar');
|
||||
|
||||
// Send a request and ensure it comes on the stream.
|
||||
// Discard "Connection closed with pending 'foo.bar'"" error because the
|
||||
// test doesn't respond to it.
|
||||
// Send requests and ensure they comes over the stream.
|
||||
// Discard "Connection closed with pending 'foo.bar'" errors because the
|
||||
// test doesn't respond to these.
|
||||
|
||||
// Call service with no parameters.
|
||||
unawaited(
|
||||
client.call('foo', 'bar').onError((_, __) => DTDResponse('', '', {})),
|
||||
);
|
||||
expect(jsonDecode(await clientToServer.stream.first), exampleCallToReceive);
|
||||
// Call service with parameters.
|
||||
unawaited(
|
||||
client.call('foo', 'bar', params: {'test': 'test'}).onError(
|
||||
(_, __) => DTDResponse('', '', {}),
|
||||
),
|
||||
);
|
||||
// Call method with null service.
|
||||
unawaited(
|
||||
client.call(null, 'bar').onError((_, __) => DTDResponse('', '', {})),
|
||||
);
|
||||
|
||||
const expectedRequests = 3;
|
||||
final requestsReceived = <String>[];
|
||||
final allRequestsReceived = Completer<void>();
|
||||
|
||||
StreamSubscription<String>? sub;
|
||||
sub = clientToServer.stream.asBroadcastStream().listen((request) {
|
||||
requestsReceived.add(request);
|
||||
if (requestsReceived.length == expectedRequests) {
|
||||
sub!.cancel();
|
||||
allRequestsReceived.complete();
|
||||
}
|
||||
});
|
||||
addTearDown(() => sub?.cancel());
|
||||
await allRequestsReceived.future;
|
||||
|
||||
expect(
|
||||
jsonDecode(requestsReceived[0]),
|
||||
{'jsonrpc': '2.0', 'method': 'foo.bar', 'id': 0},
|
||||
);
|
||||
expect(
|
||||
jsonDecode(requestsReceived[1]),
|
||||
{
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'foo.bar',
|
||||
'id': 1,
|
||||
'params': <String, Object?>{'test': 'test'},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
jsonDecode(requestsReceived[2]),
|
||||
{'jsonrpc': '2.0', 'method': 'bar', 'id': 2},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2025, 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 'package:dart_service_protocol_shared/dart_service_protocol_shared.dart';
|
||||
import 'package:dtd/dtd.dart';
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
// TODO(kenz): add test coverage for other response types.
|
||||
group('RegisteredServicesResponse', () {
|
||||
test('fromDTDResponse and toJson', () {
|
||||
final response = DTDResponse('id', 'method', {
|
||||
'type': 'RegisteredServicesResponse',
|
||||
'dtdServices': ['service1', 'service2'],
|
||||
'clientServices': [
|
||||
ClientServiceInfo('client1', {
|
||||
'method1': ClientServiceMethodInfo('method1', {'cap1': true}),
|
||||
}).toJson(),
|
||||
],
|
||||
});
|
||||
final servicesResponse =
|
||||
RegisteredServicesResponse.fromDTDResponse(response);
|
||||
expect(
|
||||
RegisteredServicesResponse.type,
|
||||
equals('RegisteredServicesResponse'),
|
||||
);
|
||||
expect(servicesResponse.dtdServices, equals(['service1', 'service2']));
|
||||
expect(servicesResponse.clientServices.length, equals(1));
|
||||
expect(servicesResponse.clientServices[0].name, equals('client1'));
|
||||
|
||||
expect(
|
||||
servicesResponse.toJson(),
|
||||
equals(
|
||||
{
|
||||
'type': 'RegisteredServicesResponse',
|
||||
'dtdServices': ['service1', 'service2'],
|
||||
'clientServices': [
|
||||
{
|
||||
'name': 'client1',
|
||||
'methods': [
|
||||
{
|
||||
'name': 'method1',
|
||||
'capabilities': {'cap1': true},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('fromDTDResponse throws on invalid type', () {
|
||||
final response = DTDResponse('id', 'method', {
|
||||
'type': 'InvalidType',
|
||||
'dtdServices': ['service1'],
|
||||
'clientServices': [],
|
||||
});
|
||||
expect(
|
||||
() => RegisteredServicesResponse.fromDTDResponse(response),
|
||||
throwsA(isA<json_rpc.RpcException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('toString formats correctly', () {
|
||||
final response = RegisteredServicesResponse(
|
||||
dtdServices: ['service1'],
|
||||
clientServices: [
|
||||
ClientServiceInfo('client1', {
|
||||
'method1': ClientServiceMethodInfo('method1', {'cap1': true}),
|
||||
}),
|
||||
],
|
||||
);
|
||||
expect(
|
||||
response.toString(),
|
||||
equals(
|
||||
'[RegisteredServicesResponse dtdServices: [service1], '
|
||||
'clientServices: [client1 (method1 {cap1: true})]]',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:dtd/dtd.dart';
|
||||
import 'package:dtd/src/unified_analytics_service.dart';
|
||||
import 'package:dtd/src/services/unified_analytics_service.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:unified_analytics/unified_analytics.dart';
|
||||
|
||||
|
||||
+12
-1
@@ -21,10 +21,21 @@ To run the tooling daemon compiled with the Dart SDK:
|
||||
2. run `dart tooling-daemon`
|
||||
> :info The dart binary should be the one you just built in step 1.
|
||||
|
||||
### Testing changes
|
||||
### Testing changes locally
|
||||
|
||||
To quickly test changes to the tooling daemon, start it by running:
|
||||
|
||||
```bash
|
||||
dart run bin/dtd.dart
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
To run the tests under the `test/` directory, run `dart test test/`.
|
||||
|
||||
However, if you are testing changes that span `pkg/dtd` and `pkg/dtd_impl`,
|
||||
you'll need to build the Dart SDK, and then use the built Dart executable to run
|
||||
the test.
|
||||
|
||||
1. Build the Dart SDK: `./tools/build.py create_platform_sdk`
|
||||
2. Run the test: `xcodebuild/ReleaseARM64/dart-sdk/bin/dart test test/`
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:sse/server/sse_handler.dart';
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:dtd/dtd.dart' show RpcErrorCodes;
|
||||
import 'package:dtd/dtd.dart' show RpcErrorCodes, RegisteredServicesResponse;
|
||||
|
||||
import 'constants.dart';
|
||||
import 'dart_tooling_daemon.dart';
|
||||
@@ -67,7 +67,7 @@ class DTDClient extends Client {
|
||||
return;
|
||||
}
|
||||
|
||||
return await _clientPeer.sendRequest(method, parameters.asMap);
|
||||
return await _clientPeer.sendRequest(method, parameters.value);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -82,12 +82,22 @@ class DTDClient extends Client {
|
||||
(_) => dtd.streamManager.onClientDisconnect(this),
|
||||
);
|
||||
|
||||
/// The set of RPC methods registered by DTD itself.
|
||||
///
|
||||
/// This will be a combination of first party DTD methods and methods
|
||||
/// registered by internal services.
|
||||
final _dtdRpcMethods = <String>{};
|
||||
|
||||
/// Registers handlers for the Dart Tooling Daemon JSON RPC method endpoints.
|
||||
void _registerJsonRpcMethods() {
|
||||
_clientPeer.registerMethod('streamListen', _streamListen);
|
||||
_clientPeer.registerMethod('streamCancel', _streamCancel);
|
||||
_clientPeer.registerMethod('postEvent', _postEvent);
|
||||
_clientPeer.registerMethod('registerService', _registerService);
|
||||
_registerDtdMethod('streamListen', _streamListen);
|
||||
_registerDtdMethod('streamCancel', _streamCancel);
|
||||
_registerDtdMethod('postEvent', _postEvent);
|
||||
_registerDtdMethod('registerService', _registerService);
|
||||
_registerDtdMethod(
|
||||
'getRegisteredServices',
|
||||
_getRegisteredServices,
|
||||
);
|
||||
|
||||
// Handle service extension invocations.
|
||||
_clientPeer.registerFallback(_fallback);
|
||||
@@ -125,14 +135,36 @@ class DTDClient extends Client {
|
||||
DTDStreamManager.servicesStreamId,
|
||||
DTDStreamManager.serviceRegisteredId,
|
||||
_buildServiceRegisteredData(
|
||||
service.name,
|
||||
method.name,
|
||||
method.capabilities,
|
||||
service: service.name,
|
||||
method: method.name,
|
||||
capabilities: method.capabilities,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final method in _dtdRpcMethods) {
|
||||
// If the DTD service method has the form 'service.method', split up the
|
||||
// two values. Otherwise, leave the service null and use the entire name
|
||||
// as the method.
|
||||
String? serviceName;
|
||||
String methodName;
|
||||
final parts = method.split('.');
|
||||
if (parts.length == 2) {
|
||||
serviceName = parts[0];
|
||||
}
|
||||
methodName = parts.last;
|
||||
|
||||
_streamNotifyHelper(
|
||||
DTDStreamManager.servicesStreamId,
|
||||
DTDStreamManager.serviceRegisteredId,
|
||||
_buildServiceRegisteredData(
|
||||
service: serviceName,
|
||||
method: methodName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return RPCResponses.success;
|
||||
@@ -259,11 +291,31 @@ class DTDClient extends Client {
|
||||
dtd.streamManager.postEventHelper(
|
||||
DTDStreamManager.servicesStreamId,
|
||||
DTDStreamManager.serviceRegisteredId,
|
||||
_buildServiceRegisteredData(serviceName, methodName, capabilities),
|
||||
_buildServiceRegisteredData(
|
||||
service: serviceName,
|
||||
method: methodName,
|
||||
capabilities: capabilities,
|
||||
),
|
||||
);
|
||||
return RPCResponses.success;
|
||||
}
|
||||
|
||||
/// Returns a structured response with all the currently registered services
|
||||
/// available on this DTD instance.
|
||||
Map<String, Object?> _getRegisteredServices() {
|
||||
final clientServices =
|
||||
dtd.clientManager.clients.map((client) => client.services);
|
||||
final combinedClientServices = {
|
||||
// This will not create collisions because [_registerService] ensures
|
||||
// the uniqueness of service methods across clients.
|
||||
for (final servicesForClient in clientServices) ...servicesForClient,
|
||||
};
|
||||
return RegisteredServicesResponse(
|
||||
dtdServices: _dtdRpcMethods.toList(),
|
||||
clientServices: combinedClientServices.values.toList(),
|
||||
).toJson();
|
||||
}
|
||||
|
||||
/// Cleans up when this client is disconnecting, before it is removed from the
|
||||
/// client manager.
|
||||
void onClientDisconnect() {
|
||||
@@ -279,13 +331,18 @@ class DTDClient extends Client {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object?> _buildServiceRegisteredData(
|
||||
String service,
|
||||
String method,
|
||||
/// Builds a structured object describing a service method that is being
|
||||
/// registered on DTD.
|
||||
///
|
||||
/// [service] may be null if this service method is a first party service
|
||||
/// method registered by DTD or by an internal service.
|
||||
Map<String, Object?> _buildServiceRegisteredData({
|
||||
required String? service,
|
||||
required String method,
|
||||
Map<String, Object?>? capabilities,
|
||||
) {
|
||||
}) {
|
||||
return {
|
||||
'service': service,
|
||||
if (service != null) 'service': service,
|
||||
'method': method,
|
||||
if (capabilities != null) 'capabilities': capabilities,
|
||||
};
|
||||
@@ -356,6 +413,13 @@ class DTDClient extends Client {
|
||||
void Function(json_rpc.Parameters parameters) callback,
|
||||
) {
|
||||
final combinedName = '$service.$method';
|
||||
_clientPeer.registerMethod(combinedName, callback);
|
||||
_registerDtdMethod(combinedName, callback);
|
||||
}
|
||||
|
||||
/// A helper method for registering a service method on DTD and adding the
|
||||
/// service method to the [_dtdRpcMethods] set.
|
||||
void _registerDtdMethod(String method, Function callback) {
|
||||
_dtdRpcMethods.add(method);
|
||||
_clientPeer.registerMethod(method, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,7 +457,9 @@ void main() {
|
||||
);
|
||||
|
||||
// Expect we had a service registered event.
|
||||
final event = await serviceStream.stream.first;
|
||||
final event = await serviceStream.stream.firstWhere(
|
||||
(event) => (event['eventData'] as Map?)?['service'] == 'foo1',
|
||||
);
|
||||
expect(event['streamId'], DTDStreamManager.servicesStreamId);
|
||||
expect(event['eventKind'], DTDStreamManager.serviceRegisteredId);
|
||||
expect(
|
||||
@@ -535,9 +537,12 @@ void main() {
|
||||
|
||||
// Expect we had a service unregistered event (after the registered
|
||||
// event).
|
||||
final event = await serviceStream.stream.skip(1).first;
|
||||
final event = await serviceStream.stream.skip(1).firstWhere((event) {
|
||||
return event['eventKind'] ==
|
||||
DTDStreamManager.serviceUnregisteredId &&
|
||||
(event['eventData'] as Map?)?['service'] == 'foo1';
|
||||
});
|
||||
expect(event['streamId'], DTDStreamManager.servicesStreamId);
|
||||
expect(event['eventKind'], DTDStreamManager.serviceUnregisteredId);
|
||||
expect(
|
||||
event['eventData'],
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user