// 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()); 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 {}; }); 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.encode(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.encode(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.encode(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.encode(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>(); 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.encode('$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>(); 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>(); 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> _badMessage() async* { yield utf8.encode('The Dart VM service is listening on foo bar\n'); final sampleJson = { 'id': '0', 'error': _badErrorMessage, }; yield utf8.encode(json.encode(sampleJson)); } Stream> _eventMessage() async* { yield utf8.encode('The Dart VM service is listening on foo bar\n'); final sampleJson = { 'event': 'fooEvent', 'params': {'foo': 'bar', 'baz': 'bang'} }; yield utf8.encode(json.encode(sampleJson)); } Stream> _goodMessage() async* { yield utf8.encode('The Dart VM service is listening on foo bar\n'); final sampleJson = { 'id': '0', 'result': {'foo': 'bar'} }; yield utf8.encode(json.encode(sampleJson)); } Stream> _noMessage() async* { yield utf8.encode(''); } class MockProcess implements Process { MockStdin mockin = MockStdin(); bool killed = false; @override late Stream> stderr; @override late Stream> stdout; @override late Future 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(); @override void add(List data) { controller.add(utf8.decode(data)); } @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _ServerListener with ServerListener { final unexpectedMessageController = StreamController(); final unexpectedNotificationFormatCompleter = StreamController(); final unexpectedResponseCompleter = StreamController(); final unexpectedResponseFormatCompleter = StreamController(); @override void log(String prefix, String details) {} @override void unexpectedMessage(Map message) { unexpectedMessageController.add(message); } @override void unexpectedNotificationFormat(Map message) { unexpectedNotificationFormatCompleter.add(message); } @override void unexpectedResponse(Map message, Object id) { unexpectedResponseCompleter.add(message); } @override void unexpectedResponseFormat(Map message) { unexpectedResponseFormatCompleter.add(message); } }