| // Copyright (c) 2020, 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 'package:dds/dds.dart'; |
| import 'package:dds/src/dds_impl.dart'; |
| import 'package:dds/src/rpc_error_codes.dart'; |
| import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc; |
| import 'package:pedantic/pedantic.dart'; |
| import 'package:test/test.dart'; |
| import 'package:web_socket_channel/web_socket_channel.dart'; |
| |
| import 'common/fakes.dart'; |
| |
| class StreamCancelDisconnectPeer extends FakePeer { |
| @override |
| Future<dynamic> sendRequest(String method, [args]) async { |
| final completer = Completer<dynamic>(); |
| switch (method) { |
| case 'streamCancel': |
| completer.completeError( |
| StateError('The client closed with pending request "streamCancel".'), |
| ); |
| // Notify listeners that this client is closed. |
| doneCompleter.complete(); |
| break; |
| case 'foo': |
| completer.completeError( |
| StateError('The client closed with pending request "foo".'), |
| ); |
| doneCompleter.completeError( |
| StateError('The client closed with pending request "foo".'), |
| ); |
| break; |
| default: |
| completer.complete(await super.sendRequest(method, args)); |
| } |
| return completer.future; |
| } |
| } |
| |
| void main() { |
| webSocketBuilder = (Uri _) => FakeWebSocketChannel(); |
| peerBuilder = |
| (WebSocketChannel _, dynamic __) async => StreamCancelDisconnectPeer(); |
| |
| test('StateError handled by _StreamManager.clientDisconnect', () async { |
| final dds = await DartDevelopmentService.startDartDevelopmentService( |
| Uri(scheme: 'http')); |
| final ws = await WebSocketChannel.connect(dds.uri!.replace(scheme: 'ws')); |
| |
| // Create a VM service client that connects to DDS. |
| final client = json_rpc.Client(ws.cast<String>()); |
| unawaited(client.listen()); |
| |
| // Listen to a non-core DDS stream so that DDS will cancel it once the |
| // client disconnects. |
| await client.sendRequest('streamListen', { |
| 'streamId': 'Service', |
| }); |
| |
| // Closing the client should result in DDS cleaning up stream subscriptions |
| // with no more clients subscribed to them. This will result in |
| // streamCancel being invoked, which StreamCancelDisconnectPeer overrides |
| // to act as if the VM service has shutdown with the request in flight |
| // which would result in a StateError being thrown by sendRequest. This |
| // test ensures that this exception is handled and doesn't escape outside |
| // of DDS. |
| await client.close(); |
| await dds.done; |
| }); |
| |
| test('StateError handled by _DartDevelopmentServiceClient request forwarder', |
| () async { |
| final dds = await DartDevelopmentService.startDartDevelopmentService( |
| Uri(scheme: 'http')); |
| final ws = await WebSocketChannel.connect(dds.uri!.replace(scheme: 'ws')); |
| |
| // Create a VM service client that connects to DDS. |
| final client = json_rpc.Client(ws.cast<String>()); |
| unawaited(client.listen()); |
| |
| bool caught = false; |
| |
| // Make a request that causes the VM service peer to close in the middle of |
| // handling a request. This is meant to mimic a device being disconnected |
| // unexpectedly. |
| try { |
| await client.sendRequest('foo'); |
| } on json_rpc.RpcException catch (e) { |
| // This RPC exception is expected. This test is ensuring that DDS exits |
| // gracefully even if the VM service disappears. |
| expect(e.code, RpcErrorCodes.kServiceDisappeared); |
| caught = true; |
| } |
| |
| expect(caught, true); |
| |
| // DDS should shutdown if the VM service peer disconnects. |
| await dds.done; |
| }); |
| } |