// 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 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/ios/application_package.dart'; import 'package:flutter_tools/src/ios/core_devices.dart'; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/iproxy.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcode_debug.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; import '../../src/fakes.dart'; const Map<String, String> kDyLdLibEntry = <String, String>{ 'DYLD_LIBRARY_PATH': '/path/to/libraries', }; void main() { late Artifacts artifacts; late String iosDeployPath; late FileSystem fileSystem; late Directory bundleDirectory; setUp(() { artifacts = Artifacts.test(); fileSystem = MemoryFileSystem.test(); bundleDirectory = fileSystem.directory('bundle'); iosDeployPath = artifacts.getHostArtifact(HostArtifact.iosDeploy).path; }); testWithoutContext('IOSDevice.installApp calls ios-deploy correctly with USB', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: fileSystem.currentDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: <String>[ iosDeployPath, '--id', '1234', '--bundle', '/', '--no-wifi', ], environment: const <String, String>{ 'PATH': '/usr/bin:null', ...kDyLdLibEntry, }, ), ]); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, interfaceType: DeviceConnectionInterface.attached, artifacts: artifacts, ); final bool wasInstalled = await device.installApp(iosApp); expect(wasInstalled, true); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('IOSDevice.installApp calls ios-deploy correctly with network', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: fileSystem.currentDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: <String>[ iosDeployPath, '--id', '1234', '--bundle', '/', ], environment: const <String, String>{ 'PATH': '/usr/bin:null', ...kDyLdLibEntry, }, ), ]); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, interfaceType: DeviceConnectionInterface.wireless, artifacts: artifacts, ); final bool wasInstalled = await device.installApp(iosApp); expect(wasInstalled, true); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('IOSDevice.installApp uses devicectl for CoreDevices', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: fileSystem.currentDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.empty(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, interfaceType: DeviceConnectionInterface.attached, artifacts: artifacts, isCoreDevice: true, ); final bool wasInstalled = await device.installApp(iosApp); expect(wasInstalled, true); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('IOSDevice.uninstallApp calls ios-deploy correctly', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: bundleDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: <String>[ iosDeployPath, '--id', '1234', '--uninstall_only', '--bundle_id', 'app', ], environment: const <String, String>{ 'PATH': '/usr/bin:null', ...kDyLdLibEntry, }, ), ]); final IOSDevice device = setUpIOSDevice(processManager: processManager, artifacts: artifacts); final bool wasUninstalled = await device.uninstallApp(iosApp); expect(wasUninstalled, true); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('IOSDevice.uninstallApp uses devicectl for CoreDevices', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: fileSystem.currentDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.empty(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, interfaceType: DeviceConnectionInterface.attached, artifacts: artifacts, isCoreDevice: true, ); final bool wasUninstalled = await device.uninstallApp(iosApp); expect(wasUninstalled, true); expect(processManager, hasNoRemainingExpectations); }); group('isAppInstalled', () { testWithoutContext('catches ProcessException from ios-deploy', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: bundleDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand(command: <String>[ iosDeployPath, '--id', '1234', '--exists', '--timeout', '10', '--bundle_id', 'app', ], environment: const <String, String>{ 'PATH': '/usr/bin:null', ...kDyLdLibEntry, }, exception: const ProcessException('ios-deploy', <String>[])), ]); final IOSDevice device = setUpIOSDevice(processManager: processManager, artifacts: artifacts); final bool isAppInstalled = await device.isAppInstalled(iosApp); expect(isAppInstalled, false); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('returns true when app is installed', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: bundleDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: <String>[ iosDeployPath, '--id', '1234', '--exists', '--timeout', '10', '--bundle_id', 'app', ], environment: const <String, String>{ 'PATH': '/usr/bin:null', ...kDyLdLibEntry, }, ), ]); final IOSDevice device = setUpIOSDevice(processManager: processManager, artifacts: artifacts); final bool isAppInstalled = await device.isAppInstalled(iosApp); expect(isAppInstalled, isTrue); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('returns false when app is not installed', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: bundleDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: <String>[ iosDeployPath, '--id', '1234', '--exists', '--timeout', '10', '--bundle_id', 'app', ], environment: const <String, String>{ 'PATH': '/usr/bin:null', ...kDyLdLibEntry, }, exitCode: 255, ), ]); final BufferLogger logger = BufferLogger.test(); final IOSDevice device = setUpIOSDevice(processManager: processManager, logger: logger, artifacts: artifacts); final bool isAppInstalled = await device.isAppInstalled(iosApp); expect(isAppInstalled, isFalse); expect(processManager, hasNoRemainingExpectations); expect(logger.traceText, contains('${iosApp.id} not installed on ${device.id}')); }); testWithoutContext('returns false on command timeout or other error', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: bundleDirectory, applicationPackage: bundleDirectory, ); const String stderr = '2020-03-26 17:48:43.484 ios-deploy[21518:5501783] [ !! ] Timed out waiting for device'; final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: <String>[ iosDeployPath, '--id', '1234', '--exists', '--timeout', '10', '--bundle_id', 'app', ], environment: const <String, String>{ 'PATH': '/usr/bin:null', ...kDyLdLibEntry, }, stderr: stderr, exitCode: 253, ), ]); final BufferLogger logger = BufferLogger.test(); final IOSDevice device = setUpIOSDevice(processManager: processManager, logger: logger, artifacts: artifacts); final bool isAppInstalled = await device.isAppInstalled(iosApp); expect(isAppInstalled, isFalse); expect(processManager, hasNoRemainingExpectations); expect(logger.traceText, contains(stderr)); }); testWithoutContext('uses devicectl for CoreDevices', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: fileSystem.currentDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.empty(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, interfaceType: DeviceConnectionInterface.attached, artifacts: artifacts, isCoreDevice: true, ); final bool wasInstalled = await device.isAppInstalled(iosApp); expect(wasInstalled, true); expect(processManager, hasNoRemainingExpectations); }); }); testWithoutContext('IOSDevice.installApp catches ProcessException from ios-deploy', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: fileSystem.currentDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand(command: <String>[ iosDeployPath, '--id', '1234', '--bundle', '/', '--no-wifi', ], environment: const <String, String>{ 'PATH': '/usr/bin:null', ...kDyLdLibEntry, }, exception: const ProcessException('ios-deploy', <String>[])), ]); final IOSDevice device = setUpIOSDevice(processManager: processManager, artifacts: artifacts); final bool wasAppInstalled = await device.installApp(iosApp); expect(wasAppInstalled, false); }); testWithoutContext('IOSDevice.uninstallApp catches ProcessException from ios-deploy', () async { final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', uncompressedBundle: bundleDirectory, applicationPackage: bundleDirectory, ); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand(command: <String>[ iosDeployPath, '--id', '1234', '--uninstall_only', '--bundle_id', 'app', ], environment: const <String, String>{ 'PATH': '/usr/bin:null', ...kDyLdLibEntry, }, exception: const ProcessException('ios-deploy', <String>[])), ]); final IOSDevice device = setUpIOSDevice(processManager: processManager, artifacts: artifacts); final bool wasAppUninstalled = await device.uninstallApp(iosApp); expect(wasAppUninstalled, false); }); } IOSDevice setUpIOSDevice({ required ProcessManager processManager, FileSystem? fileSystem, Logger? logger, DeviceConnectionInterface? interfaceType, Artifacts? artifacts, bool isCoreDevice = false, }) { logger ??= BufferLogger.test(); final FakePlatform platform = FakePlatform( operatingSystem: 'macos', environment: <String, String>{}, ); artifacts ??= Artifacts.test(); final Cache cache = Cache.test( platform: platform, artifacts: <ArtifactSet>[ FakeDyldEnvironmentArtifact(), ], processManager: FakeProcessManager.any(), ); return IOSDevice( '1234', name: 'iPhone 1', logger: logger, fileSystem: fileSystem ?? MemoryFileSystem.test(), sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64, platform: platform, iMobileDevice: IMobileDevice( logger: logger, processManager: processManager, artifacts: artifacts, cache: cache, ), iosDeploy: IOSDeploy( logger: logger, platform: platform, processManager: processManager, artifacts: artifacts, cache: cache, ), coreDeviceControl: FakeIOSCoreDeviceControl(), xcodeDebug: FakeXcodeDebug(), iProxy: IProxy.test(logger: logger, processManager: processManager), connectionInterface: interfaceType ?? DeviceConnectionInterface.attached, isConnected: true, devModeEnabled: true, isCoreDevice: isCoreDevice, ); } class FakeXcodeDebug extends Fake implements XcodeDebug {} class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { @override Future<bool> installApp({ required String deviceId, required String bundlePath, }) async { return true; } @override Future<bool> uninstallApp({ required String deviceId, required String bundleId, }) async { return true; } @override Future<bool> isAppInstalled({ required String deviceId, required String bundleId, }) async { return true; } }