// 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:io' as io; import 'dart:typed_data'; import 'package:fake_async/fake_async.dart'; import 'package:file/file.dart'; import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/android/android_workflow.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/base/dds.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/utils.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/commands/daemon.dart'; import 'package:flutter_tools/src/daemon.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/ios/ios_workflow.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:flutter_tools/src/windows/windows_workflow.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_devices.dart'; import '../../src/fakes.dart'; /// Runs a callback using FakeAsync.run while continually pumping the /// microtask queue. This avoids a deadlock when tests `await` a Future /// which queues a microtask that will not be processed unless the queue /// is flushed. Future _runFakeAsync(Future Function(FakeAsync time) f) async { return FakeAsync().run((FakeAsync time) async { bool pump = true; final Future future = f(time).whenComplete(() => pump = false); while (pump) { time.flushMicrotasks(); } return future; }); } class FakeDaemonStreams implements DaemonStreams { final StreamController inputs = StreamController(); final StreamController outputs = StreamController(); @override Stream get inputStream { return inputs.stream; } @override void send(Map message, [List? binary]) { outputs.add(DaemonMessage(message, binary != null ? Stream>.value(binary) : null)); } @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 Daemon daemon; late NotifyingLogger notifyingLogger; group('daemon', () { late FakeDaemonStreams daemonStreams; late DaemonConnection daemonConnection; setUp(() { BufferLogger bufferLogger; bufferLogger = BufferLogger.test(); notifyingLogger = NotifyingLogger(verbose: false, parent: bufferLogger); daemonStreams = FakeDaemonStreams(); daemonConnection = DaemonConnection(daemonStreams: daemonStreams, logger: bufferLogger); }); tearDown(() async { await daemon.shutdown(); notifyingLogger.dispose(); await daemonConnection.dispose(); }); testUsingContext('daemon.version command should succeed', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'daemon.version'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['result'], isNotEmpty); expect(response.data['result'], isA()); }); testUsingContext( 'daemon.getSupportedPlatforms command should succeed', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); // Use the flutter_gallery project which has a known set of supported platforms. final String projectPath = globals.fs.path.join( getFlutterRoot(), 'dev', 'integration_tests', 'flutter_gallery', ); daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'daemon.getSupportedPlatforms', 'params': {'projectRoot': projectPath}, }), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['result'], isNotEmpty); expect(response.data['result']! as Map, const { 'platforms': ['macos', 'windows'], 'platformTypes': >{ 'web': { 'isSupported': false, 'reasons': >[ { 'reasonText': 'the Web feature is not enabled', 'fixText': 'Run "flutter config --enable-web"', 'fixCode': 'config', }, ], }, 'android': { 'isSupported': false, 'reasons': >[ { 'reasonText': 'the Android feature is not enabled', 'fixText': 'Run "flutter config --enable-android"', 'fixCode': 'config', }, ], }, 'ios': { 'isSupported': false, 'reasons': >[ { 'reasonText': 'the iOS feature is not enabled', 'fixText': 'Run "flutter config --enable-ios"', 'fixCode': 'config', }, ], }, 'linux': { 'isSupported': false, 'reasons': >[ { 'reasonText': 'the Linux feature is not enabled', 'fixText': 'Run "flutter config --enable-linux-desktop"', 'fixCode': 'config', }, ], }, 'macos': {'isSupported': true}, 'windows': {'isSupported': true}, 'fuchsia': { 'isSupported': false, 'reasons': >[ { 'reasonText': 'the Fuchsia feature is not enabled', 'fixText': 'Run "flutter config --enable-fuchsia"', 'fixCode': 'config', }, { 'reasonText': 'the Fuchsia platform is not enabled for this project', 'fixText': 'Run "flutter create --platforms=fuchsia ." in your application directory', 'fixCode': 'create', }, ], }, 'custom': { 'isSupported': false, 'reasons': >[ { 'reasonText': 'the custom devices feature is not enabled', 'fixText': 'Run "flutter config --enable-custom-devices"', 'fixCode': 'config', }, ], }, }, }); }, overrides: { // Disable Android/iOS and enable macOS to make sure result is consistent and defaults are tested off. FeatureFlags: () => TestFeatureFlags( isAndroidEnabled: false, isIOSEnabled: false, isMacOSEnabled: true, isWindowsEnabled: true, ), }, ); testUsingContext( 'printError should send daemon.logMessage event', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); globals.printError('daemon.logMessage test'); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(( DaemonMessage message, ) { return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map)['level'] == 'error'; }); expect(response.data['id'], isNull); expect(response.data['event'], 'daemon.logMessage'); final Map logMessage = castStringKeyedMap(response.data['params'])!.cast(); expect(logMessage['level'], 'error'); expect(logMessage['message'], 'daemon.logMessage test'); }, overrides: {Logger: () => notifyingLogger}, ); testUsingContext( 'printWarning should send daemon.logMessage event', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); globals.printWarning('daemon.logMessage test'); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(( DaemonMessage message, ) { return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map)['level'] == 'warning'; }); expect(response.data['id'], isNull); expect(response.data['event'], 'daemon.logMessage'); final Map logMessage = castStringKeyedMap(response.data['params'])!.cast(); expect(logMessage['level'], 'warning'); expect(logMessage['message'], 'daemon.logMessage test'); }, overrides: {Logger: () => notifyingLogger}, ); testUsingContext( 'printStatus should log to stdout when logToStdout is enabled', () async { final StringBuffer buffer = await capturedConsolePrint(() { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger, logToStdout: true); globals.printStatus('daemon.logMessage test'); return Future.value(); }); expect(buffer.toString().trim(), 'daemon.logMessage test'); }, overrides: {Logger: () => notifyingLogger}, ); testUsingContext( 'printBox should log to stdout when logToStdout is enabled', () async { final StringBuffer buffer = await capturedConsolePrint(() { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger, logToStdout: true); globals.printBox('This is the box message', title: 'Sample title'); return Future.value(); }); expect(buffer.toString().trim(), contains('Sample title: This is the box message')); }, overrides: {Logger: () => notifyingLogger}, ); testUsingContext( 'printTrace should send daemon.logMessage event when notifyVerbose is enabled', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); notifyingLogger.notifyVerbose = false; globals.printTrace('daemon.logMessage test 1'); notifyingLogger.notifyVerbose = true; globals.printTrace('daemon.logMessage test 2'); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(( DaemonMessage message, ) { return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map)['level'] == 'trace'; }); expect(response.data['id'], isNull); expect(response.data['event'], 'daemon.logMessage'); final Map logMessage = castStringKeyedMap(response.data['params'])!.cast(); expect(logMessage['level'], 'trace'); expect(logMessage['message'], 'daemon.logMessage test 2'); }, overrides: {Logger: () => notifyingLogger}, ); testUsingContext( 'daemon.setNotifyVerbose command should update the notify verbose status to true', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); expect(notifyingLogger.notifyVerbose, false); daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'daemon.setNotifyVerbose', 'params': {'verbose': true}, }), ); await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(notifyingLogger.notifyVerbose, true); }, ); testUsingContext( 'daemon.setNotifyVerbose command should update the notify verbose status to false', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); notifyingLogger.notifyVerbose = false; daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'daemon.setNotifyVerbose', 'params': {'verbose': false}, }), ); await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(notifyingLogger.notifyVerbose, false); }, ); testUsingContext('daemon.shutdown command should stop daemon', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'daemon.shutdown'}), ); return daemon.onExit.then((int code) async { await daemonStreams.inputs.close(); expect(code, 0); }); }); testUsingContext('app.restart without an appId should report an error', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add(DaemonMessage({'id': 0, 'method': 'app.restart'})); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['error'], contains('appId is required')); }); testUsingContext( 'ext.flutter.debugPaint via service extension without an appId should report an error', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'app.callServiceExtension', 'params': {'methodName': 'ext.flutter.debugPaint'}, }), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['error'], contains('appId is required')); }, ); testUsingContext('app.stop without appId should report an error', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add(DaemonMessage({'id': 0, 'method': 'app.stop'})); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['error'], contains('appId is required')); }); testUsingContext('device.getDevices should respond with list', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'device.getDevices'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['result'], isList); }); testUsingContext('device.getDevices reports available devices', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon.deviceDomain.addDeviceDiscoverer(discoverer); discoverer.addDevice(FakeAndroidDevice()); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'device.getDevices'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); final Object? result = response.data['result']; expect(result, isList); expect(result, isNotEmpty); }); testUsingContext( 'should send device.added event when device is discovered', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon.deviceDomain.addDeviceDiscoverer(discoverer); discoverer.addDevice(FakeAndroidDevice()); final List> names = >[]; await daemonStreams.outputs.stream.skipWhile(_isConnectedEvent).take(1).forEach(( DaemonMessage response, ) async { expect(response.data['event'], 'device.added'); expect(response.data['params'], isMap); final Map params = castStringKeyedMap(response.data['params'])!; names.add(params); }); await daemonStreams.outputs.close(); expect( names, containsAll(const >[ { 'id': 'device', 'name': 'android device', 'platform': 'android-arm', 'emulator': false, 'category': 'mobile', 'platformType': 'android', 'ephemeral': false, 'emulatorId': 'device', 'sdk': 'Android 12', 'isConnected': true, 'connectionInterface': 'attached', 'capabilities': { 'hotReload': true, 'hotRestart': true, 'screenshot': true, 'fastStart': true, 'flutterExit': true, 'hardwareRendering': true, 'startPaused': true, }, }, ]), ); }, overrides: { AndroidWorkflow: () => FakeAndroidWorkflow(), IOSWorkflow: () => FakeIOSWorkflow(), WindowsWorkflow: () => FakeWindowsWorkflow(), }, ); testUsingContext('device.discoverDevices should respond with list', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'device.discoverDevices'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['result'], isList); }); testUsingContext('device.discoverDevices reports available devices', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon.deviceDomain.addDeviceDiscoverer(discoverer); discoverer.addDevice(FakeAndroidDevice()); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'device.discoverDevices'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); final Object? result = response.data['result']; expect(result, isList); expect(result, isNotEmpty); expect(discoverer.discoverDevicesCalled, true); }); testUsingContext('device.supportsRuntimeMode returns correct value', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon.deviceDomain.addDeviceDiscoverer(discoverer); final FakeAndroidDevice device = FakeAndroidDevice(); discoverer.addDevice(device); daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'device.supportsRuntimeMode', 'params': {'deviceId': 'device', 'buildMode': 'profile'}, }), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); final Object? result = response.data['result']; expect(result, true); expect(device.supportsRuntimeModeCalledBuildMode, BuildMode.profile); }); testUsingContext('device.logReader.start and .stop starts and stops log reader', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon.deviceDomain.addDeviceDiscoverer(discoverer); final FakeAndroidDevice device = FakeAndroidDevice(); discoverer.addDevice(device); final FakeDeviceLogReader logReader = FakeDeviceLogReader(); device.logReader = logReader; daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'device.logReader.start', 'params': {'deviceId': 'device'}, }), ); final Stream broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream(); final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent); expect(firstResponse.data['id'], 0); final String? logReaderId = firstResponse.data['result'] as String?; expect(logReaderId, isNotNull); // Try sending logs. logReader.logLinesController.add('Sample log line'); final DaemonMessage logEvent = await broadcastOutput.firstWhere( (DaemonMessage message) => message.data['event'] != null && message.data['event'] != 'device.added', ); expect(logEvent.data['params'], 'Sample log line'); // Now try to stop the log reader. expect(logReader.disposeCalled, false); daemonStreams.inputs.add( DaemonMessage({ 'id': 1, 'method': 'device.logReader.stop', 'params': {'id': logReaderId}, }), ); final DaemonMessage stopResponse = await broadcastOutput.firstWhere(_notEvent); expect(stopResponse.data['id'], 1); expect(logReader.disposeCalled, true); }); group('device.startApp and .stopApp', () { late FakeApplicationPackageFactory applicationPackageFactory; setUp(() { applicationPackageFactory = FakeApplicationPackageFactory(); }); testUsingContext( 'device.startApp and .stopApp starts and stops an app', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon.deviceDomain.addDeviceDiscoverer(discoverer); final FakeAndroidDevice device = FakeAndroidDevice(); discoverer.addDevice(device); final Stream broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream(); // First upload the application package. final FakeApplicationPackage applicationPackage = FakeApplicationPackage(); applicationPackageFactory.applicationPackage = applicationPackage; daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'device.uploadApplicationPackage', 'params': { 'targetPlatform': 'android', 'applicationBinary': 'test_file', }, }), ); final DaemonMessage applicationPackageIdResponse = await broadcastOutput.firstWhere( _notEvent, ); expect(applicationPackageIdResponse.data['id'], 0); expect(applicationPackageFactory.applicationBinaryRequested!.basename, 'test_file'); expect(applicationPackageFactory.platformRequested, TargetPlatform.android); final String? applicationPackageId = applicationPackageIdResponse.data['result'] as String?; // Try starting the app. final Uri vmServiceUri = Uri.parse('http://127.0.0.1:12345/vmService'); device.launchResult = LaunchResult.succeeded(vmServiceUri: vmServiceUri); daemonStreams.inputs.add( DaemonMessage({ 'id': 1, 'method': 'device.startApp', 'params': { 'deviceId': 'device', 'applicationPackageId': applicationPackageId, 'debuggingOptions': DebuggingOptions.enabled(BuildInfo.debug).toJson(), }, }), ); final DaemonMessage startAppResponse = await broadcastOutput.firstWhere(_notEvent); expect(startAppResponse.data['id'], 1); expect(device.startAppPackage, applicationPackage); final Map startAppResult = startAppResponse.data['result']! as Map; expect(startAppResult['started'], true); expect(startAppResult['vmServiceUri'], vmServiceUri.toString()); // Try stopping the app. daemonStreams.inputs.add( DaemonMessage({ 'id': 2, 'method': 'device.stopApp', 'params': { 'deviceId': 'device', 'applicationPackageId': applicationPackageId, }, }), ); final DaemonMessage stopAppResponse = await broadcastOutput.firstWhere(_notEvent); expect(stopAppResponse.data['id'], 2); expect(device.stopAppPackage, applicationPackage); final bool? stopAppResult = stopAppResponse.data['result'] as bool?; expect(stopAppResult, true); }, overrides: {ApplicationPackageFactory: () => applicationPackageFactory}, ); }); testUsingContext( 'device.startDartDevelopmentService and .shutdownDartDevelopmentService starts and stops DDS', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon.deviceDomain.addDeviceDiscoverer(discoverer); final FakeAndroidDevice device = FakeAndroidDevice(); discoverer.addDevice(device); final Completer ddsDoneCompleter = Completer(); device.dds.done = ddsDoneCompleter.future; final Uri fakeDdsUri = Uri.parse('http://fake_dds_uri'); device.dds.uri = fakeDdsUri; // Try starting DDS. expect(device.dds.startCalled, false); daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'device.startDartDevelopmentService', 'params': { 'deviceId': 'device', 'disableServiceAuthCodes': false, 'vmServiceUri': 'http://fake_uri/auth_code', }, }), ); final Stream broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream(); final DaemonMessage startResponse = await broadcastOutput.firstWhere(_notEvent); expect(startResponse.data['id'], 0); expect(startResponse.data['error'], isNull); final Map? result = startResponse.data['result'] as Map?; final String? ddsUri = result!['ddsUri'] as String?; expect(ddsUri, fakeDdsUri.toString()); expect(device.dds.startCalled, true); expect(device.dds.startDisableServiceAuthCodes, false); expect(device.dds.startVMServiceUri, Uri.parse('http://fake_uri/auth_code')); // dds.done event should be sent to the client. ddsDoneCompleter.complete(); final DaemonMessage startEvent = await broadcastOutput.firstWhere( (DaemonMessage message) => message.data['event'] != null && message.data['event'] == 'device.dds.done.device', ); expect(startEvent, isNotNull); // Try stopping DDS. expect(device.dds.shutdownCalled, false); daemonStreams.inputs.add( DaemonMessage({ 'id': 1, 'method': 'device.shutdownDartDevelopmentService', 'params': {'deviceId': 'device'}, }), ); final DaemonMessage stopResponse = await broadcastOutput.firstWhere(_notEvent); expect(stopResponse.data['id'], 1); expect(stopResponse.data['error'], isNull); expect(device.dds.shutdownCalled, true); }, ); testUsingContext('device.getDiagnostics returns correct value', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); final FakePollingDeviceDiscovery discoverer1 = FakePollingDeviceDiscovery(); discoverer1.diagnostics = ['fake diagnostic 1', 'fake diagnostic 2']; final FakePollingDeviceDiscovery discoverer2 = FakePollingDeviceDiscovery(); discoverer2.diagnostics = ['fake diagnostic 3', 'fake diagnostic 4']; daemon.deviceDomain.addDeviceDiscoverer(discoverer1); daemon.deviceDomain.addDeviceDiscoverer(discoverer2); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'device.getDiagnostics'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['result'], [ 'fake diagnostic 1', 'fake diagnostic 2', 'fake diagnostic 3', 'fake diagnostic 4', ]); }); testUsingContext('emulator.launch without an emulatorId should report an error', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'emulator.launch'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['error'], contains('emulatorId is required')); }); testUsingContext('emulator.launch coldboot parameter must be boolean', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); final Map params = {'emulatorId': 'device', 'coldBoot': 1}; daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'emulator.launch', 'params': params}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['error'], contains('coldBoot is not a bool')); }); testUsingContext('emulator.getEmulators should respond with list', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'emulator.getEmulators'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent); expect(response.data['id'], 0); expect(response.data['result'], isList); }); testUsingContext('daemon can send exposeUrl requests to the client', () async { const String originalUrl = 'http://localhost:1234/'; const String mappedUrl = 'https://publichost:4321/'; daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); // Respond to any requests from the daemon to expose a URL. unawaited( daemonStreams.outputs.stream .firstWhere((DaemonMessage request) => request.data['method'] == 'app.exposeUrl') .then((DaemonMessage request) { expect((request.data['params']! as Map)['url'], equals(originalUrl)); daemonStreams.inputs.add( DaemonMessage({ 'id': request.data['id'], 'result': {'url': mappedUrl}, }), ); }), ); final String exposedUrl = await daemon.daemonDomain.exposeUrl(originalUrl); expect(exposedUrl, equals(mappedUrl)); }); testUsingContext( 'devtools.serve command should return host and port on success', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'devtools.serve'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere( (DaemonMessage response) => response.data['id'] == 0, ); final Map result = response.data['result']! as Map; expect(result, isNotEmpty); expect(result['host'], '127.0.0.1'); expect(result['port'], 1234); }, overrides: { DevtoolsLauncher: () => FakeDevtoolsLauncher(serverAddress: DevToolsServerAddress('127.0.0.1', 1234)), }, ); testUsingContext( 'devtools.serve command should return null fields if null returned', () async { daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({'id': 0, 'method': 'devtools.serve'}), ); final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere( (DaemonMessage response) => response.data['id'] == 0, ); final Map result = response.data['result']! as Map; expect(result, isNotEmpty); expect(result['host'], null); expect(result['port'], null); }, overrides: {DevtoolsLauncher: () => FakeDevtoolsLauncher()}, ); testUsingContext( 'proxy.connect tries to connect to an ipv4 address and proxies the connection correctly', () async { final TestIOOverrides ioOverrides = TestIOOverrides(); await io.IOOverrides.runWithIOOverrides(() async { final FakeSocket socket = FakeSocket(); bool connectCalled = false; int? connectPort; ioOverrides.connectCallback = (Object? host, int port) async { connectCalled = true; connectPort = port; if (host == io.InternetAddress.loopbackIPv4) { return socket; } throw const io.SocketException('fail'); }; daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'proxy.connect', 'params': {'port': 123}, }), ); final Stream broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream(); final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent); expect(firstResponse.data['id'], 0); expect(firstResponse.data['result'], isNotNull); expect(connectCalled, true); expect(connectPort, 123); final Object? id = firstResponse.data['result']; // Can send received data as event. socket.controller.add(Uint8List.fromList([10, 11, 12])); final DaemonMessage dataEvent = await broadcastOutput.firstWhere( (DaemonMessage message) => message.data['event'] != null && message.data['event'] == 'proxy.data.$id', ); expect(dataEvent.binary, isNotNull); final List> data = await dataEvent.binary!.toList(); expect(data[0], [10, 11, 12]); // Can proxy data to the socket. daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'proxy.write', 'params': {'id': id}, }, Stream>.value([21, 22, 23])), ); await pumpEventQueue(); expect(socket.addedData[0], [21, 22, 23]); // Closes the connection when disconnect request received. expect(socket.closeCalled, false); daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'proxy.disconnect', 'params': {'id': id}, }), ); await pumpEventQueue(); expect(socket.closeCalled, true); // Sends disconnected event when socket.done completer finishes. socket.doneCompleter.complete(true); final DaemonMessage disconnectEvent = await broadcastOutput.firstWhere( (DaemonMessage message) => message.data['event'] != null && message.data['event'] == 'proxy.disconnected.$id', ); expect(disconnectEvent.data, isNotNull); }, ioOverrides); }, ); testUsingContext('proxy.connect connects to ipv6 if ipv4 failed', () async { final TestIOOverrides ioOverrides = TestIOOverrides(); await io.IOOverrides.runWithIOOverrides(() async { final FakeSocket socket = FakeSocket(); bool connectIpv4Called = false; int? connectPort; ioOverrides.connectCallback = (Object? host, int port) async { connectPort = port; if (host == io.InternetAddress.loopbackIPv4) { connectIpv4Called = true; } else if (host == io.InternetAddress.loopbackIPv6) { return socket; } throw const io.SocketException('fail'); }; daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'proxy.connect', 'params': {'port': 123}, }), ); final Stream broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream(); final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent); expect(firstResponse.data['id'], 0); expect(firstResponse.data['result'], isNotNull); expect(connectIpv4Called, true); expect(connectPort, 123); }, ioOverrides); }); testUsingContext('proxy.connect fails if both ipv6 and ipv4 failed', () async { final TestIOOverrides ioOverrides = TestIOOverrides(); await io.IOOverrides.runWithIOOverrides(() async { ioOverrides.connectCallback = (Object? host, int port) => throw const io.SocketException('fail'); daemon = Daemon(daemonConnection, notifyingLogger: notifyingLogger); daemonStreams.inputs.add( DaemonMessage({ 'id': 0, 'method': 'proxy.connect', 'params': {'port': 123}, }), ); final Stream broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream(); final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent); expect(firstResponse.data['id'], 0); expect(firstResponse.data['result'], isNull); expect(firstResponse.data['error'], isNotNull); }, ioOverrides); }); }); group('notifyingLogger', () { late BufferLogger bufferLogger; setUp(() { bufferLogger = BufferLogger.test(); }); tearDown(() { bufferLogger.clear(); }); testUsingContext('outputs trace messages in verbose mode', () async { final NotifyingLogger logger = NotifyingLogger(verbose: true, parent: bufferLogger); logger.printTrace('test'); expect(bufferLogger.errorText, contains('test')); }); testUsingContext('ignores trace messages in non-verbose mode', () async { final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger); final Future messageResult = logger.onMessage.first; logger.printTrace('test'); logger.printStatus('hello'); final LogMessage message = await messageResult; expect(message.level, 'status'); expect(message.message, 'hello'); expect(bufferLogger.errorText, isEmpty); }); testUsingContext('sends trace messages in notify verbose mode', () async { final NotifyingLogger logger = NotifyingLogger( verbose: false, parent: bufferLogger, notifyVerbose: true, ); final Future messageResult = logger.onMessage.first; logger.printTrace('hello'); final LogMessage message = await messageResult; expect(message.level, 'trace'); expect(message.message, 'hello'); expect(bufferLogger.errorText, isEmpty); }); testUsingContext('buffers messages sent before a subscription', () async { final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger); logger.printStatus('hello'); final LogMessage message = await logger.onMessage.first; expect(message.level, 'status'); expect(message.message, 'hello'); }); testWithoutContext('responds to .supportsColor', () async { final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger); expect(logger.supportsColor, isFalse); }); }); group('daemon queue', () { late DebounceOperationQueue queue; const Duration debounceDuration = Duration(seconds: 1); setUp(() { queue = DebounceOperationQueue(); }); testWithoutContext('debounces/merges same operation type and returns same result', () async { await _runFakeAsync((FakeAsync time) async { final List> operations = >[ queue.queueAndDebounce('OP1', debounceDuration, () async => 1), queue.queueAndDebounce('OP1', debounceDuration, () async => 2), ]; time.elapse(debounceDuration * 5); final List results = await Future.wait(operations); expect(results, orderedEquals([1, 1])); }); }); testWithoutContext('does not merge results outside of the debounce duration', () async { await _runFakeAsync((FakeAsync time) async { final List> operations = >[ queue.queueAndDebounce('OP1', debounceDuration, () async => 1), Future.delayed( debounceDuration * 2, ).then((_) => queue.queueAndDebounce('OP1', debounceDuration, () async => 2)), ]; time.elapse(debounceDuration * 5); final List results = await Future.wait(operations); expect(results, orderedEquals([1, 2])); }); }); testWithoutContext('does not merge results of different operations', () async { await _runFakeAsync((FakeAsync time) async { final List> operations = >[ queue.queueAndDebounce('OP1', debounceDuration, () async => 1), queue.queueAndDebounce('OP2', debounceDuration, () async => 2), ]; time.elapse(debounceDuration * 5); final List results = await Future.wait(operations); expect(results, orderedEquals([1, 2])); }); }); testWithoutContext('does not run any operations concurrently', () async { // Crete a function that's slow, but throws if another instance of the // function is running. bool isRunning = false; Future f(int ret) async { if (isRunning) { throw Exception('Functions ran concurrently!'); } isRunning = true; await Future.delayed(debounceDuration * 2); isRunning = false; return ret; } await _runFakeAsync((FakeAsync time) async { final List> operations = >[ queue.queueAndDebounce('OP1', debounceDuration, () => f(1)), queue.queueAndDebounce('OP2', debounceDuration, () => f(2)), ]; time.elapse(debounceDuration * 5); final List results = await Future.wait(operations); expect(results, orderedEquals([1, 2])); }); }); }); } bool _notEvent(DaemonMessage message) => message.data['event'] == null; bool _isConnectedEvent(DaemonMessage message) => message.data['event'] == 'daemon.connected'; class FakeWindowsWorkflow extends Fake implements WindowsWorkflow { FakeWindowsWorkflow({this.canListDevices = true}); @override final bool canListDevices; } class FakeAndroidWorkflow extends Fake implements AndroidWorkflow { FakeAndroidWorkflow({this.canListDevices = true}); @override final bool canListDevices; } class FakeIOSWorkflow extends Fake implements IOSWorkflow { FakeIOSWorkflow({this.canListDevices = true}); @override final bool canListDevices; } class FakeAndroidDevice extends Fake implements AndroidDevice { @override final String id = 'device'; @override final String name = 'android device'; @override String get displayName => name; @override Future get emulatorId async => 'device'; @override Future get targetPlatform async => TargetPlatform.android_arm; @override Future get isLocalEmulator async => false; @override final Category category = Category.mobile; @override final PlatformType platformType = PlatformType.android; @override final bool ephemeral = false; @override final bool isConnected = true; @override final DeviceConnectionInterface connectionInterface = DeviceConnectionInterface.attached; @override Future get sdkNameAndVersion async => 'Android 12'; @override bool get supportsHotReload => true; @override bool get supportsHotRestart => true; @override bool get supportsScreenshot => true; @override bool get supportsFastStart => true; @override bool get supportsFlutterExit => true; @override Future get supportsHardwareRendering async => true; @override bool get supportsStartPaused => true; @override final FakeDartDevelopmentService dds = FakeDartDevelopmentService(); BuildMode? supportsRuntimeModeCalledBuildMode; @override Future supportsRuntimeMode(BuildMode buildMode) async { supportsRuntimeModeCalledBuildMode = buildMode; return true; } late DeviceLogReader logReader; @override FutureOr getLogReader({ApplicationPackage? app, bool includePastLogs = false}) => logReader; ApplicationPackage? startAppPackage; late LaunchResult launchResult; @override Future startApp( ApplicationPackage? package, { String? mainPath, String? route, DebuggingOptions? debuggingOptions, Map platformArgs = const {}, bool prebuiltApplication = false, bool ipv6 = false, String? userIdentifier, }) async { startAppPackage = package; return launchResult; } ApplicationPackage? stopAppPackage; @override Future stopApp(ApplicationPackage? app, {String? userIdentifier}) async { stopAppPackage = app; return true; } } class FakeDartDevelopmentService extends Fake implements DartDevelopmentService { bool startCalled = false; late Uri startVMServiceUri; bool? startDisableServiceAuthCodes; bool shutdownCalled = false; @override late Future done; @override Uri? uri; @override Future startDartDevelopmentService( Uri vmServiceUri, { int? ddsPort, FlutterDevice? device, bool? ipv6, bool? disableServiceAuthCodes, bool enableDevTools = false, bool cacheStartupProfile = false, String? google3WorkspaceRoot, Uri? devToolsServerAddress, }) async { startCalled = true; startVMServiceUri = vmServiceUri; startDisableServiceAuthCodes = disableServiceAuthCodes; } @override Future shutdown() async { shutdownCalled = true; } } class FakeDeviceLogReader implements DeviceLogReader { final StreamController logLinesController = StreamController(); bool disposeCalled = false; @override void dispose() { disposeCalled = true; } @override Stream get logLines => logLinesController.stream; @override String get name => 'device'; @override Future provideVmService(FlutterVmService? connectedVmService) async {} } class FakeApplicationPackageFactory implements ApplicationPackageFactory { TargetPlatform? platformRequested; File? applicationBinaryRequested; ApplicationPackage? applicationPackage; @override Future getPackageForPlatform( TargetPlatform platform, { BuildInfo? buildInfo, File? applicationBinary, }) async { platformRequested = platform; applicationBinaryRequested = applicationBinary; return applicationPackage; } } class FakeApplicationPackage extends Fake implements ApplicationPackage {} class TestIOOverrides extends io.IOOverrides { late Future Function(Object? host, int port) connectCallback; @override Future socketConnect( Object? host, int port, { Object? sourceAddress, int sourcePort = 0, Duration? timeout, }) { return connectCallback(host, port); } } class FakeSocket extends Fake implements io.Socket { bool closeCalled = false; final StreamController controller = StreamController(); final List> addedData = >[]; final Completer doneCompleter = Completer(); @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 add(List data) { addedData.add(data); } @override Future close() async { closeCalled = true; } @override Future get done => doneCompleter.future; @override void destroy() {} }