// 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 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.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/proxied_devices/devices.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_devices.dart'; void main() { Daemon? daemon; late NotifyingLogger notifyingLogger; late BufferLogger bufferLogger; late FakeAndroidDevice fakeDevice; late FakeApplicationPackageFactory applicationPackageFactory; late MemoryFileSystem memoryFileSystem; late FakeProcessManager fakeProcessManager; group('ProxiedDevices', () { late DaemonConnection serverDaemonConnection; late DaemonConnection clientDaemonConnection; setUp(() { bufferLogger = BufferLogger.test(); notifyingLogger = NotifyingLogger(verbose: false, parent: bufferLogger); final FakeDaemonStreams serverDaemonStreams = FakeDaemonStreams(); serverDaemonConnection = DaemonConnection( daemonStreams: serverDaemonStreams, logger: bufferLogger, ); final FakeDaemonStreams clientDaemonStreams = FakeDaemonStreams(); clientDaemonConnection = DaemonConnection( daemonStreams: clientDaemonStreams, logger: bufferLogger, ); serverDaemonStreams.inputs.addStream(clientDaemonStreams.outputs.stream); clientDaemonStreams.inputs.addStream(serverDaemonStreams.outputs.stream); applicationPackageFactory = FakeApplicationPackageFactory(); memoryFileSystem = MemoryFileSystem(); fakeProcessManager = FakeProcessManager.empty(); }); tearDown(() async { if (daemon != null) { return daemon!.shutdown(); } notifyingLogger.dispose(); await serverDaemonConnection.dispose(); await clientDaemonConnection.dispose(); }); testUsingContext('can list devices', () async { daemon = Daemon( serverDaemonConnection, notifyingLogger: notifyingLogger, ); fakeDevice = FakeAndroidDevice(); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon!.deviceDomain.addDeviceDiscoverer(discoverer); discoverer.addDevice(fakeDevice); final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger); final List<Device> devices = await proxiedDevices.discoverDevices(); expect(devices, hasLength(1)); final Device device = devices[0]; expect(device.id, fakeDevice.id); expect(device.name, 'Proxied ${fakeDevice.name}'); expect(await device.targetPlatform, await fakeDevice.targetPlatform); expect(await device.isLocalEmulator, await fakeDevice.isLocalEmulator); }); testUsingContext('calls supportsRuntimeMode', () async { daemon = Daemon( serverDaemonConnection, notifyingLogger: notifyingLogger, ); fakeDevice = FakeAndroidDevice(); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon!.deviceDomain.addDeviceDiscoverer(discoverer); discoverer.addDevice(fakeDevice); final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger); final List<Device> devices = await proxiedDevices.devices; expect(devices, hasLength(1)); final Device device = devices[0]; final bool supportsRuntimeMode = await device.supportsRuntimeMode(BuildMode.release); expect(fakeDevice.supportsRuntimeModeCalledBuildMode, BuildMode.release); expect(supportsRuntimeMode, true); }); testUsingContext('redirects logs', () async { daemon = Daemon( serverDaemonConnection, notifyingLogger: notifyingLogger, ); fakeDevice = FakeAndroidDevice(); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon!.deviceDomain.addDeviceDiscoverer(discoverer); discoverer.addDevice(fakeDevice); final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger); final FakeDeviceLogReader fakeLogReader = FakeDeviceLogReader(); fakeDevice.logReader = fakeLogReader; final List<Device> devices = await proxiedDevices.devices; expect(devices, hasLength(1)); final Device device = devices[0]; final DeviceLogReader logReader = await device.getLogReader(); fakeLogReader.logLinesController.add('Some log line'); final String receivedLogLine = await logReader.logLines.first; expect(receivedLogLine, 'Some log line'); // Now try to stop the log reader expect(fakeLogReader.disposeCalled, false); logReader.dispose(); await pumpEventQueue(); expect(fakeLogReader.disposeCalled, true); }); testUsingContext('starts and stops app', () async { daemon = Daemon( serverDaemonConnection, notifyingLogger: notifyingLogger, ); fakeDevice = FakeAndroidDevice(); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon!.deviceDomain.addDeviceDiscoverer(discoverer); discoverer.addDevice(fakeDevice); final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger); final FakePrebuiltApplicationPackage prebuiltApplicationPackage = FakePrebuiltApplicationPackage(); final File dummyApplicationBinary = memoryFileSystem.file('/directory/dummy_file'); dummyApplicationBinary.parent.createSync(); dummyApplicationBinary.writeAsStringSync('dummy content'); prebuiltApplicationPackage.applicationPackage = dummyApplicationBinary; final List<Device> devices = await proxiedDevices.devices; expect(devices, hasLength(1)); final Device device = devices[0]; // Now try to start the app final FakeApplicationPackage applicationPackage = FakeApplicationPackage(); applicationPackageFactory.applicationPackage = applicationPackage; final Uri vmServiceUri = Uri.parse('http://127.0.0.1:12345/vmService'); fakeDevice.launchResult = LaunchResult.succeeded(vmServiceUri: vmServiceUri); final LaunchResult launchResult = await device.startApp( prebuiltApplicationPackage, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ); expect(launchResult.started, true); // The returned vmServiceUri was a forwarded port, so we cannot compare them directly. expect(launchResult.vmServiceUri!.path, vmServiceUri.path); expect(applicationPackageFactory.applicationBinaryRequested!.readAsStringSync(), 'dummy content'); expect(applicationPackageFactory.platformRequested, TargetPlatform.android_arm); expect(fakeDevice.startAppPackage, applicationPackage); // Now try to stop the app final bool stopAppResult = await device.stopApp(prebuiltApplicationPackage); expect(fakeDevice.stopAppPackage, applicationPackage); expect(stopAppResult, true); }, overrides: <Type, Generator>{ ApplicationPackageFactory: () => applicationPackageFactory, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeProcessManager, }); testUsingContext('takes screenshot', () async { daemon = Daemon( serverDaemonConnection, notifyingLogger: notifyingLogger, ); fakeDevice = FakeAndroidDevice(); final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); daemon!.deviceDomain.addDeviceDiscoverer(discoverer); discoverer.addDevice(fakeDevice); final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger); final List<Device> devices = await proxiedDevices.devices; expect(devices, hasLength(1)); final Device device = devices[0]; final List<int> screenshot = <int>[1,2,3,4,5]; fakeDevice.screenshot = screenshot; final File screenshotOutputFile = memoryFileSystem.file('screenshot_file'); await device.takeScreenshot(screenshotOutputFile); expect(await screenshotOutputFile.readAsBytes(), screenshot); }, overrides: <Type, Generator>{ FileSystem: () => memoryFileSystem, ProcessManager: () => fakeProcessManager, }); }); } class FakeDaemonStreams implements DaemonStreams { final StreamController<DaemonMessage> inputs = StreamController<DaemonMessage>(); final StreamController<DaemonMessage> outputs = StreamController<DaemonMessage>(); @override Stream<DaemonMessage> get inputStream { return inputs.stream; } @override void send(Map<String, dynamic> message, [ List<int>? binary ]) { outputs.add(DaemonMessage(message, binary != null ? Stream<List<int>>.value(binary) : null)); } @override Future<void> dispose() async { await inputs.close(); // In some tests, outputs have no listeners. We don't wait for outputs to close. unawaited(outputs.close()); } } // Unfortunately Device, despite not being immutable, has an `operator ==`. // Until we fix that, we have to also ignore related lints here. // ignore: avoid_implementing_value_types class FakeAndroidDevice extends Fake implements AndroidDevice { @override final String id = 'device'; @override final String name = 'device'; @override Future<String> get emulatorId async => 'device'; @override Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm; @override Future<bool> get isLocalEmulator async => false; @override final Category category = Category.mobile; @override final PlatformType platformType = PlatformType.android; @override final bool ephemeral = false; @override Future<String> 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<bool> get supportsHardwareRendering async => true; @override bool get supportsStartPaused => true; BuildMode? supportsRuntimeModeCalledBuildMode; @override Future<bool> supportsRuntimeMode(BuildMode buildMode) async { supportsRuntimeModeCalledBuildMode = buildMode; return true; } late DeviceLogReader logReader; @override FutureOr<DeviceLogReader> getLogReader({ ApplicationPackage? app, bool includePastLogs = false, }) => logReader; ApplicationPackage? startAppPackage; late LaunchResult launchResult; @override Future<LaunchResult> startApp( ApplicationPackage? package, { String? mainPath, String? route, DebuggingOptions? debuggingOptions, Map<String, Object?> platformArgs = const <String, Object>{}, bool prebuiltApplication = false, bool ipv6 = false, String? userIdentifier, }) async { startAppPackage = package; return launchResult; } ApplicationPackage? stopAppPackage; @override Future<bool> stopApp( ApplicationPackage? app, { String? userIdentifier, }) async { stopAppPackage = app; return true; } late List<int> screenshot; @override Future<void> takeScreenshot(File outputFile) { return outputFile.writeAsBytes(screenshot); } } class FakeDeviceLogReader implements DeviceLogReader { final StreamController<String> logLinesController = StreamController<String>(); bool disposeCalled = false; @override int? appPid; @override FlutterVmService? connectedVMService; @override void dispose() { disposeCalled = true; } @override Stream<String> get logLines => logLinesController.stream; @override String get name => 'device'; } class FakeApplicationPackageFactory implements ApplicationPackageFactory { TargetPlatform? platformRequested; File? applicationBinaryRequested; ApplicationPackage? applicationPackage; @override Future<ApplicationPackage?> getPackageForPlatform(TargetPlatform platform, {BuildInfo? buildInfo, File? applicationBinary}) async { platformRequested = platform; applicationBinaryRequested = applicationBinary; return applicationPackage; } } class FakeApplicationPackage extends Fake implements ApplicationPackage {} class FakePrebuiltApplicationPackage extends Fake implements PrebuiltApplicationPackage { @override late File applicationPackage; }