Remove usage of ideviceinstaller in favor of ios-deploy (#50772)
diff --git a/dev/devicelab/lib/framework/adb.dart b/dev/devicelab/lib/framework/adb.dart index 9ca6f86..880b850 100644 --- a/dev/devicelab/lib/framework/adb.dart +++ b/dev/devicelab/lib/framework/adb.dart
@@ -498,9 +498,7 @@ 'usbmuxd', 'libplist', 'openssl', - 'ideviceinstaller', 'ios-deploy', - 'libzip', ].map((String packageName) => path.join(getArtifactPath(), packageName)).join(':'); return <String, String>{'DYLD_LIBRARY_PATH': libPath}; }
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index cc50882..bfbde09 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart
@@ -1261,9 +1261,7 @@ 'usbmuxd', 'libplist', 'openssl', - 'ideviceinstaller', 'ios-deploy', - 'libzip', ]; // For unknown reasons, users are getting into bad states where libimobiledevice is
diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart index a90b9a9..b196542 100644 --- a/packages/flutter_tools/lib/src/commands/install.dart +++ b/packages/flutter_tools/lib/src/commands/install.dart
@@ -6,6 +6,7 @@ import '../application_package.dart'; import '../base/common.dart'; +import '../base/io.dart'; import '../cache.dart'; import '../device.dart'; import '../globals.dart' as globals; @@ -54,11 +55,15 @@ return false; } - if (uninstall && await device.isAppInstalled(package)) { - globals.printStatus('Uninstalling old version...'); - if (!await device.uninstallApp(package)) { - globals.printError('Warning: uninstalling old version failed'); + try { + if (uninstall && await device.isAppInstalled(package)) { + globals.printStatus('Uninstalling old version...'); + if (!await device.uninstallApp(package)) { + globals.printError('Warning: uninstalling old version failed'); + } } + } on ProcessException catch (e) { + globals.printError('Error accessing device ${device.id}:\n${e.message}'); } return device.installApp(package);
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index f159906..922f45c 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -35,7 +35,7 @@ import 'fuchsia/fuchsia_sdk.dart' show FuchsiaSdk, FuchsiaArtifacts; import 'fuchsia/fuchsia_workflow.dart' show FuchsiaWorkflow; import 'globals.dart' as globals; -import 'ios/devices.dart' show IOSDeploy; +import 'ios/ios_deploy.dart'; import 'ios/ios_workflow.dart'; import 'ios/mac.dart'; import 'ios/simulators.dart'; @@ -125,7 +125,13 @@ GradleUtils: () => GradleUtils(), HotRunnerConfig: () => HotRunnerConfig(), IMobileDevice: () => IMobileDevice(), - IOSDeploy: () => const IOSDeploy(), + IOSDeploy: () => IOSDeploy( + artifacts: globals.artifacts, + cache: globals.cache, + logger: globals.logger, + platform: globals.platform, + processManager: globals.processManager, + ), IOSSimulatorUtils: () => IOSSimulatorUtils(), IOSWorkflow: () => const IOSWorkflow(), KernelCompilerFactory: () => const KernelCompilerFactory(),
diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart index bea01e8..68cda51 100644 --- a/packages/flutter_tools/lib/src/globals.dart +++ b/packages/flutter_tools/lib/src/globals.dart
@@ -20,6 +20,7 @@ import 'base/terminal.dart'; import 'base/user_messages.dart'; import 'cache.dart'; +import 'ios/ios_deploy.dart'; import 'ios/mac.dart'; import 'macos/xcode.dart'; import 'persistent_tool_state.dart'; @@ -62,6 +63,7 @@ AndroidSdk get androidSdk => context.get<AndroidSdk>(); FlutterVersion get flutterVersion => context.get<FlutterVersion>(); IMobileDevice get iMobileDevice => context.get<IMobileDevice>(); +IOSDeploy get iosDeploy => context.get<IOSDeploy>(); UserMessages get userMessages => context.get<UserMessages>(); Xcode get xcode => context.get<Xcode>();
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index e3bbae8..6cdea04 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -11,7 +11,6 @@ import '../application_package.dart'; import '../artifacts.dart'; import '../base/common.dart'; -import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; @@ -25,85 +24,11 @@ import '../project.dart'; import '../protocol_discovery.dart'; import '../vmservice.dart'; -import 'code_signing.dart'; import 'fallback_discovery.dart'; +import 'ios_deploy.dart'; import 'ios_workflow.dart'; import 'mac.dart'; -class IOSDeploy { - const IOSDeploy(); - - static IOSDeploy get instance => context.get<IOSDeploy>(); - - /// Installs and runs the specified app bundle using ios-deploy, then returns - /// the exit code. - Future<int> runApp({ - @required String deviceId, - @required String bundlePath, - @required List<String> launchArguments, - }) async { - final String iosDeployPath = globals.artifacts.getArtifactPath(Artifact.iosDeploy, platform: TargetPlatform.ios); - final List<String> launchCommand = <String>[ - iosDeployPath, - '--id', - deviceId, - '--bundle', - bundlePath, - '--no-wifi', - '--justlaunch', - if (launchArguments.isNotEmpty) ...<String>[ - '--args', - launchArguments.join(' '), - ], - ]; - - // Push /usr/bin to the front of PATH to pick up default system python, package 'six'. - // - // ios-deploy transitively depends on LLDB.framework, which invokes a - // Python script that uses package 'six'. LLDB.framework relies on the - // python at the front of the path, which may not include package 'six'. - // Ensure that we pick up the system install of python, which does include - // it. - final Map<String, String> iosDeployEnv = Map<String, String>.from(globals.platform.environment); - iosDeployEnv['PATH'] = '/usr/bin:${iosDeployEnv['PATH']}'; - iosDeployEnv.addEntries(<MapEntry<String, String>>[globals.cache.dyLdLibEntry]); - - return await processUtils.stream( - launchCommand, - mapFunction: _monitorInstallationFailure, - trace: true, - environment: iosDeployEnv, - ); - } - - // Maps stdout line stream. Must return original line. - String _monitorInstallationFailure(String stdout) { - // Installation issues. - if (stdout.contains('Error 0xe8008015') || stdout.contains('Error 0xe8000067')) { - globals.printError(noProvisioningProfileInstruction, emphasis: true); - - // Launch issues. - } else if (stdout.contains('e80000e2')) { - globals.printError(''' -═══════════════════════════════════════════════════════════════════════════════════ -Your device is locked. Unlock your device first before running. -═══════════════════════════════════════════════════════════════════════════════════''', - emphasis: true); - } else if (stdout.contains('Error 0xe8000022')) { - globals.printError(''' -═══════════════════════════════════════════════════════════════════════════════════ -Error launching app. Try launching from within Xcode via: - open ios/Runner.xcworkspace - -Your Xcode version may be too old for your iOS version. -═══════════════════════════════════════════════════════════════════════════════════''', - emphasis: true); - } - - return stdout; - } -} - class IOSDevices extends PollingDeviceDiscovery { IOSDevices() : super('iOS devices'); @@ -122,35 +47,38 @@ class IOSDevice extends Device { IOSDevice(String id, { + @required FileSystem fileSystem, @required this.name, @required this.cpuArchitecture, @required String sdkVersion, + @required Platform platform, + @required Artifacts artifacts, + @required IOSDeploy iosDeploy, }) : _sdkVersion = sdkVersion, + _iosDeploy = iosDeploy, + _fileSystem = fileSystem, super( id, category: Category.mobile, platformType: PlatformType.ios, ephemeral: true, ) { - if (!globals.platform.isMacOS) { + if (!platform.isMacOS) { assert(false, 'Control of iOS devices or simulators only supported on Mac OS.'); return; } - _installerPath = globals.artifacts.getArtifactPath( - Artifact.ideviceinstaller, - platform: TargetPlatform.ios, - ); - _iproxyPath = globals.artifacts.getArtifactPath( + _iproxyPath = artifacts.getArtifactPath( Artifact.iproxy, platform: TargetPlatform.ios, ); } - String _installerPath; String _iproxyPath; final String _sdkVersion; + final IOSDeploy _iosDeploy; + final FileSystem _fileSystem; /// May be 0 if version cannot be parsed. int get majorSdkVersion { @@ -200,19 +128,17 @@ @override Future<bool> isAppInstalled(IOSApp app) async { - RunResult apps; + bool result; try { - apps = await processUtils.run( - <String>[_installerPath, '--list-apps'], - throwOnError: true, - environment: Map<String, String>.fromEntries( - <MapEntry<String, String>>[globals.cache.dyLdLibEntry], - ), + result = await _iosDeploy.isAppInstalled( + bundleId: app.id, + deviceId: id, ); - } on ProcessException { + } on ProcessException catch (e) { + globals.printError(e.message); return false; } - return RegExp(app.id, multiLine: true).hasMatch(apps.stdout); + return result; } @override @@ -220,42 +146,50 @@ @override Future<bool> installApp(IOSApp app) async { - final Directory bundle = globals.fs.directory(app.deviceBundlePath); + final Directory bundle = _fileSystem.directory(app.deviceBundlePath); if (!bundle.existsSync()) { globals.printError('Could not find application bundle at ${bundle.path}; have you run "flutter build ios"?'); return false; } + int installationResult; try { - await processUtils.run( - <String>[_installerPath, '-i', app.deviceBundlePath], - throwOnError: true, - environment: Map<String, String>.fromEntries( - <MapEntry<String, String>>[globals.cache.dyLdLibEntry], - ), + installationResult = await _iosDeploy.installApp( + deviceId: id, + bundlePath: bundle.path, + launchArguments: <String>[], ); - return true; - } on ProcessException catch (error) { - globals.printError(error.message); + } on ProcessException catch (e) { + globals.printError(e.message); return false; } + if (installationResult != 0) { + globals.printError('Could not install ${bundle.path} on $id.'); + globals.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:'); + globals.printError(' open ios/Runner.xcworkspace'); + globals.printError(''); + return false; + } + return true; } @override Future<bool> uninstallApp(IOSApp app) async { + int uninstallationResult; try { - await processUtils.run( - <String>[_installerPath, '-U', app.id], - throwOnError: true, - environment: Map<String, String>.fromEntries( - <MapEntry<String, String>>[globals.cache.dyLdLibEntry], - ), + uninstallationResult = await _iosDeploy.uninstallApp( + deviceId: id, + bundleId: app.id, ); - return true; - } on ProcessException catch (error) { - globals.printError(error.message); + } on ProcessException catch (e) { + globals.printError(e.message); return false; } + if (uninstallationResult != 0) { + globals.printError('Could not uninstall ${app.id} on $id.'); + return false; + } + return true; } @override @@ -301,7 +235,7 @@ packageId ??= package.id; // Step 2: Check that the application exists at the specified path. - final Directory bundle = globals.fs.directory(package.deviceBundlePath); + final Directory bundle = _fileSystem.directory(package.deviceBundlePath); if (!bundle.existsSync()) { globals.printError('Could not find the built application bundle at ${bundle.path}.'); return LaunchResult.failed(); @@ -359,13 +293,13 @@ ipv6: ipv6, ); } - final int installationResult = await IOSDeploy.instance.runApp( + final int installationResult = await _iosDeploy.runApp( deviceId: id, bundlePath: bundle.path, launchArguments: launchArguments, ); if (installationResult != 0) { - globals.printError('Could not install ${bundle.path} on $id.'); + globals.printError('Could not run ${bundle.path} on $id.'); globals.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:'); globals.printError(' open ios/Runner.xcworkspace'); globals.printError(''); @@ -395,6 +329,9 @@ return LaunchResult.failed(); } return LaunchResult.succeeded(observatoryUri: localUri); + } on ProcessException catch (e) { + globals.printError(e.message); + return LaunchResult.failed(); } finally { installStatus.stop(); } @@ -450,7 +387,7 @@ @override Future<void> dispose() async { - _logReaders.forEach((IOSApp application, DeviceLogReader logReader) { + _logReaders?.forEach((IOSApp application, DeviceLogReader logReader) { logReader.dispose(); }); await _portForwarder?.dispose();
diff --git a/packages/flutter_tools/lib/src/ios/ios_deploy.dart b/packages/flutter_tools/lib/src/ios/ios_deploy.dart new file mode 100644 index 0000000..de0fe59 --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/ios_deploy.dart
@@ -0,0 +1,187 @@ +// 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:meta/meta.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + +import '../artifacts.dart'; +import '../base/logger.dart'; +import '../base/process.dart'; +import '../build_info.dart'; +import '../cache.dart'; +import 'code_signing.dart'; + +// Error message patterns from ios-deploy output +const String noProvisioningProfileErrorOne = 'Error 0xe8008015'; +const String noProvisioningProfileErrorTwo = 'Error 0xe8000067'; +const String deviceLockedError = 'e80000e2'; +const String unknownAppLaunchError = 'Error 0xe8000022'; + +class IOSDeploy { + IOSDeploy({ + @required Artifacts artifacts, + @required Cache cache, + @required Logger logger, + @required Platform platform, + @required ProcessManager processManager, + }) : _platform = platform, + _cache = cache, + _processUtils = ProcessUtils(processManager: processManager, logger: logger), + _logger = logger, + _binaryPath = artifacts.getArtifactPath(Artifact.iosDeploy, platform: TargetPlatform.ios); + + final Cache _cache; + final String _binaryPath; + final Logger _logger; + final Platform _platform; + final ProcessUtils _processUtils; + + Map<String, String> get iosDeployEnv { + // Push /usr/bin to the front of PATH to pick up default system python, package 'six'. + // + // ios-deploy transitively depends on LLDB.framework, which invokes a + // Python script that uses package 'six'. LLDB.framework relies on the + // python at the front of the path, which may not include package 'six'. + // Ensure that we pick up the system install of python, which includes it. + final Map<String, String> environment = Map<String, String>.from(_platform.environment); + environment['PATH'] = '/usr/bin:${environment['PATH']}'; + environment.addEntries(<MapEntry<String, String>>[_cache.dyLdLibEntry]); + return environment; + } + + /// Uninstalls the specified app bundle. + /// + /// Uses ios-deploy and returns the exit code. + Future<int> uninstallApp({ + @required String deviceId, + @required String bundleId, + }) async { + final List<String> launchCommand = <String>[ + _binaryPath, + '--id', + deviceId, + '--uninstall_only', + '--bundle_id', + bundleId, + ]; + + return _processUtils.stream( + launchCommand, + mapFunction: _monitorFailure, + trace: true, + environment: iosDeployEnv, + ); + } + + /// Installs the specified app bundle. + /// + /// Uses ios-deploy and returns the exit code. + Future<int> installApp({ + @required String deviceId, + @required String bundlePath, + @required List<String>launchArguments, + }) async { + final List<String> launchCommand = <String>[ + _binaryPath, + '--id', + deviceId, + '--bundle', + bundlePath, + '--no-wifi', + if (launchArguments.isNotEmpty) ...<String>[ + '--args', + launchArguments.join(' '), + ], + ]; + + return _processUtils.stream( + launchCommand, + mapFunction: _monitorFailure, + trace: true, + environment: iosDeployEnv, + ); + } + + /// Installs and then runs the specified app bundle. + /// + /// Uses ios-deploy and returns the exit code. + Future<int> runApp({ + @required String deviceId, + @required String bundlePath, + @required List<String> launchArguments, + }) async { + final List<String> launchCommand = <String>[ + _binaryPath, + '--id', + deviceId, + '--bundle', + bundlePath, + '--no-wifi', + '--justlaunch', + if (launchArguments.isNotEmpty) ...<String>[ + '--args', + launchArguments.join(' '), + ], + ]; + + return _processUtils.stream( + launchCommand, + mapFunction: _monitorFailure, + trace: true, + environment: iosDeployEnv, + ); + } + + Future<bool> isAppInstalled({ + @required String bundleId, + @required String deviceId, + }) async { + final List<String> launchCommand = <String>[ + _binaryPath, + '--id', + deviceId, + '--exists', + '--bundle_id', + bundleId, + ]; + final RunResult result = await _processUtils.run( + launchCommand, + environment: iosDeployEnv, + ); + if (result.exitCode != 0) { + return false; + } + return result.stdout.contains(bundleId); + } + + // Maps stdout line stream. Must return original line. + String _monitorFailure(String stdout) { + // Installation issues. + if (stdout.contains(noProvisioningProfileErrorOne) || stdout.contains(noProvisioningProfileErrorTwo)) { + _logger.printError(noProvisioningProfileInstruction, emphasis: true); + + // Launch issues. + } else if (stdout.contains(deviceLockedError)) { + _logger.printError(''' +═══════════════════════════════════════════════════════════════════════════════════ +Your device is locked. Unlock your device first before running. +═══════════════════════════════════════════════════════════════════════════════════''', + emphasis: true); + } else if (stdout.contains(unknownAppLaunchError)) { + _logger.printError(''' +═══════════════════════════════════════════════════════════════════════════════════ +Error launching app. Try launching from within Xcode via: + open ios/Runner.xcworkspace + +Your Xcode version may be too old for your iOS version. +═══════════════════════════════════════════════════════════════════════════════════''', + emphasis: true); + } + + return stdout; + } +}
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index c4dcd78..6e4971a 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -91,7 +91,6 @@ return XcodeBuildResult(success: false); } - final XcodeProjectInfo projectInfo = await xcodeProjectInterpreter.getInfo(app.project.hostAppRoot.path); if (!projectInfo.targets.contains('Runner')) { globals.printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');
diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index 81d2656..55d22dc 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart
@@ -15,6 +15,7 @@ import '../base/process.dart'; import '../build_info.dart'; import '../convert.dart'; +import '../globals.dart' as globals; import '../ios/devices.dart'; import '../ios/xcodeproj.dart'; import '../reporting/reporting.dart'; @@ -349,6 +350,10 @@ name: device['name'] as String, cpuArchitecture: _cpuArchitecture(deviceProperties), sdkVersion: _sdkVersion(deviceProperties), + artifacts: globals.artifacts, + fileSystem: globals.fs, + iosDeploy: globals.iosDeploy, + platform: globals.platform, )); } return devices;
diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index b30b9ee..6ec864d 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
@@ -12,6 +12,7 @@ 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/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/create.dart'; @@ -19,6 +20,7 @@ import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/mac.dart'; +import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/ios_workflow.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:flutter_tools/src/mdns_discovery.dart'; @@ -40,19 +42,23 @@ class MockApplicationPackage extends Mock implements ApplicationPackage {} class MockArtifacts extends Mock implements Artifacts {} class MockCache extends Mock implements Cache {} +class MockDevicePortForwarder extends Mock implements DevicePortForwarder {} class MockDirectory extends Mock implements Directory {} +class MockFile extends Mock implements File {} class MockFileSystem extends Mock implements FileSystem {} class MockForwardedPort extends Mock implements ForwardedPort {} class MockIMobileDevice extends Mock implements IMobileDevice {} class MockIOSDeploy extends Mock implements IOSDeploy {} -class MockDevicePortForwarder extends Mock implements DevicePortForwarder {} +class MockLogger extends Mock implements Logger {} class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {} class MockMDnsObservatoryDiscoveryResult extends Mock implements MDnsObservatoryDiscoveryResult {} -class MockXcode extends Mock implements Xcode {} -class MockFile extends Mock implements File {} +class MockPlatform extends Mock implements Platform {} class MockPortForwarder extends Mock implements DevicePortForwarder {} +// src/mocks.dart imports `MockProcessManager` which implements some methods, this is a full mock +class FullMockProcessManager extends Mock implements ProcessManager {} class MockUsage extends Mock implements Usage {} class MockXcdevice extends Mock implements XCDevice {} +class MockXcode extends Mock implements Xcode {} void main() { final FakePlatform macPlatform = FakePlatform.fromPlatform(const LocalPlatform()); @@ -64,34 +70,187 @@ group('IOSDevice', () { final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform]; + Artifacts mockArtifacts; + MockCache mockCache; + MockLogger mockLogger; + IOSDeploy iosDeploy; + FileSystem mockFileSystem; - testUsingContext('successfully instantiates on Mac OS', () { - IOSDevice('device-123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); - }, overrides: <Type, Generator>{ - Platform: () => macPlatform, + setUp(() { + mockArtifacts = MockArtifacts(); + mockCache = MockCache(); + const MapEntry<String, String> dyLdLibEntry = MapEntry<String, String>('DYLD_LIBRARY_PATH', '/path/to/libs'); + when(mockCache.dyLdLibEntry).thenReturn(dyLdLibEntry); + mockFileSystem = MockFileSystem(); + mockLogger = MockLogger(); + iosDeploy = IOSDeploy( + artifacts: mockArtifacts, + cache: mockCache, + logger: mockLogger, + platform: macPlatform, + processManager: FakeProcessManager.any(), + ); }); - testUsingContext('parses major version', () { - expect(IOSDevice('device-123', name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, sdkVersion: '1.0.0').majorSdkVersion, 1); - expect(IOSDevice('device-123', name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, sdkVersion: '13.1.1').majorSdkVersion, 13); - expect(IOSDevice('device-123', name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, sdkVersion: '10').majorSdkVersion, 10); - expect(IOSDevice('device-123', name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, sdkVersion: '0').majorSdkVersion, 0); - expect(IOSDevice('device-123', name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, sdkVersion: 'bogus').majorSdkVersion, 0); - }, overrides: <Type, Generator>{ - Platform: () => macPlatform, + testWithoutContext('successfully instantiates on Mac OS', () { + IOSDevice( + 'device-123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64 + ); + }); + + testWithoutContext('parses major version', () { + expect(IOSDevice( + 'device-123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + cpuArchitecture: DarwinArch.arm64, + sdkVersion: '1.0.0' + ).majorSdkVersion, 1); + expect(IOSDevice( + 'device-123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + cpuArchitecture: DarwinArch.arm64, + sdkVersion: '13.1.1' + ).majorSdkVersion, 13); + expect(IOSDevice( + 'device-123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + cpuArchitecture: DarwinArch.arm64, + sdkVersion: '10' + ).majorSdkVersion, 10); + expect(IOSDevice( + 'device-123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + cpuArchitecture: DarwinArch.arm64, + sdkVersion: '0' + ).majorSdkVersion, 0); + expect(IOSDevice( + 'device-123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + cpuArchitecture: DarwinArch.arm64, + sdkVersion: 'bogus' + ).majorSdkVersion, 0); }); for (final Platform platform in unsupportedPlatforms) { - testUsingContext('throws UnsupportedError exception if instantiated on ${platform.operatingSystem}', () { + testWithoutContext('throws UnsupportedError exception if instantiated on ${platform.operatingSystem}', () { expect( - () { IOSDevice('device-123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); }, + () { + IOSDevice( + 'device-123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: platform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + ); + }, throwsAssertionError, ); - }, overrides: <Type, Generator>{ - Platform: () => platform, }); } + group('ios-deploy wrappers', () { + const String appId = '789'; + IOSDevice device; + IOSDeploy iosDeploy; + FullMockProcessManager mockProcessManager; + + setUp(() { + mockProcessManager = FullMockProcessManager(); + iosDeploy = IOSDeploy( + artifacts: mockArtifacts, + cache: mockCache, + logger: mockLogger, + platform: macPlatform, + processManager: mockProcessManager, + ); + + device = IOSDevice( + 'device-123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + ); + }); + + testUsingContext('isAppInstalled() catches ProcessException from ios-deploy', () async { + final MockIOSApp mockApp = MockIOSApp(); + when(mockApp.id).thenReturn(appId); + when(mockProcessManager.run( + any, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).thenThrow(const ProcessException('ios-deploy', <String>[])); + + final bool result = await device.isAppInstalled(mockApp); + expect(result, false); + }); + + testUsingContext('installApp() catches ProcessException from ios-deploy', () async { + const String bundlePath = '/path/to/bundle'; + final MockIOSApp mockApp = MockIOSApp(); + when(mockApp.id).thenReturn(appId); + when(mockApp.deviceBundlePath).thenReturn(bundlePath); + final MockDirectory mockDirectory = MockDirectory(); + when(mockFileSystem.directory(bundlePath)).thenReturn(mockDirectory); + when(mockDirectory.existsSync()).thenReturn(true); + when(mockProcessManager.start( + any, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).thenThrow(const ProcessException('ios-deploy', <String>[])); + + final bool result = await device.installApp(mockApp); + expect(result, false); + }); + + testUsingContext('uninstallApp() catches ProcessException from ios-deploy', () async { + final MockIOSApp mockApp = MockIOSApp(); + when(mockApp.id).thenReturn(appId); + when(mockProcessManager.start( + any, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).thenThrow(const ProcessException('ios-deploy', <String>[])); + + final bool result = await device.uninstallApp(mockApp); + expect(result, false); + }); + }); + group('.dispose()', () { IOSDevice device; MockIOSApp appPackage1; @@ -103,6 +262,11 @@ MockProcess mockProcess3; IOSDevicePortForwarder portForwarder; ForwardedPort forwardedPort; + Artifacts mockArtifacts; + MockCache mockCache; + MockLogger mockLogger; + IOSDeploy iosDeploy; + FileSystem mockFileSystem; IOSDevicePortForwarder createPortForwarder( ForwardedPort forwardedPort, @@ -130,10 +294,29 @@ mockProcess2 = MockProcess(); mockProcess3 = MockProcess(); forwardedPort = ForwardedPort.withContext(123, 456, mockProcess3); + mockArtifacts = MockArtifacts(); + mockCache = MockCache(); + mockFileSystem = MockFileSystem(); + iosDeploy = IOSDeploy( + artifacts: mockArtifacts, + cache: mockCache, + logger: mockLogger, + platform: macPlatform, + processManager: FakeProcessManager.any(), + ); }); - testUsingContext(' kills all log readers & port forwarders', () async { - device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + testWithoutContext('kills all log readers & port forwarders', () async { + device = IOSDevice( + '123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + ); logReader1 = createLogReader(device, appPackage1, mockProcess1); logReader2 = createLogReader(device, appPackage2, mockProcess2); portForwarder = createPortForwarder(forwardedPort, device); @@ -146,8 +329,6 @@ verify(mockProcess1.kill()); verify(mockProcess2.kill()); verify(mockProcess3.kill()); - }, overrides: <Type, Generator>{ - Platform: () => macPlatform, }); }); @@ -156,6 +337,7 @@ MockArtifacts mockArtifacts; MockCache mockCache; MockFileSystem mockFileSystem; + MockPlatform mockPlatform; MockProcessManager mockProcessManager; MockDeviceLogReader mockLogReader; MockMDnsObservatoryDiscovery mockMDnsObservatoryDiscovery; @@ -188,6 +370,8 @@ mockCache = MockCache(); when(mockCache.dyLdLibEntry).thenReturn(libraryEntry); mockFileSystem = MockFileSystem(); + mockPlatform = MockPlatform(); + when(mockPlatform.isMacOS).thenReturn(true); mockMDnsObservatoryDiscovery = MockMDnsObservatoryDiscovery(); mockProcessManager = MockProcessManager(); mockLogReader = MockDeviceLogReader(); @@ -254,17 +438,43 @@ }); testUsingContext('disposing device disposes the portForwarder', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice( + '123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + ); device.portForwarder = mockPortForwarder; device.setLogReader(mockApp, mockLogReader); await device.dispose(); verify(mockPortForwarder.dispose()).called(1); - }, overrides: <Type, Generator>{ - Platform: () => macPlatform, }); - testUsingContext(' succeeds in debug mode via mDNS', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + testUsingContext('succeeds in debug mode via mDNS', () async { + final IOSDevice device = IOSDevice( + '123', + name: 'iPhone 1', + sdkVersion: '13.3', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: mockIosDeploy, + cpuArchitecture: DarwinArch.arm64, + ); + when(mockIosDeploy.installApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: <String>[], + )).thenAnswer((Invocation invocation) => Future<int>.value(0)); + when(mockIosDeploy.runApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: anyNamed('launchArguments'), + )).thenAnswer((Invocation invocation) => Future<int>.value(0)); device.portForwarder = mockPortForwarder; device.setLogReader(mockApp, mockLogReader); final Uri uri = Uri( @@ -298,10 +508,19 @@ // By default, the .forward() method will try every port between 1024 // and 65535; this test verifies we are killing iproxy processes when // we timeout on a port - testUsingContext(' .forward() will kill iproxy processes before invoking a second', () async { + testUsingContext('.forward() will kill iproxy processes before invoking a second', () async { const String deviceId = '123'; const int devicePort = 456; - final IOSDevice device = IOSDevice(deviceId, name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice( + deviceId, + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: iosDeploy, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + ); final IOSDevicePortForwarder portForwarder = IOSDevicePortForwarder(device); bool firstRun = true; final MockProcess successProcess = MockProcess( @@ -334,8 +553,25 @@ Usage: () => mockUsage, }); - testUsingContext(' succeeds in debug mode when mDNS fails by falling back to manual protocol discovery', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + testUsingContext('succeeds in debug mode when mDNS fails by falling back to manual protocol discovery', () async { + final IOSDevice device = IOSDevice( + '123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: mockIosDeploy, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + ); + when( + mockIosDeploy.installApp(deviceId: device.id, bundlePath: anyNamed('bundlePath'), launchArguments: <String>[]) + ).thenAnswer((Invocation invocation) => Future<int>.value(0)); + when(mockIosDeploy.runApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: anyNamed('launchArguments'), + )).thenAnswer((Invocation invocation) => Future<int>.value(0)); device.portForwarder = mockPortForwarder; device.setLogReader(mockApp, mockLogReader); // Now that the reader is used, start writing messages to it. @@ -351,10 +587,10 @@ debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)), platformArgs: <String, dynamic>{}, ); - verify(mockUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1); - verify(mockUsage.sendEvent('ios-handshake', 'fallback-success')).called(1); expect(launchResult.started, isTrue); expect(launchResult.hasObservatory, isTrue); + verify(mockUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1); + verify(mockUsage.sendEvent('ios-handshake', 'fallback-success')).called(1); expect(await device.stopApp(mockApp), isFalse); }, overrides: <Type, Generator>{ Artifacts: () => mockArtifacts, @@ -366,8 +602,27 @@ Usage: () => mockUsage, }); - testUsingContext(' fails in debug mode when mDNS fails and when Observatory URI is malformed', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + testUsingContext('fails in debug mode when mDNS fails and when Observatory URI is malformed', () async { + final IOSDevice device = IOSDevice( + '123', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: mockIosDeploy, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + ); + when(mockIosDeploy.installApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: <String>[], + )).thenAnswer((Invocation invocation) => Future<int>.value(0)); + when(mockIosDeploy.runApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: anyNamed('launchArguments'), + )).thenAnswer((Invocation invocation) => Future<int>.value(0)); device.portForwarder = mockPortForwarder; device.setLogReader(mockApp, mockLogReader); @@ -384,6 +639,8 @@ debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)), platformArgs: <String, dynamic>{}, ); + expect(launchResult.started, isFalse); + expect(launchResult.hasObservatory, isFalse); verify(mockUsage.sendEvent( 'ios-handshake', 'failure', @@ -392,8 +649,6 @@ )).called(1); verify(mockUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1); verify(mockUsage.sendEvent('ios-handshake', 'fallback-failure')).called(1); - expect(launchResult.started, isFalse); - expect(launchResult.hasObservatory, isFalse); }, overrides: <Type, Generator>{ Artifacts: () => mockArtifacts, Cache: () => mockCache, @@ -405,25 +660,57 @@ }); testUsingContext('succeeds in release mode', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice( + '123', + name: 'iPhone 1', + fileSystem: mockFileSystem, + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + platform: mockPlatform, + artifacts: mockArtifacts, + iosDeploy: mockIosDeploy, + ); + when(mockIosDeploy.installApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: <String>[], + )).thenAnswer((Invocation invocation) => Future<int>.value(0)); + when(mockIosDeploy.runApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: anyNamed('launchArguments'), + )).thenAnswer((Invocation invocation) => Future<int>.value(0)); final LaunchResult launchResult = await device.startApp(mockApp, prebuiltApplication: true, debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null, treeShakeIcons: false)), platformArgs: <String, dynamic>{}, ); + verify(mockIosDeploy.installApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: <String>[], + )); + verify(mockIosDeploy.runApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: anyNamed('launchArguments'), + )); expect(launchResult.started, isTrue); expect(launchResult.hasObservatory, isFalse); expect(await device.stopApp(mockApp), isFalse); - }, overrides: <Type, Generator>{ - Artifacts: () => mockArtifacts, - Cache: () => mockCache, - FileSystem: () => mockFileSystem, - Platform: () => macPlatform, - ProcessManager: () => mockProcessManager, }); testUsingContext('succeeds with --cache-sksl', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice( + '123', + name: 'iPhone 1', + sdkVersion: '13.3', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: mockIosDeploy, + cpuArchitecture: DarwinArch.arm64, + ); device.setLogReader(mockApp, mockLogReader); final Uri uri = Uri( scheme: 'http', @@ -443,6 +730,11 @@ args = inv.namedArguments[const Symbol('launchArguments')] as List<String>; return Future<int>.value(0); }); + when(mockIosDeploy.installApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: anyNamed('launchArguments'), + )).thenAnswer((Invocation invocation) => Future<int>.value(0)); final LaunchResult launchResult = await device.startApp(mockApp, prebuiltApplication: true, @@ -467,7 +759,16 @@ }); testUsingContext('succeeds with --device-vmservice-port', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice( + '123', + name: 'iPhone 1', + sdkVersion: '13.3', + artifacts: mockArtifacts, + fileSystem: mockFileSystem, + platform: macPlatform, + iosDeploy: mockIosDeploy, + cpuArchitecture: DarwinArch.arm64, + ); device.setLogReader(mockApp, mockLogReader); final Uri uri = Uri( scheme: 'http', @@ -488,6 +789,11 @@ return Future<int>.value(0); }); + when(mockIosDeploy.installApp( + deviceId: device.id, + bundlePath: anyNamed('bundlePath'), + launchArguments: anyNamed('launchArguments'), + )).thenAnswer((Invocation invocation) => Future<int>.value(0)); final LaunchResult launchResult = await device.startApp(mockApp, prebuiltApplication: true, debuggingOptions: DebuggingOptions.enabled( @@ -581,7 +887,16 @@ final IOSApp app = await AbsoluteBuildableIOSApp.fromProject( FlutterProject.fromDirectory(projectDir).ios); - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice( + '123', + name: 'iPhone 1', + sdkVersion: '13.3', + artifacts: mockArtifacts, + fileSystem: globals.fs, + platform: macPlatform, + iosDeploy: mockIosDeploy, + cpuArchitecture: DarwinArch.arm64, + ); // Pre-create the expected build products. targetBuildDir.createSync(recursive: true); @@ -611,7 +926,6 @@ }, overrides: <Type, Generator>{ DoctorValidatorsProvider: () => FakeIosDoctorProvider(), IMobileDevice: () => mockIMobileDevice, - IOSDeploy: () => mockIosDeploy, Platform: () => macPlatform, ProcessManager: () => mockProcessManager, }); @@ -658,102 +972,154 @@ }); group('Process calls', () { + const String bundlePath = '/path/to/bundle'; + FileSystem fs; + MockDirectory directory; MockIOSApp mockApp; MockArtifacts mockArtifacts; MockCache mockCache; MockFileSystem mockFileSystem; - MockProcessManager mockProcessManager; - const String installerPath = '/path/to/ideviceinstaller'; + MockLogger mockLogger; + MockPlatform mockPlatform; + FullMockProcessManager mockProcessManager; + const String iosDeployPath = '/path/to/ios-deploy'; const String appId = '789'; const MapEntry<String, String> libraryEntry = MapEntry<String, String>( 'DYLD_LIBRARY_PATH', '/path/to/libraries', ); - final Map<String, String> env = Map<String, String>.fromEntries( - <MapEntry<String, String>>[libraryEntry] - ); + IOSDeploy iosDeploy; setUp(() { + mockFileSystem = MockFileSystem(); + directory = MockDirectory(); + when(mockFileSystem.directory(bundlePath)).thenReturn(directory); + mockApp = MockIOSApp(); + when(mockApp.id).thenReturn(appId); + when(mockApp.deviceBundlePath).thenReturn(bundlePath); + when(directory.existsSync()).thenReturn(true); + when(directory.path).thenReturn(bundlePath); + mockArtifacts = MockArtifacts(); mockCache = MockCache(); + mockLogger = MockLogger(); + mockPlatform = MockPlatform(); + when(mockPlatform.environment).thenReturn(<String, String>{}); + when(mockPlatform.isMacOS).thenReturn(true); + mockProcessManager = FullMockProcessManager(); + when( + mockArtifacts.getArtifactPath( + Artifact.iosDeploy, + platform: anyNamed('platform'), + ), + ).thenReturn(iosDeployPath); + iosDeploy = IOSDeploy( + artifacts: mockArtifacts, + cache: mockCache, + logger: mockLogger, + platform: mockPlatform, + processManager: mockProcessManager, + ); when(mockCache.dyLdLibEntry).thenReturn(libraryEntry); mockFileSystem = MockFileSystem(); final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); when(mockFileSystem.currentDirectory) .thenReturn(memoryFileSystem.currentDirectory); - mockProcessManager = MockProcessManager(); - when( - mockArtifacts.getArtifactPath( - Artifact.ideviceinstaller, - platform: anyNamed('platform'), - ), - ).thenReturn(installerPath); }); - testUsingContext('installApp() invokes process with correct environment', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); - const String bundlePath = '/path/to/bundle'; - final List<String> args = <String>[installerPath, '-i', bundlePath]; - when(mockApp.deviceBundlePath).thenReturn(bundlePath); - final MockDirectory directory = MockDirectory(); + testWithoutContext('installApp() calls ios-deploy', () async { when(mockFileSystem.directory(bundlePath)).thenReturn(directory); - when(directory.existsSync()).thenReturn(true); - when(mockProcessManager.run(args, environment: env)) - .thenAnswer( - (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', '')) - ); + final IOSDevice device = IOSDevice( + '123', + name: 'iPhone 1', + fileSystem: mockFileSystem, + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + platform: mockPlatform, + artifacts: mockArtifacts, + iosDeploy: iosDeploy, + ); + final List<String> args = <String>[ + iosDeployPath, + '--id', + device.id, + '--bundle', + bundlePath, + '--no-wifi', + ]; + when(mockProcessManager.start(any, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))). + thenAnswer((Invocation invocation) { + return Future<Process>.value(createMockProcess()); + }); + await device.installApp(mockApp); - verify(mockProcessManager.run(args, environment: env)); - }, overrides: <Type, Generator>{ - Artifacts: () => mockArtifacts, - Cache: () => mockCache, - FileSystem: () => mockFileSystem, - Platform: () => macPlatform, - ProcessManager: () => mockProcessManager, + + final List<String> invocationArguments = verify(mockProcessManager.start( + captureAny, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).captured.first as List<String>; + expect(invocationArguments, args); }); - testUsingContext('isAppInstalled() invokes process with correct environment', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); - final List<String> args = <String>[installerPath, '--list-apps']; - when(mockProcessManager.run(args, environment: env)) - .thenAnswer( - (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', '')) - ); - when(mockApp.id).thenReturn(appId); - await device.isAppInstalled(mockApp); - verify(mockProcessManager.run(args, environment: env)); - }, overrides: <Type, Generator>{ - Artifacts: () => mockArtifacts, - Cache: () => mockCache, - Platform: () => macPlatform, - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('uninstallApp() invokes process with correct environment', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); - final List<String> args = <String>[installerPath, '-U', appId]; - when(mockApp.id).thenReturn(appId); - when(mockProcessManager.run(args, environment: env)) - .thenAnswer( - (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', '')) - ); + testWithoutContext('uninstallApp() calls ios-deploy', () async { + final IOSDevice device = IOSDevice( + '123', + name: 'iPhone 1', + fileSystem: fs, + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + platform: mockPlatform, + artifacts: mockArtifacts, + iosDeploy: iosDeploy, + ); + final List<String> args = <String>[ + iosDeployPath, + '--id', + device.id, + '--uninstall_only', + '--bundle_id', + appId, + ]; + when(mockProcessManager.start(args, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))). + thenAnswer((Invocation invocation) { + return Future<Process>.value(createMockProcess()); + }); await device.uninstallApp(mockApp); - verify(mockProcessManager.run(args, environment: env)); - }, overrides: <Type, Generator>{ - Artifacts: () => mockArtifacts, - Cache: () => mockCache, - Platform: () => macPlatform, - ProcessManager: () => mockProcessManager, + final List<String> invocationArguments = verify(mockProcessManager.start( + captureAny, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).captured.first as List<String>; + expect(invocationArguments, args); }); }); }); group('getAttachedDevices', () { MockXcdevice mockXcdevice; + MockArtifacts mockArtifacts; + MockCache mockCache; + MockFileSystem mockFileSystem; + MockLogger mockLogger; + FullMockProcessManager mockProcessManager; + IOSDeploy iosDeploy; setUp(() { mockXcdevice = MockXcdevice(); + mockArtifacts = MockArtifacts(); + mockCache = MockCache(); + mockLogger = MockLogger(); + mockFileSystem = MockFileSystem(); + mockProcessManager = FullMockProcessManager(); + iosDeploy = IOSDeploy( + artifacts: mockArtifacts, + cache: mockCache, + logger: mockLogger, + platform: macPlatform, + processManager: mockProcessManager, + ); }); final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform]; @@ -767,17 +1133,25 @@ }); } - testUsingContext('returns attached devices', () async { + testWithoutContext('returns attached devices', () async { when(mockXcdevice.isInstalled).thenReturn(true); - final IOSDevice device = IOSDevice('d83d5bc53967baa0ee18626ba87b6254b2ab5418', name: 'Paired iPhone', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + + final IOSDevice device = IOSDevice( + 'd83d5bc53967baa0ee18626ba87b6254b2ab5418', + name: 'Paired iPhone', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + artifacts: mockArtifacts, + iosDeploy: iosDeploy, + platform: macPlatform, + fileSystem: mockFileSystem, + ); when(mockXcdevice.getAvailableTetheredIOSDevices()) .thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[device])); final List<IOSDevice> devices = await IOSDevice.getAttachedDevices(macPlatform, mockXcdevice); expect(devices, hasLength(1)); expect(identical(devices.first, device), isTrue); - }, overrides: <Type, Generator>{ - Platform: () => macPlatform, }); }); @@ -820,13 +1194,32 @@ expect(decoded, r'I \M-b\M^O syslog!'); }); }); + group('logging', () { MockIMobileDevice mockIMobileDevice; MockIosProject mockIosProject; + MockArtifacts mockArtifacts; + MockCache mockCache; + MockFileSystem mockFileSystem; + MockLogger mockLogger; + FullMockProcessManager mockProcessManager; + IOSDeploy iosDeploy; setUp(() { mockIMobileDevice = MockIMobileDevice(); mockIosProject = MockIosProject(); + mockArtifacts = MockArtifacts(); + mockCache = MockCache(); + mockLogger = MockLogger(); + mockFileSystem = MockFileSystem(); + mockProcessManager = FullMockProcessManager(); + iosDeploy = IOSDeploy( + artifacts: mockArtifacts, + cache: mockCache, + logger: mockLogger, + platform: macPlatform, + processManager: mockProcessManager, + ); }); testUsingContext('suppresses non-Flutter lines from output', () async { @@ -843,7 +1236,16 @@ return Future<Process>.value(mockProcess); }); - final IOSDevice device = IOSDevice('123456', name: 'iPhone 1', sdkVersion: '10.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice( + '123456', + name: 'iPhone 1', + sdkVersion: '10.3', + cpuArchitecture: DarwinArch.arm64, + artifacts: mockArtifacts, + iosDeploy: iosDeploy, + platform: macPlatform, + fileSystem: mockFileSystem, + ); final DeviceLogReader logReader = device.getLogReader( app: await BuildableIOSApp.fromProject(mockIosProject), ); @@ -869,7 +1271,16 @@ return Future<Process>.value(mockProcess); }); - final IOSDevice device = IOSDevice('123456', name: 'iPhone 1', sdkVersion: '10.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice( + '123456', + name: 'iPhone 1', + sdkVersion: '10.3', + cpuArchitecture: DarwinArch.arm64, + artifacts: mockArtifacts, + iosDeploy: iosDeploy, + platform: macPlatform, + fileSystem: mockFileSystem, + ); final DeviceLogReader logReader = device.getLogReader( app: await BuildableIOSApp.fromProject(mockIosProject), ); @@ -887,47 +1298,97 @@ Platform: () => macPlatform, }); }); - testUsingContext('IOSDevice.isSupportedForProject is true on module project', () async { - globals.fs.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(r''' -name: example -flutter: - module: {} -'''); - globals.fs.file('.packages').createSync(); - final FlutterProject flutterProject = FlutterProject.current(); + group('isSupportedForProject', () { + Artifacts mockArtifacts; + MockCache mockCache; + MockLogger mockLogger; + IOSDeploy iosDeploy; - expect(IOSDevice('test', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64).isSupportedForProject(flutterProject), true); - }, overrides: <Type, Generator>{ - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - Platform: () => macPlatform, - }); - testUsingContext('IOSDevice.isSupportedForProject is true with editable host app', () async { - globals.fs.file('pubspec.yaml').createSync(); - globals.fs.file('.packages').createSync(); - globals.fs.directory('ios').createSync(); - final FlutterProject flutterProject = FlutterProject.current(); + setUp(() { + mockArtifacts = MockArtifacts(); + mockCache = MockCache(); + iosDeploy = IOSDeploy( + artifacts: mockArtifacts, + cache: mockCache, + logger: mockLogger, + platform: macPlatform, + processManager: FakeProcessManager.any(), + ); + }); - expect(IOSDevice('test', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64).isSupportedForProject(flutterProject), true); - }, overrides: <Type, Generator>{ - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - Platform: () => macPlatform, - }); + testUsingContext('is true on module project', () async { + globals.fs.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' + name: example - testUsingContext('IOSDevice.isSupportedForProject is false with no host app and no module', () async { - globals.fs.file('pubspec.yaml').createSync(); - globals.fs.file('.packages').createSync(); - final FlutterProject flutterProject = FlutterProject.current(); + flutter: + module: {} + '''); + globals.fs.file('.packages').createSync(); + final FlutterProject flutterProject = FlutterProject.current(); - expect(IOSDevice('test', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64).isSupportedForProject(flutterProject), false); - }, overrides: <Type, Generator>{ - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - Platform: () => macPlatform, + final IOSDevice device = IOSDevice( + 'test', + artifacts: mockArtifacts, + fileSystem: globals.fs, + iosDeploy: iosDeploy, + platform: globals.platform, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64 + ); + expect(device.isSupportedForProject(flutterProject), true); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + Platform: () => macPlatform, + }); + + testUsingContext('is true with editable host app', () async { + globals.fs.file('pubspec.yaml').createSync(); + globals.fs.file('.packages').createSync(); + globals.fs.directory('ios').createSync(); + final FlutterProject flutterProject = FlutterProject.current(); + final IOSDevice device = IOSDevice( + 'test', + artifacts: mockArtifacts, + fileSystem: globals.fs, + iosDeploy: iosDeploy, + platform: globals.platform, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + ); + expect(device.isSupportedForProject(flutterProject), true); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + Platform: () => macPlatform, + }); + + testUsingContext('is false with no host app and no module', () async { + globals.fs.file('pubspec.yaml').createSync(); + globals.fs.file('.packages').createSync(); + final FlutterProject flutterProject = FlutterProject.current(); + + final IOSDevice device = IOSDevice( + 'test', + artifacts: mockArtifacts, + fileSystem: globals.fs, + iosDeploy: iosDeploy, + platform: globals.platform, + name: 'iPhone 1', + sdkVersion: '13.3', + cpuArchitecture: DarwinArch.arm64, + ); + expect(device.isSupportedForProject(flutterProject), false); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + Platform: () => macPlatform, + }); }); }
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart new file mode 100644 index 0000000..391f405 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart
@@ -0,0 +1,119 @@ +// 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:io' show Process; + +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/ios/ios_deploy.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/mocks.dart'; + +class MockArtifacts extends Mock implements Artifacts {} +class MockCache extends Mock implements Cache {} +class MockLogger extends Mock implements Logger {} +class MockPlatform extends Mock implements Platform {} +class MockProcess extends Mock implements Process {} +class MockProcessManager extends Mock implements ProcessManager {} + +void main () { + group('IOSDeploy()', () { + Artifacts mockArtifacts; + Cache mockCache; + IOSDeploy iosDeploy; + Logger mockLogger; + Platform mockPlatform; + ProcessManager mockProcessManager; + const String iosDeployPath = '/path/to/ios-deploy'; + const String deviceId = '123'; + const String bundleId = 'com.example.app'; + + setUp(() { + mockArtifacts = MockArtifacts(); + when(mockArtifacts.getArtifactPath(Artifact.iosDeploy, platform: TargetPlatform.ios)) + .thenReturn(iosDeployPath); + mockCache = MockCache(); + const MapEntry<String, String> mapEntry = MapEntry<String, String>('DYLD_LIBRARY_PATH', '/path/to/libs'); + when(mockCache.dyLdLibEntry).thenReturn(mapEntry); + mockLogger = MockLogger(); + mockPlatform = MockPlatform(); + when(mockPlatform.environment).thenReturn(<String, String>{ + 'PATH': '/usr/local/bin:/usr/bin', + }); + mockProcessManager = MockProcessManager(); + iosDeploy = IOSDeploy( + artifacts: mockArtifacts, + cache: mockCache, + logger: mockLogger, + platform: mockPlatform, + processManager: mockProcessManager, + ); + }); + + testWithoutContext('iosDeployEnv returns path with /usr/bin first', () { + final Map<String, String> env = iosDeploy.iosDeployEnv; + expect(env['PATH'].startsWith('/usr/bin'), true); + }); + + testWithoutContext('uninstallApp() calls ios-deploy with correct arguments and returns 0 on success', () async { + final List<String> args = <String>[ + iosDeployPath, + '--id', + deviceId, + '--uninstall_only', + '--bundle_id', + bundleId, + ]; + when(mockProcessManager.start( + args, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).thenAnswer((Invocation invocation) => Future<Process>.value(createMockProcess(exitCode: 0))); + final int exitCode = await iosDeploy.uninstallApp( + deviceId: deviceId, + bundleId: bundleId, + ); + + verify(mockProcessManager.start( + args, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )); + expect(exitCode, 0); + }); + + testWithoutContext('uninstallApp() returns non-zero exit code when ios-deploy does the same', () async { + final List<String> args = <String>[ + iosDeployPath, + '--id', + deviceId, + '--uninstall_only', + '--bundle_id', + bundleId, + ]; + when(mockProcessManager.start( + args, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).thenAnswer((Invocation invocation) => Future<Process>.value(createMockProcess(exitCode: 1))); + final int exitCode = await iosDeploy.uninstallApp( + deviceId: deviceId, + bundleId: bundleId, + ); + + verify(mockProcessManager.start( + args, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )); + expect(exitCode, 1); + }); + }); +}