diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index b6b0bf77b9b..3ac717b334e 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -166,11 +166,6 @@ Future runInContext( fileSystem: globals.fs, xcodeProjectInterpreter: xcodeProjectInterpreter, ), - XCDevice: () => XCDevice( - processManager: globals.processManager, - logger: globals.logger, - xcode: globals.xcode, - ), XcodeProjectInterpreter: () => XcodeProjectInterpreter( logger: globals.logger, processManager: globals.processManager, diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart index eadba0cbd6f..87245904cb0 100644 --- a/packages/flutter_tools/lib/src/globals.dart +++ b/packages/flutter_tools/lib/src/globals.dart @@ -57,8 +57,6 @@ Xcode get xcode => context.get(); FlutterVersion get flutterVersion => context.get(); IMobileDevice get iMobileDevice => context.get(); -XCDevice get xcdevice => context.get(); - /// Display an error level message to the user. Commands should use this if they /// fail in some way. /// diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index a31fca30883..4d59ca5ca67 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'package:platform/platform.dart'; import '../application_package.dart'; import '../artifacts.dart'; @@ -19,7 +18,6 @@ import '../build_info.dart'; import '../convert.dart'; import '../device.dart'; import '../globals.dart' as globals; -import '../macos/xcode.dart'; import '../mdns_discovery.dart'; import '../project.dart'; import '../protocol_discovery.dart'; @@ -113,18 +111,11 @@ class IOSDevices extends PollingDeviceDiscovery { bool get canListAnything => iosWorkflow.canListDevices; @override - Future> pollingGetDevices() => IOSDevice.getAttachedDevices(globals.platform, globals.xcdevice); - - @override - Future> getDiagnostics() => IOSDevice.getDiagnostics(globals.platform, globals.xcdevice); + Future> pollingGetDevices() => IOSDevice.getAttachedDevices(); } class IOSDevice extends Device { - IOSDevice(String id, { - @required this.name, - @required this.cpuArchitecture, - @required String sdkVersion, - }) + IOSDevice(String id, { this.name, String sdkVersion }) : _sdkVersion = sdkVersion, super( id, @@ -166,8 +157,6 @@ class IOSDevice extends Device { @override final String name; - final DarwinArch cpuArchitecture; - Map _logReaders; DevicePortForwarder _portForwarder; @@ -181,20 +170,34 @@ class IOSDevice extends Device { @override bool get supportsStartPaused => false; - static Future> getAttachedDevices(Platform platform, XCDevice xcdevice) async { - if (!platform.isMacOS) { - throw UnsupportedError('Control of iOS devices or simulators only supported on macOS.'); + static Future> getAttachedDevices() async { + if (!globals.platform.isMacOS) { + throw UnsupportedError('Control of iOS devices or simulators only supported on Mac OS.'); + } + if (!globals.iMobileDevice.isInstalled) { + return []; } - return await xcdevice.getAvailableTetheredIOSDevices(); - } + final List devices = []; + for (String id in (await globals.iMobileDevice.getAvailableDeviceIDs()).split('\n')) { + id = id.trim(); + if (id.isEmpty) { + continue; + } - static Future> getDiagnostics(Platform platform, XCDevice xcdevice) async { - if (!platform.isMacOS) { - return const ['Control of iOS devices or simulators only supported on macOS.']; + try { + final String deviceName = await globals.iMobileDevice.getInfoForDevice(id, 'DeviceName'); + final String sdkVersion = await globals.iMobileDevice.getInfoForDevice(id, 'ProductVersion'); + devices.add(IOSDevice(id, name: deviceName, sdkVersion: sdkVersion)); + } on IOSDeviceNotFoundError catch (error) { + // Unable to find device with given udid. Possibly a network device. + globals.printTrace('Error getting attached iOS device: $error'); + } on IOSDeviceNotTrustedError catch (error) { + globals.printTrace('Error getting attached iOS device information: $error'); + UsageEvent('device', 'ios-trust-failure').send(); + } } - - return await xcdevice.getDiagnostics(); + return devices; } @override @@ -277,13 +280,24 @@ class IOSDevice extends Device { // TODO(chinmaygarde): Use mainPath, route. globals.printTrace('Building ${package.name} for $id'); + String cpuArchitecture; + + try { + cpuArchitecture = await globals.iMobileDevice.getInfoForDevice(id, 'CPUArchitecture'); + } on IOSDeviceNotFoundError catch (e) { + globals.printError(e.message); + return LaunchResult.failed(); + } + + final DarwinArch iosArch = getIOSArchForName(cpuArchitecture); + // Step 1: Build the precompiled/DBC application if necessary. final XcodeBuildResult buildResult = await buildXcodeProject( app: package as BuildableIOSApp, buildInfo: debuggingOptions.buildInfo, targetOverride: mainPath, buildForDevice: true, - activeArch: cpuArchitecture, + activeArch: iosArch, ); if (!buildResult.success) { globals.printError('Could not build the precompiled application for the device.'); diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index be4a8df6be4..326a06f571d 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart @@ -13,11 +13,7 @@ import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/process.dart'; -import '../build_info.dart'; -import '../convert.dart'; -import '../ios/devices.dart'; import '../ios/xcodeproj.dart'; -import '../reporting/reporting.dart'; const int kXcodeRequiredVersionMajor = 10; const int kXcodeRequiredVersionMinor = 2; @@ -189,315 +185,3 @@ class Xcode { ); } } - -/// A utility class for interacting with Xcode xcdevice command line tools. -class XCDevice { - XCDevice({ - @required ProcessManager processManager, - @required Logger logger, - @required Xcode xcode, - }) : _processUtils = ProcessUtils(logger: logger, processManager: processManager), - _logger = logger, - _xcode = xcode; - - final ProcessUtils _processUtils; - final Logger _logger; - final Xcode _xcode; - - bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck && xcdevicePath != null; - - String _xcdevicePath; - String get xcdevicePath { - if (_xcdevicePath == null) { - try { - _xcdevicePath = _processUtils.runSync( - [ - 'xcrun', - '--find', - 'xcdevice' - ], - throwOnError: true, - ).stdout.trim(); - } on ProcessException catch (exception) { - _logger.printTrace('Process exception finding xcdevice:\n$exception'); - } on ArgumentError catch (exception) { - _logger.printTrace('Argument exception finding xcdevice:\n$exception'); - } - } - return _xcdevicePath; - } - - Future> _getAllDevices({bool useCache = false}) async { - if (!isInstalled) { - _logger.printTrace('Xcode not found. Run \'flutter doctor\' for more information.'); - return null; - } - if (useCache && _cachedListResults != null) { - return _cachedListResults; - } - try { - // USB-tethered devices should be found quickly. 1 second timeout is faster than the default. - final RunResult result = await _processUtils.run( - [ - 'xcrun', - 'xcdevice', - 'list', - '--timeout', - '1', - ], - throwOnError: true, - ); - if (result.exitCode == 0) { - final List listResults = json.decode(result.stdout) as List; - _cachedListResults = listResults; - return listResults; - } - _logger.printTrace('xcdevice returned an error:\n${result.stderr}'); - } on ProcessException catch (exception) { - _logger.printTrace('Process exception running xcdevice list:\n$exception'); - } on ArgumentError catch (exception) { - _logger.printTrace('Argument exception running xcdevice list:\n$exception'); - } - - return null; - } - - List _cachedListResults; - - /// List of devices available over USB. - Future> getAvailableTetheredIOSDevices() async { - final List allAvailableDevices = await _getAllDevices(); - - if (allAvailableDevices == null) { - return const []; - } - - // [ - // { - // "simulator" : true, - // "operatingSystemVersion" : "13.3 (17K446)", - // "available" : true, - // "platform" : "com.apple.platform.appletvsimulator", - // "modelCode" : "AppleTV5,3", - // "identifier" : "CBB5E1ED-2172-446E-B4E7-F2B5823DBBA6", - // "architecture" : "x86_64", - // "modelName" : "Apple TV", - // "name" : "Apple TV" - // }, - // { - // "simulator" : false, - // "operatingSystemVersion" : "13.3 (17C54)", - // "interface" : "usb", - // "available" : true, - // "platform" : "com.apple.platform.iphoneos", - // "modelCode" : "iPhone8,1", - // "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418", - // "architecture" : "arm64", - // "modelName" : "iPhone 6s", - // "name" : "iPhone" - // }, - // { - // "simulator" : true, - // "operatingSystemVersion" : "6.1.1 (17S445)", - // "available" : true, - // "platform" : "com.apple.platform.watchsimulator", - // "modelCode" : "Watch5,4", - // "identifier" : "2D74FB11-88A0-44D0-B81E-C0C142B1C94A", - // "architecture" : "i386", - // "modelName" : "Apple Watch Series 5 - 44mm", - // "name" : "Apple Watch Series 5 - 44mm" - // }, - // ... - - final List devices = []; - for (final dynamic device in allAvailableDevices) { - if (device is! Map) { - continue; - } - final Map deviceProperties = device as Map; - - // Only include iPhone, iPad, iPod, or other iOS devices. - if (!_isIPhoneOSDevice(deviceProperties)) { - continue; - } - - final String errorMessage = _parseErrorMessage(deviceProperties); - if (errorMessage != null) { - if (errorMessage.contains('not paired')) { - UsageEvent('device', 'ios-trust-failure').send(); - } - _logger.printTrace(errorMessage); - - continue; - } - - // In case unavailable without an error (may not be possible...) - if (!_isAvailable(deviceProperties)) { - continue; - } - - // Only support USB devices, skip "network" interface (Xcode > Window > Devices and Simulators > Connect via network). - if (!_isUSBTethered(deviceProperties)) { - continue; - } - - devices.add(IOSDevice( - device['identifier'] as String, - name: device['name'] as String, - cpuArchitecture: _cpuArchitecture(deviceProperties), - sdkVersion: _sdkVersion(deviceProperties), - )); - } - return devices; - } - - /// Despite the name, com.apple.platform.iphoneos includes iPhone, iPads, and all iOS devices. - /// Excludes simulators. - static bool _isIPhoneOSDevice(Map deviceProperties) { - if (deviceProperties.containsKey('platform')) { - final String platform = deviceProperties['platform'] as String; - return platform == 'com.apple.platform.iphoneos'; - } - return false; - } - - static bool _isAvailable(Map deviceProperties) { - return deviceProperties.containsKey('available') && (deviceProperties['available'] as bool); - } - - static bool _isUSBTethered(Map deviceProperties) { - // Interface can be "usb", "network", or not present for simulators. - return deviceProperties.containsKey('interface') && - (deviceProperties['interface'] as String).toLowerCase() == 'usb'; - } - - static String _sdkVersion(Map deviceProperties) { - if (deviceProperties.containsKey('operatingSystemVersion')) { - // Parse out the OS version, ignore the build number in parentheses. - // "13.3 (17C54)" - final RegExp operatingSystemRegex = RegExp(r'(.*) \(.*\)$'); - final String operatingSystemVersion = deviceProperties['operatingSystemVersion'] as String; - return operatingSystemRegex.firstMatch(operatingSystemVersion.trim())?.group(1); - } - return null; - } - - DarwinArch _cpuArchitecture(Map deviceProperties) { - DarwinArch cpuArchitecture; - if (deviceProperties.containsKey('architecture')) { - final String architecture = deviceProperties['architecture'] as String; - try { - cpuArchitecture = getIOSArchForName(architecture); - } catch (error) { - // Fallback to default iOS architecture. Future-proof against a theoretical version - // of Xcode that changes this string to something slightly different like "ARM64". - cpuArchitecture ??= defaultIOSArchs.first; - _logger.printError('Unknown architecture $architecture, defaulting to ${getNameForDarwinArch(cpuArchitecture)}'); - } - } - return cpuArchitecture; - } - - /// Error message parsed from xcdevice. null if no error. - static String _parseErrorMessage(Map deviceProperties) { - // { - // "simulator" : false, - // "operatingSystemVersion" : "13.3 (17C54)", - // "interface" : "usb", - // "available" : false, - // "platform" : "com.apple.platform.iphoneos", - // "modelCode" : "iPhone8,1", - // "identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44", - // "architecture" : "arm64", - // "modelName" : "iPhone 6s", - // "name" : "iPhone", - // "error" : { - // "code" : -9, - // "failureReason" : "", - // "underlyingErrors" : [ - // { - // "code" : 5, - // "failureReason" : "allowsSecureServices: 1. isConnected: 0. Platform: >. DTDKDeviceIdentifierIsIDID: 0", - // "description" : "📱 -- Failed _shouldMakeReadyForDevelopment check even though device is not locked by passcode.", - // "recoverySuggestion" : "", - // "domain" : "com.apple.platform.iphoneos" - // } - // ], - // "description" : "iPhone is not paired with your computer.", - // "recoverySuggestion" : "To use iPhone with Xcode, unlock it and choose to trust this computer when prompted.", - // "domain" : "com.apple.platform.iphoneos" - // } - // }, - // { - // "simulator" : false, - // "operatingSystemVersion" : "13.3 (17C54)", - // "interface" : "usb", - // "available" : false, - // "platform" : "com.apple.platform.iphoneos", - // "modelCode" : "iPhone8,1", - // "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418", - // "architecture" : "arm64", - // "modelName" : "iPhone 6s", - // "name" : "iPhone", - // "error" : { - // "code" : -9, - // "failureReason" : "", - // "description" : "iPhone is not paired with your computer.", - // "domain" : "com.apple.platform.iphoneos" - // } - // } - // ... - - if (!deviceProperties.containsKey('error')) { - return null; - } - - final Map error = deviceProperties['error'] as Map; - - final StringBuffer errorMessage = StringBuffer('Error: '); - - if (error.containsKey('description')) { - final String description = error['description'] as String; - errorMessage.write(description); - if (!description.endsWith('.')) { - errorMessage.write('.'); - } - } else { - errorMessage.write('Xcode pairing error.'); - } - - if (error.containsKey('recoverySuggestion')) { - final String recoverySuggestion = error['recoverySuggestion'] as String; - errorMessage.write(' $recoverySuggestion'); - } - - if (error.containsKey('code') && error['code'] is int) { - final int code = error['code'] as int; - errorMessage.write(' (code $code)'); - } - - return errorMessage.toString(); - } - - /// List of all devices reporting errors. - Future> getDiagnostics() async { - final List allAvailableDevices = await _getAllDevices(useCache: true); - - if (allAvailableDevices == null) { - return const []; - } - - final List diagnostics = []; - for (final dynamic device in allAvailableDevices) { - if (device is! Map) { - continue; - } - final Map deviceProperties = device as Map; - final String errorMessage = _parseErrorMessage(deviceProperties); - if (errorMessage != null) { - diagnostics.add(errorMessage); - } - } - return diagnostics; - } -} 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 e78a0bc8f1b..9a2e3395ef2 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -52,7 +52,6 @@ class MockXcode extends Mock implements Xcode {} class MockFile extends Mock implements File {} class MockPortForwarder extends Mock implements DevicePortForwarder {} class MockUsage extends Mock implements Usage {} -class MockXcdevice extends Mock implements XCDevice {} void main() { final FakePlatform macPlatform = FakePlatform.fromPlatform(const LocalPlatform()); @@ -66,17 +65,17 @@ void main() { final List unsupportedPlatforms = [linuxPlatform, windowsPlatform]; testUsingContext('successfully instantiates on Mac OS', () { - IOSDevice('device-123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + IOSDevice('device-123'); }, overrides: { Platform: () => macPlatform, }); 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); + expect(IOSDevice('device-123', sdkVersion: '1.0.0').majorSdkVersion, 1); + expect(IOSDevice('device-123', sdkVersion: '13.1.1').majorSdkVersion, 13); + expect(IOSDevice('device-123', sdkVersion: '10').majorSdkVersion, 10); + expect(IOSDevice('device-123', sdkVersion: '0').majorSdkVersion, 0); + expect(IOSDevice('device-123', sdkVersion: 'bogus').majorSdkVersion, 0); }, overrides: { Platform: () => macPlatform, }); @@ -84,7 +83,7 @@ void main() { for (final Platform platform in unsupportedPlatforms) { testUsingContext('throws UnsupportedError exception if instantiated on ${platform.operatingSystem}', () { expect( - () { IOSDevice('device-123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); }, + () { IOSDevice('device-123'); }, throwsAssertionError, ); }, overrides: { @@ -133,7 +132,7 @@ void main() { }); testUsingContext(' kills all log readers & port forwarders', () async { - device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + device = IOSDevice('123'); logReader1 = createLogReader(device, appPackage1, mockProcess1); logReader2 = createLogReader(device, appPackage2, mockProcess2); portForwarder = createPortForwarder(forwardedPort, device); @@ -240,6 +239,9 @@ void main() { )).thenAnswer( (_) => Future.value(ProcessResult(1, 0, '', '')) ); + + when(mockIMobileDevice.getInfoForDevice(any, 'CPUArchitecture')) + .thenAnswer((_) => Future.value('arm64')); }); tearDown(() { @@ -250,7 +252,7 @@ void main() { }); 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'); device.portForwarder = mockPortForwarder; device.setLogReader(mockApp, mockLogReader); await device.dispose(); @@ -259,8 +261,24 @@ void main() { Platform: () => macPlatform, }); + testUsingContext('returns failed if the IOSDevice is not found', () async { + final IOSDevice device = IOSDevice('123'); + when(mockIMobileDevice.getInfoForDevice(any, 'CPUArchitecture')).thenThrow( + const IOSDeviceNotFoundError( + 'ideviceinfo could not find device:\n' + 'No device found with udid 123, is it plugged in?\n' + 'Try unlocking attached devices.' + ) + ); + final LaunchResult result = await device.startApp(mockApp); + expect(result.started, false); + }, overrides: { + IMobileDevice: () => mockIMobileDevice, + Platform: () => macPlatform, + }); + testUsingContext(' succeeds in debug mode via mDNS', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice('123'); device.portForwarder = mockPortForwarder; device.setLogReader(mockApp, mockLogReader); final Uri uri = Uri( @@ -297,7 +315,7 @@ void main() { 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); final IOSDevicePortForwarder portForwarder = IOSDevicePortForwarder(device); bool firstRun = true; final MockProcess successProcess = MockProcess( @@ -331,7 +349,7 @@ void main() { }); 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); + final IOSDevice device = IOSDevice('123'); device.portForwarder = mockPortForwarder; device.setLogReader(mockApp, mockLogReader); // Now that the reader is used, start writing messages to it. @@ -363,7 +381,7 @@ void main() { }); 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); + final IOSDevice device = IOSDevice('123'); device.portForwarder = mockPortForwarder; device.setLogReader(mockApp, mockLogReader); @@ -395,7 +413,7 @@ void main() { }); 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'); final LaunchResult launchResult = await device.startApp(mockApp, prebuiltApplication: true, debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null, treeShakeIcons: false)), @@ -413,7 +431,7 @@ void main() { }); 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'); device.setLogReader(mockApp, mockLogReader); final Uri uri = Uri( scheme: 'http', @@ -457,7 +475,7 @@ void main() { }); 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'); device.setLogReader(mockApp, mockLogReader); final Uri uri = Uri( scheme: 'http', @@ -571,7 +589,7 @@ void main() { 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'); // Pre-create the expected build products. targetBuildDir.createSync(recursive: true); @@ -679,7 +697,7 @@ void main() { }); testUsingContext('installApp() invokes process with correct environment', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice('123'); const String bundlePath = '/path/to/bundle'; final List args = [installerPath, '-i', bundlePath]; when(mockApp.deviceBundlePath).thenReturn(bundlePath); @@ -701,7 +719,7 @@ void main() { }); testUsingContext('isAppInstalled() invokes process with correct environment', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice('123'); final List args = [installerPath, '--list-apps']; when(mockProcessManager.run(args, environment: env)) .thenAnswer( @@ -718,7 +736,7 @@ void main() { }); testUsingContext('uninstallApp() invokes process with correct environment', () async { - final IOSDevice device = IOSDevice('123', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice('123'); final List args = [installerPath, '-U', appId]; when(mockApp.id).thenReturn(appId); when(mockProcessManager.run(args, environment: env)) @@ -737,61 +755,90 @@ void main() { }); group('getAttachedDevices', () { - MockXcdevice mockXcdevice; + MockIMobileDevice mockIMobileDevice; setUp(() { - mockXcdevice = MockXcdevice(); + mockIMobileDevice = MockIMobileDevice(); + }); + + testUsingContext('return no devices if Xcode is not installed', () async { + when(mockIMobileDevice.isInstalled).thenReturn(false); + expect(await IOSDevice.getAttachedDevices(), isEmpty); + }, overrides: { + IMobileDevice: () => mockIMobileDevice, + Platform: () => macPlatform, + }); + + testUsingContext('returns no devices if none are attached', () async { + when(globals.iMobileDevice.isInstalled).thenReturn(true); + when(globals.iMobileDevice.getAvailableDeviceIDs()) + .thenAnswer((Invocation invocation) => Future.value('')); + final List devices = await IOSDevice.getAttachedDevices(); + expect(devices, isEmpty); + }, overrides: { + IMobileDevice: () => mockIMobileDevice, + Platform: () => macPlatform, }); final List unsupportedPlatforms = [linuxPlatform, windowsPlatform]; - for (final Platform unsupportedPlatform in unsupportedPlatforms) { - testWithoutContext('throws Unsupported Operation exception on ${unsupportedPlatform.operatingSystem}', () async { - when(mockXcdevice.isInstalled).thenReturn(false); + for (final Platform platform in unsupportedPlatforms) { + testUsingContext('throws Unsupported Operation exception on ${platform.operatingSystem}', () async { + when(globals.iMobileDevice.isInstalled).thenReturn(false); + when(globals.iMobileDevice.getAvailableDeviceIDs()) + .thenAnswer((Invocation invocation) => Future.value('')); expect( - () async { await IOSDevice.getAttachedDevices(unsupportedPlatform, mockXcdevice); }, + () async { await IOSDevice.getAttachedDevices(); }, throwsA(isA()), ); + }, overrides: { + IMobileDevice: () => mockIMobileDevice, + Platform: () => platform, }); } testUsingContext('returns attached devices', () async { - when(mockXcdevice.isInstalled).thenReturn(true); - final IOSDevice device = IOSDevice('d83d5bc53967baa0ee18626ba87b6254b2ab5418', name: 'Paired iPhone', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64); - when(mockXcdevice.getAvailableTetheredIOSDevices()) - .thenAnswer((Invocation invocation) => Future>.value([device])); - - final List devices = await IOSDevice.getAttachedDevices(macPlatform, mockXcdevice); - expect(devices, hasLength(1)); - expect(identical(devices.first, device), isTrue); + when(globals.iMobileDevice.isInstalled).thenReturn(true); + when(globals.iMobileDevice.getAvailableDeviceIDs()) + .thenAnswer((Invocation invocation) => Future.value(''' +98206e7a4afd4aedaff06e687594e089dede3c44 +f577a7903cc54959be2e34bc4f7f80b7009efcf4 +''')); + when(globals.iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName')) + .thenAnswer((_) => Future.value('La tele me regarde')); + when(globals.iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'ProductVersion')) + .thenAnswer((_) => Future.value('10.3.2')); + when(globals.iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName')) + .thenAnswer((_) => Future.value('Puits sans fond')); + when(globals.iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'ProductVersion')) + .thenAnswer((_) => Future.value('11.0')); + final List devices = await IOSDevice.getAttachedDevices(); + expect(devices, hasLength(2)); + expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); + expect(devices[0].name, 'La tele me regarde'); + expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4'); + expect(devices[1].name, 'Puits sans fond'); }, overrides: { + IMobileDevice: () => mockIMobileDevice, Platform: () => macPlatform, }); - }); - group('getDiagnostics', () { - MockXcdevice mockXcdevice; - - setUp(() { - mockXcdevice = MockXcdevice(); - }); - - final List unsupportedPlatforms = [linuxPlatform, windowsPlatform]; - for (final Platform unsupportedPlatform in unsupportedPlatforms) { - testWithoutContext('throws returns platform diagnostic exception on ${unsupportedPlatform.operatingSystem}', () async { - when(mockXcdevice.isInstalled).thenReturn(false); - expect((await IOSDevice.getDiagnostics(unsupportedPlatform, mockXcdevice)).first, 'Control of iOS devices or simulators only supported on macOS.'); - }); - } - - testUsingContext('returns diagnostics', () async { - when(mockXcdevice.isInstalled).thenReturn(true); - when(mockXcdevice.getDiagnostics()) - .thenAnswer((Invocation invocation) => Future>.value(['Generic pairing error'])); - - final List diagnostics = await IOSDevice.getDiagnostics(macPlatform, mockXcdevice); - expect(diagnostics, hasLength(1)); - expect(diagnostics.first, 'Generic pairing error'); + testUsingContext('returns attached devices and ignores devices that cannot be found by ideviceinfo', () async { + when(globals.iMobileDevice.isInstalled).thenReturn(true); + when(globals.iMobileDevice.getAvailableDeviceIDs()) + .thenAnswer((Invocation invocation) => Future.value(''' +98206e7a4afd4aedaff06e687594e089dede3c44 +f577a7903cc54959be2e34bc4f7f80b7009efcf4 +''')); + when(globals.iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName')) + .thenAnswer((_) => Future.value('La tele me regarde')); + when(globals.iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName')) + .thenThrow(const IOSDeviceNotFoundError('Device not found')); + final List devices = await IOSDevice.getAttachedDevices(); + expect(devices, hasLength(1)); + expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); + expect(devices[0].name, 'La tele me regarde'); }, overrides: { + IMobileDevice: () => mockIMobileDevice, Platform: () => macPlatform, }); }); @@ -830,7 +877,7 @@ Runner(UIKit)[297] : E is for enpitsu" return Future.value(mockProcess); }); - final IOSDevice device = IOSDevice('123456', name: 'iPhone 1', sdkVersion: '10.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice('123456'); final DeviceLogReader logReader = device.getLogReader( app: await BuildableIOSApp.fromProject(mockIosProject), ); @@ -841,7 +888,6 @@ Runner(UIKit)[297] : E is for enpitsu" IMobileDevice: () => mockIMobileDevice, Platform: () => macPlatform, }); - testUsingContext('includes multi-line Flutter logs in the output', () async { when(mockIMobileDevice.startLogger('123456')).thenAnswer((Invocation invocation) { final Process mockProcess = MockProcess( @@ -856,7 +902,7 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt return Future.value(mockProcess); }); - final IOSDevice device = IOSDevice('123456', name: 'iPhone 1', sdkVersion: '10.3', cpuArchitecture: DarwinArch.arm64); + final IOSDevice device = IOSDevice('123456'); final DeviceLogReader logReader = device.getLogReader( app: await BuildableIOSApp.fromProject(mockIosProject), ); @@ -886,7 +932,7 @@ flutter: globals.fs.file('.packages').createSync(); final FlutterProject flutterProject = FlutterProject.current(); - expect(IOSDevice('test', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64).isSupportedForProject(flutterProject), true); + expect(IOSDevice('test').isSupportedForProject(flutterProject), true); }, overrides: { FileSystem: () => MemoryFileSystem(), ProcessManager: () => FakeProcessManager.any(), @@ -898,7 +944,7 @@ flutter: globals.fs.directory('ios').createSync(); final FlutterProject flutterProject = FlutterProject.current(); - expect(IOSDevice('test', name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64).isSupportedForProject(flutterProject), true); + expect(IOSDevice('test').isSupportedForProject(flutterProject), true); }, overrides: { FileSystem: () => MemoryFileSystem(), ProcessManager: () => FakeProcessManager.any(), @@ -910,7 +956,7 @@ flutter: 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); + expect(IOSDevice('test').isSupportedForProject(flutterProject), false); }, overrides: { FileSystem: () => MemoryFileSystem(), ProcessManager: () => FakeProcessManager.any(), diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart index 5d8c9cd31ce..93b1bc19488 100644 --- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart @@ -6,8 +6,6 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult; import 'package:flutter_tools/src/base/logger.dart'; -import 'package:flutter_tools/src/build_info.dart'; -import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:mockito/mockito.dart'; @@ -23,494 +21,160 @@ class MockPlatform extends Mock implements Platform {} void main() { ProcessManager processManager; + Xcode xcode; + MockXcodeProjectInterpreter mockXcodeProjectInterpreter; + MockPlatform platform; Logger logger; + FileSystem fileSystem; setUp(() { logger = MockLogger(); + fileSystem = MemoryFileSystem(); processManager = MockProcessManager(); + mockXcodeProjectInterpreter = MockXcodeProjectInterpreter(); + platform = MockPlatform(); + xcode = Xcode( + logger: logger, + platform: platform, + fileSystem: fileSystem, + processManager: processManager, + xcodeProjectInterpreter: mockXcodeProjectInterpreter, + ); }); - group('Xcode', () { - Xcode xcode; - MockXcodeProjectInterpreter mockXcodeProjectInterpreter; - MockPlatform platform; - FileSystem fileSystem; + testWithoutContext('xcodeSelectPath returns null when xcode-select is not installed', () { + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenThrow(const ProcessException('/usr/bin/xcode-select', ['--print-path'])); + expect(xcode.xcodeSelectPath, isNull); + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenThrow(ArgumentError('Invalid argument(s): Cannot find executable for /usr/bin/xcode-select')); - setUp(() { - fileSystem = MemoryFileSystem(); - mockXcodeProjectInterpreter = MockXcodeProjectInterpreter(); - platform = MockPlatform(); - xcode = Xcode( - logger: logger, - platform: platform, - fileSystem: fileSystem, - processManager: processManager, - xcodeProjectInterpreter: mockXcodeProjectInterpreter, - ); - }); - - testWithoutContext('xcodeSelectPath returns null when xcode-select is not installed', () { - when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenThrow(const ProcessException('/usr/bin/xcode-select', ['--print-path'])); - expect(xcode.xcodeSelectPath, isNull); - when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenThrow(ArgumentError('Invalid argument(s): Cannot find executable for /usr/bin/xcode-select')); - - expect(xcode.xcodeSelectPath, isNull); - }); - - testWithoutContext('xcodeSelectPath returns path when xcode-select is installed', () { - const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; - when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenReturn(ProcessResult(1, 0, xcodePath, '')); - - expect(xcode.xcodeSelectPath, xcodePath); - }); - - testWithoutContext('xcodeVersionSatisfactory is false when version is less than minimum', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); - - expect(xcode.isVersionSatisfactory, isFalse); - }); - - testWithoutContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); - - expect(xcode.isVersionSatisfactory, isFalse); - }); - - testWithoutContext('xcodeVersionSatisfactory is true when version meets minimum', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - - expect(xcode.isVersionSatisfactory, isTrue); - }); - - testWithoutContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - - expect(xcode.isVersionSatisfactory, isTrue); - }); - - testWithoutContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3); - - expect(xcode.isVersionSatisfactory, isTrue); - }); - - testWithoutContext('isInstalledAndMeetsVersionCheck is false when not macOS', () { - when(platform.isMacOS).thenReturn(false); - - expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); - }); - - testWithoutContext('isInstalledAndMeetsVersionCheck is false when not installed', () { - when(platform.isMacOS).thenReturn(true); - const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; - when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenReturn(ProcessResult(1, 0, xcodePath, '')); - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); - - expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); - }); - - testWithoutContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () { - when(platform.isMacOS).thenReturn(true); - when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenReturn(ProcessResult(1, 127, '', 'ERROR')); - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - - expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); - }); - - testWithoutContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () { - when(platform.isMacOS).thenReturn(true); - const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; - when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenReturn(ProcessResult(1, 0, xcodePath, '')); - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); - - expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); - }); - - testWithoutContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () { - when(platform.isMacOS).thenReturn(true); - const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; - when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenReturn(ProcessResult(1, 0, xcodePath, '')); - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - - expect(xcode.isInstalledAndMeetsVersionCheck, isTrue); - }); - - testWithoutContext('eulaSigned is false when clang is not installed', () { - when(processManager.runSync(['/usr/bin/xcrun', 'clang'])) - .thenThrow(const ProcessException('/usr/bin/xcrun', ['clang'])); - - expect(xcode.eulaSigned, isFalse); - }); - - testWithoutContext('eulaSigned is false when clang output indicates EULA not yet accepted', () { - when(processManager.runSync(['/usr/bin/xcrun', 'clang'])) - .thenReturn(ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.')); - - expect(xcode.eulaSigned, isFalse); - }); - - testWithoutContext('eulaSigned is true when clang output indicates EULA has been accepted', () { - when(processManager.runSync(['/usr/bin/xcrun', 'clang'])) - .thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files')); - - expect(xcode.eulaSigned, isTrue); - }); - - testWithoutContext('SDK name', () { - expect(getNameForSdk(SdkType.iPhone), 'iphoneos'); - expect(getNameForSdk(SdkType.iPhoneSimulator), 'iphonesimulator'); - expect(getNameForSdk(SdkType.macOS), 'macosx'); - }); + expect(xcode.xcodeSelectPath, isNull); }); - group('xcdevice', () { - XCDevice xcdevice; - MockXcode mockXcode; + testWithoutContext('xcodeSelectPath returns path when xcode-select is installed', () { + const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(ProcessResult(1, 0, xcodePath, '')); - setUp(() { - mockXcode = MockXcode(); - xcdevice = XCDevice( - processManager: processManager, - logger: logger, - xcode: mockXcode, - ); - }); + expect(xcode.xcodeSelectPath, xcodePath); + }); - group('installed', () { - testWithoutContext('Xcode not installed', () { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false); - expect(xcdevice.isInstalled, false); - }); + testWithoutContext('xcodeVersionSatisfactory is false when version is less than minimum', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); - testWithoutContext("xcrun can't find xcdevice", () { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true); + expect(xcode.isVersionSatisfactory, isFalse); + }); - when(processManager.runSync(['xcrun', '--find', 'xcdevice'])) - .thenThrow(const ProcessException('xcrun', ['--find', 'xcdevice'])); - expect(xcdevice.isInstalled, false); - verify(processManager.runSync(any)).called(1); - }); + testWithoutContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); - testWithoutContext('is installed', () { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true); + expect(xcode.isVersionSatisfactory, isFalse); + }); - when(processManager.runSync(['xcrun', '--find', 'xcdevice'])) - .thenReturn(ProcessResult(1, 0, '/path/to/xcdevice', '')); - expect(xcdevice.isInstalled, true); - }); - }); + testWithoutContext('xcodeVersionSatisfactory is true when version meets minimum', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - group('available devices', () { - final FakePlatform macPlatform = FakePlatform.fromPlatform(const LocalPlatform()); - macPlatform.operatingSystem = 'macos'; + expect(xcode.isVersionSatisfactory, isTrue); + }); - testWithoutContext('Xcode not installed', () async { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false); + testWithoutContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - expect(await xcdevice.getAvailableTetheredIOSDevices(), isEmpty); - verifyNever(processManager.run(any)); - }); + expect(xcode.isVersionSatisfactory, isTrue); + }); - testWithoutContext('xcdevice fails', () async { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true); + testWithoutContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3); - when(processManager.runSync(['xcrun', '--find', 'xcdevice'])) - .thenReturn(ProcessResult(1, 0, '/path/to/xcdevice', '')); + expect(xcode.isVersionSatisfactory, isTrue); + }); - when(processManager.run(['xcrun', 'xcdevice', 'list', '--timeout', '1'])) - .thenThrow(const ProcessException('xcrun', ['xcdevice', 'list', '--timeout', '1'])); + testWithoutContext('isInstalledAndMeetsVersionCheck is false when not macOS', () { + when(platform.isMacOS).thenReturn(false); - expect(await xcdevice.getAvailableTetheredIOSDevices(), isEmpty); - }); + expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); + }); - testUsingContext('returns devices', () async { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true); + testWithoutContext('isInstalledAndMeetsVersionCheck is false when not installed', () { + when(platform.isMacOS).thenReturn(true); + const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(ProcessResult(1, 0, xcodePath, '')); + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); - when(processManager.runSync(['xcrun', '--find', 'xcdevice'])) - .thenReturn(ProcessResult(1, 0, '/path/to/xcdevice', '')); + expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); + }); - const String devicesOutput = ''' -[ - { - "simulator" : true, - "operatingSystemVersion" : "13.3 (17K446)", - "available" : true, - "platform" : "com.apple.platform.appletvsimulator", - "modelCode" : "AppleTV5,3", - "identifier" : "CBB5E1ED-2172-446E-B4E7-F2B5823DBBA6", - "architecture" : "x86_64", - "modelName" : "Apple TV", - "name" : "Apple TV" - }, - { - "simulator" : false, - "operatingSystemVersion" : "13.3 (17C54)", - "interface" : "usb", - "available" : true, - "platform" : "com.apple.platform.iphoneos", - "modelCode" : "iPhone8,1", - "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418", - "architecture" : "arm64", - "modelName" : "iPhone 6s", - "name" : "An iPhone (Space Gray)" - }, - { - "simulator" : false, - "operatingSystemVersion" : "10.1 (14C54)", - "interface" : "usb", - "available" : true, - "platform" : "com.apple.platform.iphoneos", - "modelCode" : "iPad11,4", - "identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44", - "architecture" : "armv7", - "modelName" : "iPad Air 3rd Gen", - "name" : "iPad 1" - }, - { - "simulator" : false, - "operatingSystemVersion" : "10.1 (14C54)", - "interface" : "network", - "available" : true, - "platform" : "com.apple.platform.iphoneos", - "modelCode" : "iPad11,4", - "identifier" : "234234234234234234345445687594e089dede3c44", - "architecture" : "arm64", - "modelName" : "iPad Air 3rd Gen", - "name" : "A networked iPad" - }, - { - "simulator" : false, - "operatingSystemVersion" : "10.1 (14C54)", - "interface" : "usb", - "available" : true, - "platform" : "com.apple.platform.iphoneos", - "modelCode" : "iPad11,4", - "identifier" : "f577a7903cc54959be2e34bc4f7f80b7009efcf4", - "architecture" : "BOGUS", - "modelName" : "iPad Air 3rd Gen", - "name" : "iPad 2" - }, - { - "simulator" : true, - "operatingSystemVersion" : "6.1.1 (17S445)", - "available" : true, - "platform" : "com.apple.platform.watchsimulator", - "modelCode" : "Watch5,4", - "identifier" : "2D74FB11-88A0-44D0-B81E-C0C142B1C94A", - "architecture" : "i386", - "modelName" : "Apple Watch Series 5 - 44mm", - "name" : "Apple Watch Series 5 - 44mm" - }, - { - "simulator" : false, - "operatingSystemVersion" : "13.3 (17C54)", - "interface" : "usb", - "available" : false, - "platform" : "com.apple.platform.iphoneos", - "modelCode" : "iPhone8,1", - "identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2", - "architecture" : "arm64", - "modelName" : "iPhone 6s", - "name" : "iPhone", - "error" : { - "code" : -9, - "failureReason" : "", - "description" : "iPhone is not paired with your computer.", - "domain" : "com.apple.platform.iphoneos" - } - } -] -'''; + testWithoutContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () { + when(platform.isMacOS).thenReturn(true); + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(ProcessResult(1, 127, '', 'ERROR')); + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - when(processManager.run(['xcrun', 'xcdevice', 'list', '--timeout', '1'])) - .thenAnswer((_) => Future.value(ProcessResult(1, 0, devicesOutput, ''))); - final List devices = await xcdevice.getAvailableTetheredIOSDevices(); - expect(devices, hasLength(3)); - expect(devices[0].id, 'd83d5bc53967baa0ee18626ba87b6254b2ab5418'); - expect(devices[0].name, 'An iPhone (Space Gray)'); - expect(await devices[0].sdkNameAndVersion, 'iOS 13.3'); - expect(devices[0].cpuArchitecture, DarwinArch.arm64); - expect(devices[1].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); - expect(devices[1].name, 'iPad 1'); - expect(await devices[1].sdkNameAndVersion, 'iOS 10.1'); - expect(devices[1].cpuArchitecture, DarwinArch.armv7); - expect(devices[2].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4'); - expect(devices[2].name, 'iPad 2'); - expect(await devices[2].sdkNameAndVersion, 'iOS 10.1'); - expect(devices[2].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture. - }, overrides: { - Platform: () => macPlatform, - }); - }); + expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); + }); - group('diagnostics', () { - final FakePlatform macPlatform = FakePlatform.fromPlatform(const LocalPlatform()); - macPlatform.operatingSystem = 'macos'; + testWithoutContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () { + when(platform.isMacOS).thenReturn(true); + const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(ProcessResult(1, 0, xcodePath, '')); + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); - testWithoutContext('Xcode not installed', () async { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false); + expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); + }); - expect(await xcdevice.getDiagnostics(), isEmpty); - verifyNever(processManager.run(any)); - }); + testWithoutContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () { + when(platform.isMacOS).thenReturn(true); + const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(ProcessResult(1, 0, xcodePath, '')); + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - testWithoutContext('xcdevice fails', () async { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true); + expect(xcode.isInstalledAndMeetsVersionCheck, isTrue); + }); - when(processManager.runSync(['xcrun', '--find', 'xcdevice'])) - .thenReturn(ProcessResult(1, 0, '/path/to/xcdevice', '')); + testWithoutContext('eulaSigned is false when clang is not installed', () { + when(processManager.runSync(['/usr/bin/xcrun', 'clang'])) + .thenThrow(const ProcessException('/usr/bin/xcrun', ['clang'])); - when(processManager.run(['xcrun', 'xcdevice', 'list', '--timeout', '1'])) - .thenThrow(const ProcessException('xcrun', ['xcdevice', 'list', '--timeout', '1'])); + expect(xcode.eulaSigned, isFalse); + }); - expect(await xcdevice.getDiagnostics(), isEmpty); - }); + testWithoutContext('eulaSigned is false when clang output indicates EULA not yet accepted', () { + when(processManager.runSync(['/usr/bin/xcrun', 'clang'])) + .thenReturn(ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.')); - testUsingContext('uses cache', () async { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true); + expect(xcode.eulaSigned, isFalse); + }); - when(processManager.runSync(['xcrun', '--find', 'xcdevice'])) - .thenReturn(ProcessResult(1, 0, '/path/to/xcdevice', '')); + testWithoutContext('eulaSigned is true when clang output indicates EULA has been accepted', () { + when(processManager.runSync(['/usr/bin/xcrun', 'clang'])) + .thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files')); - const String devicesOutput = ''' -[ - { - "simulator" : false, - "operatingSystemVersion" : "13.3 (17C54)", - "interface" : "network", - "available" : false, - "platform" : "com.apple.platform.iphoneos", - "modelCode" : "iPhone8,1", - "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418", - "architecture" : "arm64", - "modelName" : "iPhone 6s", - "error" : { - "code" : -13, - "failureReason" : "", - "domain" : "com.apple.platform.iphoneos" - } - } -] -'''; + expect(xcode.eulaSigned, isTrue); + }); - when(processManager.run(['xcrun', 'xcdevice', 'list', '--timeout', '1'])) - .thenAnswer((_) => Future.value(ProcessResult(1, 0, devicesOutput, ''))); - await xcdevice.getAvailableTetheredIOSDevices(); - final List errors = await xcdevice.getDiagnostics(); - expect(errors, hasLength(1)); - - verify(processManager.run(any)).called(1); - }, overrides: { - Platform: () => macPlatform, - }); - - testUsingContext('returns error message', () async { - when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true); - - when(processManager.runSync(['xcrun', '--find', 'xcdevice'])) - .thenReturn(ProcessResult(1, 0, '/path/to/xcdevice', '')); - - const String devicesOutput = ''' -[ - { - "simulator" : false, - "operatingSystemVersion" : "13.3 (17C54)", - "interface" : "usb", - "available" : false, - "platform" : "com.apple.platform.iphoneos", - "modelCode" : "iPhone8,1", - "identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44", - "architecture" : "arm64", - "modelName" : "iPhone 6s", - "name" : "An iPhone (Space Gray)", - "error" : { - "code" : -9, - "failureReason" : "", - "underlyingErrors" : [ - { - "code" : 5, - "failureReason" : "allowsSecureServices: 1. isConnected: 0. Platform: >. DTDKDeviceIdentifierIsIDID: 0", - "description" : "📱 -- Failed _shouldMakeReadyForDevelopment check even though device is not locked by passcode.", - "recoverySuggestion" : "", - "domain" : "com.apple.platform.iphoneos" - } - ], - "description" : "iPhone is not paired with your computer.", - "recoverySuggestion" : "To use iPhone with Xcode, unlock it and choose to trust this computer when prompted.", - "domain" : "com.apple.platform.iphoneos" - } - }, - { - "simulator" : false, - "operatingSystemVersion" : "13.3 (17C54)", - "interface" : "usb", - "available" : false, - "platform" : "com.apple.platform.iphoneos", - "modelCode" : "iPhone8,1", - "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418", - "architecture" : "arm64", - "modelName" : "iPhone 6s", - "name" : "iPhone", - "error" : { - "code" : -9, - "failureReason" : "", - "description" : "iPhone is not paired with your computer", - "domain" : "com.apple.platform.iphoneos" - } - }, - { - "simulator" : false, - "operatingSystemVersion" : "13.3 (17C54)", - "interface" : "network", - "available" : false, - "platform" : "com.apple.platform.iphoneos", - "modelCode" : "iPhone8,1", - "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418", - "architecture" : "arm64", - "modelName" : "iPhone 6s", - "error" : { - "code" : -13, - "failureReason" : "", - "domain" : "com.apple.platform.iphoneos" - } - } -] -'''; - - when(processManager.run(['xcrun', 'xcdevice', 'list', '--timeout', '1'])) - .thenAnswer((_) => Future.value(ProcessResult(1, 0, devicesOutput, ''))); - final List errors = await xcdevice.getDiagnostics(); - expect(errors, hasLength(3)); - expect(errors[0], 'Error: iPhone is not paired with your computer. To use iPhone with Xcode, unlock it and choose to trust this computer when prompted. (code -9)'); - expect(errors[1], 'Error: iPhone is not paired with your computer. (code -9)'); - expect(errors[2], 'Error: Xcode pairing error. (code -13)'); - }, overrides: { - Platform: () => macPlatform, - }); - }); + testWithoutContext('SDK name', () { + expect(getNameForSdk(SdkType.iPhone), 'iphoneos'); + expect(getNameForSdk(SdkType.iPhoneSimulator), 'iphonesimulator'); + expect(getNameForSdk(SdkType.macOS), 'macosx'); }); } class MockLogger extends Mock implements Logger {} -class MockXcode extends Mock implements Xcode {}