// Copyright 2014 The Flutter Authors. 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:typed_data'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/daemon.dart'; import 'package:test/fake.dart'; import '../src/common.dart'; class FakeDaemonStreams extends DaemonStreams { final StreamController> inputs = StreamController>(); final StreamController> outputs = StreamController>(); @override Stream> get inputStream { return inputs.stream; } @override void send(Map message) { outputs.add(message); } @override Future dispose() async { await inputs.close(); // In some tests, outputs have no listeners. We don't wait for outputs to close. unawaited(outputs.close()); } } void main() { late BufferLogger bufferLogger; late FakeDaemonStreams daemonStreams; late DaemonConnection daemonConnection; setUp(() { bufferLogger = BufferLogger.test(); daemonStreams = FakeDaemonStreams(); daemonConnection = DaemonConnection( daemonStreams: daemonStreams, logger: bufferLogger, ); }); tearDown(() async { await daemonConnection.dispose(); }); group('DaemonConnection receiving end', () { testWithoutContext('redirects input to incoming commands', () async { final Map commandToSend = {'id': 0, 'method': 'some_method'}; daemonStreams.inputs.add(commandToSend); final Map commandReceived = await daemonConnection.incomingCommands.first; await daemonStreams.dispose(); expect(commandReceived, commandToSend); }); testWithoutContext('listenToEvent can receive the right events', () async { final Future> events = daemonConnection.listenToEvent('event1').toList(); daemonStreams.inputs.add({'event': 'event1', 'params': '1'}); daemonStreams.inputs.add({'event': 'event2', 'params': '2'}); daemonStreams.inputs.add({'event': 'event1', 'params': null}); daemonStreams.inputs.add({'event': 'event1', 'params': 3}); await pumpEventQueue(); await daemonConnection.dispose(); expect(await events, ['1', null, 3]); }); }); group('DaemonConnection sending end', () { testWithoutContext('sending requests', () async { unawaited(daemonConnection.sendRequest('some_method', 'param')); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], isNotNull); expect(data['method'], 'some_method'); expect(data['params'], 'param'); }); testWithoutContext('sending requests without param', () async { unawaited(daemonConnection.sendRequest('some_method')); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], isNotNull); expect(data['method'], 'some_method'); expect(data['params'], isNull); }); testWithoutContext('sending response', () async { daemonConnection.sendResponse('1', 'some_data'); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], '1'); expect(data['method'], isNull); expect(data['error'], isNull); expect(data['result'], 'some_data'); }); testWithoutContext('sending response without data', () async { daemonConnection.sendResponse('1'); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], '1'); expect(data['method'], isNull); expect(data['error'], isNull); expect(data['result'], isNull); }); testWithoutContext('sending error response', () async { daemonConnection.sendErrorResponse('1', 'error', StackTrace.fromString('stack trace')); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], '1'); expect(data['method'], isNull); expect(data['error'], 'error'); expect(data['trace'], 'stack trace'); }); testWithoutContext('sending events', () async { daemonConnection.sendEvent('some_event', '123'); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], isNull); expect(data['event'], 'some_event'); expect(data['params'], '123'); }); testWithoutContext('sending events without params', () async { daemonConnection.sendEvent('some_event'); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], isNull); expect(data['event'], 'some_event'); expect(data['params'], isNull); }); }); group('DaemonConnection request and response', () { testWithoutContext('receiving response from requests', () async { final Future requestFuture = daemonConnection.sendRequest('some_method', 'param'); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], isNotNull); expect(data['method'], 'some_method'); expect(data['params'], 'param'); final String id = data['id'] as String; daemonStreams.inputs.add({'id': id, 'result': '123'}); expect(await requestFuture, '123'); }); testWithoutContext('receiving response from requests without result', () async { final Future requestFuture = daemonConnection.sendRequest('some_method', 'param'); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], isNotNull); expect(data['method'], 'some_method'); expect(data['params'], 'param'); final String id = data['id'] as String; daemonStreams.inputs.add({'id': id}); expect(await requestFuture, null); }); testWithoutContext('receiving error response from requests without result', () async { final Future requestFuture = daemonConnection.sendRequest('some_method', 'param'); final Map data = await daemonStreams.outputs.stream.first; expect(data['id'], isNotNull); expect(data['method'], 'some_method'); expect(data['params'], 'param'); final String id = data['id'] as String; daemonStreams.inputs.add({'id': id, 'error': 'some_error', 'trace': 'stack trace'}); expect(requestFuture, throwsA('some_error')); }); }); group('TcpDaemonStreams', () { final Map testCommand = { 'id': 100, 'method': 'test', }; late FakeSocket socket; late TcpDaemonStreams daemonStreams; setUp(() { socket = FakeSocket(); daemonStreams = TcpDaemonStreams(socket, logger: bufferLogger); }); test('parses the message received on the socket', () async { socket.controller.add(Uint8List.fromList(utf8.encode('[${jsonEncode(testCommand)}]\n'))); final Map command = await daemonStreams.inputStream.first; expect(command, testCommand); }); test('sends the encoded message through the socket', () async { daemonStreams.send(testCommand); await pumpEventQueue(); expect(socket.writtenObjects.length, 1); expect(socket.writtenObjects[0].toString(), '[${jsonEncode(testCommand)}]\n'); }); test('dispose calls socket.close', () async { await daemonStreams.dispose(); expect(socket.closeCalled, isTrue); }); }); } class FakeSocket extends Fake implements Socket { bool closeCalled = false; final StreamController controller = StreamController(); final List writtenObjects = []; @override StreamSubscription listen( void Function(Uint8List event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError, }) { return controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override void write(Object? object) { writtenObjects.add(object); } @override Future close() async { closeCalled = true; } }