// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/doctor_validator.dart'; import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:flutter_tools/src/macos/xcode_validator.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; void main() { group('Xcode validation', () { testWithoutContext('Emits missing status when Xcode is not installed', () async { final ProcessManager processManager = FakeProcessManager.any(); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test( processManager: processManager, version: null, ), ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: FakeIOSSimulatorUtils(), ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.missing); expect(result.statusInfo, isNull); expect(result.messages.last.type, ValidationMessageType.error); expect(result.messages.last.message, contains('Xcode not installed')); }); testWithoutContext('Emits missing status when Xcode installation is incomplete', () async { final ProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['/usr/bin/xcode-select', '--print-path'], stdout: '/Library/Developer/CommandLineTools', ), ]); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test( processManager: processManager, version: null, ), ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: FakeIOSSimulatorUtils(), ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.missing); expect(result.messages.last.type, ValidationMessageType.error); expect(result.messages.last.message, contains('Xcode installation is incomplete')); }); testWithoutContext('Emits partial status when Xcode version too low', () async { final ProcessManager processManager = FakeProcessManager.any(); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test( processManager: processManager, version: Version(7, 0, 1), ), ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: FakeIOSSimulatorUtils(), ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.error); expect(result.messages.last.message, contains('Flutter requires Xcode 14 or higher')); }); testWithoutContext('Emits partial status when Xcode below recommended version', () async { final ProcessManager processManager = FakeProcessManager.any(); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test( processManager: processManager, version: Version(14, 4, null), ), ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: FakeIOSSimulatorUtils(), ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.hint); expect( result.messages.last.message, contains('Flutter recommends a minimum Xcode version of 15'), ); }, skip: false); // [intended] Skip this test when minimum and required check versions converge. testWithoutContext('Emits partial status when Xcode EULA not signed', () async { final ProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['/usr/bin/xcode-select', '--print-path'], stdout: '/Library/Developer/CommandLineTools', ), const FakeCommand(command: ['which', 'sysctl']), const FakeCommand(command: ['sysctl', 'hw.optional.arm64'], exitCode: 1), const FakeCommand( command: ['xcrun', 'clang'], exitCode: 1, stderr: 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.', ), const FakeCommand(command: ['xcrun', 'simctl', 'list', 'devices', 'booted']), ]); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: FakeIOSSimulatorUtils(), ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.error); expect(result.messages.last.message, contains('code end user license agreement not signed')); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('Emits partial status when simctl is not installed', () async { final ProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['/usr/bin/xcode-select', '--print-path'], stdout: '/Library/Developer/CommandLineTools', ), const FakeCommand(command: ['which', 'sysctl']), const FakeCommand(command: ['sysctl', 'hw.optional.arm64'], exitCode: 1), const FakeCommand(command: ['xcrun', 'clang']), const FakeCommand( command: ['xcrun', 'simctl', 'list', 'devices', 'booted'], exitCode: 1, ), ]); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: FakeIOSSimulatorUtils(), ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.error); expect(result.messages.last.message, contains('Xcode requires additional components')); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('Emits partial status when unable to find simulator SDK', () async { final ProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['/usr/bin/xcode-select', '--print-path'], stdout: '/Library/Developer/CommandLineTools', ), const FakeCommand(command: ['which', 'sysctl']), const FakeCommand(command: ['sysctl', 'hw.optional.arm64'], exitCode: 1), const FakeCommand(command: ['xcrun', 'clang']), const FakeCommand(command: ['xcrun', 'simctl', 'list', 'devices', 'booted']), const FakeCommand( command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], ), ]); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: FakeIOSSimulatorUtils(), ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.error); expect(result.messages.last.message, contains('Unable to find the iPhone Simulator SDK')); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('Emits partial status when unable to get simulator runtimes', () async { final ProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['/usr/bin/xcode-select', '--print-path'], stdout: '/Library/Developer/CommandLineTools', ), const FakeCommand(command: ['which', 'sysctl']), const FakeCommand(command: ['sysctl', 'hw.optional.arm64'], exitCode: 1), const FakeCommand(command: ['xcrun', 'clang']), const FakeCommand(command: ['xcrun', 'simctl', 'list', 'devices', 'booted']), const FakeCommand( command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], stdout: '17.0', ), ]); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: FakeIOSSimulatorUtils(), ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.error); expect( result.messages.last.message, contains('Unable to get list of installed Simulator runtimes'), ); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext( 'Emits partial status with hint when simulator runtimes do not match SDK', () async { final ProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['/usr/bin/xcode-select', '--print-path'], stdout: '/Library/Developer/CommandLineTools', ), const FakeCommand(command: ['which', 'sysctl']), const FakeCommand(command: ['sysctl', 'hw.optional.arm64'], exitCode: 1), const FakeCommand(command: ['xcrun', 'clang']), const FakeCommand(command: ['xcrun', 'simctl', 'list', 'devices', 'booted']), const FakeCommand( command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], stdout: '17.0', ), ]); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), ); final simulatorUtils = FakeIOSSimulatorUtils( runtimes: [ IOSSimulatorRuntime.fromJson({'version': '16.0'}), ], ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: simulatorUtils, ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.hint); expect(result.messages.last.message, contains('iOS 17.0 Simulator not installed')); expect(processManager, hasNoRemainingExpectations); }, ); testWithoutContext('Succeeds when all checks pass', () async { final ProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['/usr/bin/xcode-select', '--print-path'], stdout: '/Library/Developer/CommandLineTools', ), const FakeCommand(command: ['which', 'sysctl']), const FakeCommand(command: ['sysctl', 'hw.optional.arm64'], exitCode: 1), const FakeCommand(command: ['xcrun', 'clang']), const FakeCommand(command: ['xcrun', 'simctl', 'list', 'devices', 'booted']), const FakeCommand( command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], stdout: '17.0', ), ]); final xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), ); final simulatorUtils = FakeIOSSimulatorUtils( runtimes: [ IOSSimulatorRuntime.fromJson({'version': '17.0'}), ], ); final validator = XcodeValidator( xcode: xcode, userMessages: UserMessages(), iosSimulatorUtils: simulatorUtils, ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.success); expect(result.messages.length, 2); final ValidationMessage firstMessage = result.messages.first; expect(firstMessage.type, ValidationMessageType.information); expect(firstMessage.message, 'Xcode at /Library/Developer/CommandLineTools'); expect(result.statusInfo, '1000.0.0'); expect(result.messages[1].message, 'Build 13C100'); expect(processManager, hasNoRemainingExpectations); }); }); } class FakeIOSSimulatorUtils extends Fake implements IOSSimulatorUtils { FakeIOSSimulatorUtils({this.runtimes}); List? runtimes; List get _runtimesList { return runtimes ?? []; } @override Future> getAvailableIOSRuntimes() async { return _runtimesList; } }