| // Copyright (c) 2017, 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_client/listener/server_listener.dart'; |
| import 'package:analysis_server_client/protocol.dart'; |
| import 'package:analysis_server_client/server.dart'; |
| import 'package:test/test.dart'; |
| |
| void main() { |
| late _ServerListener listener; |
| late MockProcess process; |
| late Server server; |
| |
| setUp(() async { |
| process = MockProcess(); |
| listener = _ServerListener(); |
| server = Server(process: process, listener: listener); |
| }); |
| |
| group('listenToOutput', () { |
| test('good', () async { |
| process.stdout = _goodMessage(); |
| process.stderr = _noMessage(); |
| |
| final future = server.send('blahMethod', null); |
| server.listenToOutput(); |
| |
| final response = (await future)!; |
| expect(response['foo'], 'bar'); |
| }); |
| |
| test('error', () async { |
| process.stdout = _badMessage(); |
| process.stderr = _noMessage(); |
| |
| final future = server.send('blahMethod', null); |
| // ignore: unawaited_futures |
| future.catchError((e) { |
| expect(e, const TypeMatcher<RequestError>()); |
| final error = e as RequestError; |
| expect(error.code, RequestErrorCode.UNKNOWN_REQUEST); |
| expect(error.message, 'something went wrong'); |
| expect(error.stackTrace, 'some long stack trace'); |
| return <String, Object?>{}; |
| }); |
| server.listenToOutput(); |
| }); |
| |
| test('event', () async { |
| process.stdout = _eventMessage(); |
| process.stderr = _noMessage(); |
| |
| final completer = Completer(); |
| void eventHandler(Notification notification) { |
| expect(notification.event, 'fooEvent'); |
| var params = notification.params!; |
| expect(params.length, 2); |
| expect(params['foo'] as String, 'bar'); |
| expect(params['baz'] as String, 'bang'); |
| completer.complete(); |
| } |
| |
| // ignore: unawaited_futures |
| server.send('blahMethod', null); |
| server.listenToOutput(notificationProcessor: eventHandler); |
| await completer.future; |
| }); |
| |
| test('unexpected message', () async { |
| // No 'id', so not a response. |
| // No 'event', so not a notification. |
| process.stdout = Stream.value( |
| utf8.encoder.convert(json.encode({'foo': 'bar'})), |
| ); |
| process.stderr = _noMessage(); |
| |
| server.listenToOutput(); |
| |
| // Must happen for the test to pass. |
| await listener.unexpectedMessageController.stream.first; |
| }); |
| |
| test('unexpected notification format', () async { |
| process.stdout = Stream.value( |
| utf8.encoder.convert(json.encode({'event': 'foo', 'noParams': '42'})), |
| ); |
| process.stderr = _noMessage(); |
| |
| server.listenToOutput(); |
| |
| // Must happen for the test to pass. |
| await listener.unexpectedNotificationFormatCompleter.stream.first; |
| }); |
| |
| test('unexpected response', () async { |
| // We have no asked anything, but got a response. |
| process.stdout = Stream.value( |
| utf8.encoder.convert(json.encode({'id': '0'})), |
| ); |
| process.stderr = _noMessage(); |
| |
| server.listenToOutput(); |
| |
| // Must happen for the test to pass. |
| await listener.unexpectedResponseCompleter.stream.first; |
| }); |
| |
| test('unexpected response format', () async { |
| // We expect that the first request has id `0`. |
| // The response is invalid - the "result" field is not an object. |
| process.stdout = Stream.value( |
| utf8.encoder.convert(json.encode({'id': '0', 'result': '42'})), |
| ); |
| process.stderr = _noMessage(); |
| |
| // ignore: unawaited_futures |
| server.send('blahMethod', null); |
| server.listenToOutput(); |
| |
| // Must happen for the test to pass. |
| await listener.unexpectedResponseFormatCompleter.stream.first; |
| }); |
| }); |
| |
| group('stop', () { |
| test('ok', () async { |
| final mockout = StreamController<List<int>>(); |
| process.stdout = mockout.stream; |
| process.stderr = _noMessage(); |
| // ignore: unawaited_futures |
| process.mockin.controller.stream.first.then((_) { |
| var encoded = json.encode({'id': '0'}); |
| mockout.add(utf8.encoder.convert('$encoded\n')); |
| }); |
| process.exitCode = Future.value(0); |
| |
| server.listenToOutput(); |
| await server.stop(timeLimit: const Duration(milliseconds: 1)); |
| expect(process.killed, isFalse); |
| }); |
| test('stopped', () async { |
| final mockout = StreamController<List<int>>(); |
| process.stdout = mockout.stream; |
| process.stderr = _noMessage(); |
| process.exitCode = Future.value(0); |
| |
| server.listenToOutput(); |
| await server.stop(timeLimit: const Duration(milliseconds: 1)); |
| expect(process.killed, isFalse); |
| }); |
| test('kill', () async { |
| final mockout = StreamController<List<int>>(); |
| process.stdout = mockout.stream; |
| process.stderr = _noMessage(); |
| process.exitCode = Future.delayed(const Duration(seconds: 1), () => 0); |
| |
| server.listenToOutput(); |
| await server.stop(timeLimit: const Duration(milliseconds: 10)); |
| expect(process.killed, isTrue); |
| }); |
| }); |
| } |
| |
| final _badErrorMessage = { |
| 'code': 'UNKNOWN_REQUEST', |
| 'message': 'something went wrong', |
| 'stackTrace': 'some long stack trace' |
| }; |
| |
| Stream<List<int>> _badMessage() async* { |
| yield utf8.encoder.convert('Observatory listening on foo bar\n'); |
| final sampleJson = { |
| 'id': '0', |
| 'error': _badErrorMessage, |
| }; |
| yield utf8.encoder.convert(json.encode(sampleJson)); |
| } |
| |
| Stream<List<int>> _eventMessage() async* { |
| yield utf8.encoder.convert('Observatory listening on foo bar\n'); |
| final sampleJson = { |
| 'event': 'fooEvent', |
| 'params': {'foo': 'bar', 'baz': 'bang'} |
| }; |
| yield utf8.encoder.convert(json.encode(sampleJson)); |
| } |
| |
| Stream<List<int>> _goodMessage() async* { |
| yield utf8.encoder.convert('Observatory listening on foo bar\n'); |
| final sampleJson = { |
| 'id': '0', |
| 'result': {'foo': 'bar'} |
| }; |
| yield utf8.encoder.convert(json.encode(sampleJson)); |
| } |
| |
| Stream<List<int>> _noMessage() async* { |
| yield utf8.encoder.convert(''); |
| } |
| |
| class MockProcess implements Process { |
| MockStdin mockin = MockStdin(); |
| |
| bool killed = false; |
| |
| @override |
| late Stream<List<int>> stderr; |
| |
| @override |
| late Stream<List<int>> stdout; |
| |
| @override |
| late Future<int> exitCode; |
| |
| @override |
| IOSink get stdin => mockin; |
| |
| @override |
| bool kill([ProcessSignal signal = ProcessSignal.sigterm]) { |
| var wasKilled = killed; |
| killed = true; |
| return !wasKilled; |
| } |
| |
| @override |
| dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); |
| } |
| |
| class MockStdin implements IOSink { |
| final controller = StreamController<String>(); |
| |
| @override |
| void add(List<int> data) { |
| controller.add(utf8.decode(data)); |
| } |
| |
| @override |
| dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); |
| } |
| |
| class _ServerListener with ServerListener { |
| final unexpectedMessageController = StreamController<Object>(); |
| final unexpectedNotificationFormatCompleter = StreamController<Object>(); |
| final unexpectedResponseCompleter = StreamController<Object>(); |
| final unexpectedResponseFormatCompleter = StreamController<Object>(); |
| |
| @override |
| void log(String prefix, String details) {} |
| |
| @override |
| void unexpectedMessage(Map<String, Object?> message) { |
| unexpectedMessageController.add(message); |
| } |
| |
| @override |
| void unexpectedNotificationFormat(Map<String, Object?> message) { |
| unexpectedNotificationFormatCompleter.add(message); |
| } |
| |
| @override |
| void unexpectedResponse(Map<String, Object?> message, Object id) { |
| unexpectedResponseCompleter.add(message); |
| } |
| |
| @override |
| void unexpectedResponseFormat(Map<String, Object?> message) { |
| unexpectedResponseFormatCompleter.add(message); |
| } |
| } |