Unverified Commit b39604e0 authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

Check for simulator runtime in flutter doctor (#131795)

Redo of https://github.com/flutter/flutter/pull/130728 - code is the same as before. That PR was stuck in Google testing and then I messed up the rebase so started over.

----

Starting in Xcode 15, the simulator is no longer included in Xcode and must be downloaded and installed separately. This adds a validation to `flutter doctor` to warn when the needed simulator runtime is missing.

Validation message looks like:
```
[!] Xcode - develop for iOS and macOS (Xcode 15.0)
    ! iOS 17.0 Simulator not installed; this may be necessary for iOS and macOS development.
      To download and install the platform, open Xcode, select Xcode > Settings > Platforms,
      and click the GET button for the required platform.

      For more information, please visit:
        https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes
```

It may also show an error like this when something goes wrong when checking for the simulator:
```
[!] Xcode - develop for iOS and macOS (Xcode 15.0)
    ✗ Unable to find the iPhone Simulator SDK.
```

Note: I'm unsure of in the future if the SDK and the simulator runtime will need to match the exact version or just the major. For now, it only checks against the major version.

Part 3 of https://github.com/flutter/flutter/issues/129558.
parent 1cf39074
......@@ -138,7 +138,14 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
if (androidWorkflow!.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[androidValidator!, androidLicenseValidator!]),
if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[XcodeValidator(xcode: globals.xcode!, userMessages: userMessages), globals.cocoapodsValidator!]),
GroupedValidator(<DoctorValidator>[
XcodeValidator(
xcode: globals.xcode!,
userMessages: userMessages,
iosSimulatorUtils: globals.iosSimulatorUtils!,
),
globals.cocoapodsValidator!,
]),
if (webWorkflow.appliesToHostPlatform)
ChromeValidator(
chromiumLauncher: ChromiumLauncher(
......
......@@ -15,6 +15,7 @@ import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../convert.dart';
import '../devfs.dart';
......@@ -91,6 +92,14 @@ class IOSSimulatorUtils {
);
}).whereType<IOSSimulator>().toList();
}
Future<List<IOSSimulatorRuntime>> getAvailableIOSRuntimes() async {
if (!_xcode.isInstalledAndMeetsVersionCheck) {
return <IOSSimulatorRuntime>[];
}
return _simControl.listAvailableIOSRuntimes();
}
}
/// A wrapper around the `simctl` command line tool.
......@@ -293,6 +302,46 @@ class SimControl {
_logger.printError('Unable to take screenshot of $deviceId:\n$exception');
}
}
/// Runs `simctl list runtimes available iOS --json` and returns all available iOS simulator runtimes.
Future<List<IOSSimulatorRuntime>> listAvailableIOSRuntimes() async {
final List<IOSSimulatorRuntime> runtimes = <IOSSimulatorRuntime>[];
final RunResult results = await _processUtils.run(
<String>[
..._xcode.xcrunCommand(),
'simctl',
'list',
'runtimes',
'available',
'iOS',
'--json',
],
);
if (results.exitCode != 0) {
_logger.printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
return runtimes;
}
try {
final Object? decodeResult = (json.decode(results.stdout) as Map<String, Object?>)['runtimes'];
if (decodeResult is List<Object?>) {
for (final Object? runtimeData in decodeResult) {
if (runtimeData is Map<String, Object?>) {
runtimes.add(IOSSimulatorRuntime.fromJson(runtimeData));
}
}
}
return runtimes;
} on FormatException {
// We failed to parse the simctl output, or it returned junk.
// One known message is "Install Started" isn't valid JSON but is
// returned sometimes.
_logger.printError('simctl returned non-JSON response: ${results.stdout}');
return runtimes;
}
}
}
......@@ -624,6 +673,64 @@ class IOSSimulator extends Device {
}
}
class IOSSimulatorRuntime {
IOSSimulatorRuntime._({
this.bundlePath,
this.buildVersion,
this.platform,
this.runtimeRoot,
this.identifier,
this.version,
this.isInternal,
this.isAvailable,
this.name,
});
// Example:
// {
// "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Volumes\/iOS_21A5277g\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS 17.0.simruntime",
// "buildversion" : "21A5277g",
// "platform" : "iOS",
// "runtimeRoot" : "\/Library\/Developer\/CoreSimulator\/Volumes\/iOS_21A5277g\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS 17.0.simruntime\/Contents\/Resources\/RuntimeRoot",
// "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-17-0",
// "version" : "17.0",
// "isInternal" : false,
// "isAvailable" : true,
// "name" : "iOS 17.0",
// "supportedDeviceTypes" : [
// {
// "bundlePath" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/DeviceTypes\/iPhone 8.simdevicetype",
// "name" : "iPhone 8",
// "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8",
// "productFamily" : "iPhone"
// }
// ]
// },
factory IOSSimulatorRuntime.fromJson(Map<String, Object?> data) {
return IOSSimulatorRuntime._(
bundlePath: data['bundlePath']?.toString(),
buildVersion: data['buildversion']?.toString(),
platform: data['platform']?.toString(),
runtimeRoot: data['runtimeRoot']?.toString(),
identifier: data['identifier']?.toString(),
version: Version.parse(data['version']?.toString()),
isInternal: data['isInternal'] is bool? ? data['isInternal'] as bool? : null,
isAvailable: data['isAvailable'] is bool? ? data['isAvailable'] as bool? : null,
name: data['name']?.toString(),
);
}
final String? bundlePath;
final String? buildVersion;
final String? platform;
final String? runtimeRoot;
final String? identifier;
final Version? version;
final bool? isInternal;
final bool? isAvailable;
final String? name;
}
/// Launches the device log reader process on the host and parses the syslog.
@visibleForTesting
Future<Process> launchDeviceSystemLogTool(IOSSimulator device) async {
......
......@@ -48,7 +48,8 @@ class Xcode {
_fileSystem = fileSystem,
_xcodeProjectInterpreter = xcodeProjectInterpreter,
_processUtils =
ProcessUtils(logger: logger, processManager: processManager);
ProcessUtils(logger: logger, processManager: processManager),
_logger = logger;
/// Create an [Xcode] for testing.
///
......@@ -60,16 +61,18 @@ class Xcode {
XcodeProjectInterpreter? xcodeProjectInterpreter,
Platform? platform,
FileSystem? fileSystem,
Logger? logger,
}) {
platform ??= FakePlatform(
operatingSystem: 'macos',
environment: <String, String>{},
);
logger ??= BufferLogger.test();
return Xcode(
platform: platform,
processManager: processManager,
fileSystem: fileSystem ?? MemoryFileSystem.test(),
logger: BufferLogger.test(),
logger: logger,
xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
);
}
......@@ -78,6 +81,7 @@ class Xcode {
final ProcessUtils _processUtils;
final FileSystem _fileSystem;
final XcodeProjectInterpreter _xcodeProjectInterpreter;
final Logger _logger;
bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
......@@ -198,6 +202,19 @@ class Xcode {
final String appPath = _fileSystem.path.join(selectPath, 'Applications', 'Simulator.app');
return _fileSystem.directory(appPath).existsSync() ? appPath : null;
}
/// Gets the version number of the platform for the selected SDK.
Future<Version?> sdkPlatformVersion(EnvironmentType environmentType) async {
final RunResult runResult = await _processUtils.run(
<String>[...xcrunCommand(), '--sdk', getSDKNameForIOSEnvironmentType(environmentType), '--show-sdk-platform-version'],
);
if (runResult.exitCode != 0) {
_logger.printError('Could not find SDK Platform Version: ${runResult.stderr}');
return null;
}
final String versionString = runResult.stdout.trim();
return Version.parse(versionString);
}
}
EnvironmentType? environmentTypeFromSdkroot(String sdkroot, FileSystem fileSystem) {
......
......@@ -3,18 +3,32 @@
// found in the LICENSE file.
import '../base/user_messages.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../doctor_validator.dart';
import '../ios/simulators.dart';
import 'xcode.dart';
String _iOSSimulatorMissing(String version) => '''
iOS $version Simulator not installed; this may be necessary for iOS and macOS development.
To download and install the platform, open Xcode, select Xcode > Settings > Platforms,
and click the GET button for the required platform.
For more information, please visit:
https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes''';
class XcodeValidator extends DoctorValidator {
XcodeValidator({
required Xcode xcode,
required IOSSimulatorUtils iosSimulatorUtils,
required UserMessages userMessages,
}) : _xcode = xcode,
_userMessages = userMessages,
super('Xcode - develop for iOS and macOS');
}) : _xcode = xcode,
_iosSimulatorUtils = iosSimulatorUtils,
_userMessages = userMessages,
super('Xcode - develop for iOS and macOS');
final Xcode _xcode;
final IOSSimulatorUtils _iosSimulatorUtils;
final UserMessages _userMessages;
@override
......@@ -57,6 +71,11 @@ class XcodeValidator extends DoctorValidator {
messages.add(ValidationMessage.error(_userMessages.xcodeMissingSimct));
}
final ValidationMessage? missingSimulatorMessage = await _validateSimulatorRuntimeInstalled();
if (missingSimulatorMessage != null) {
xcodeStatus = ValidationType.partial;
messages.add(missingSimulatorMessage);
}
} else {
xcodeStatus = ValidationType.missing;
if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) {
......@@ -68,4 +87,45 @@ class XcodeValidator extends DoctorValidator {
return ValidationResult(xcodeStatus, messages, statusInfo: xcodeVersionInfo);
}
/// Validate the Xcode-installed iOS simulator SDK has a corresponding iOS
/// simulator runtime installed.
///
/// Starting with Xcode 15, the iOS simulator runtime is no longer downloaded
/// with Xcode and must be downloaded and installed separately.
/// iOS applications cannot be run without it.
Future<ValidationMessage?> _validateSimulatorRuntimeInstalled() async {
// Skip this validation if Xcode is not installed, Xcode is a version less
// than 15, simctl is not installed, or if the EULA is not signed.
if (!_xcode.isInstalled ||
_xcode.currentVersion == null ||
_xcode.currentVersion!.major < 15 ||
!_xcode.isSimctlInstalled ||
!_xcode.eulaSigned) {
return null;
}
final Version? platformSDKVersion = await _xcode.sdkPlatformVersion(EnvironmentType.simulator);
if (platformSDKVersion == null) {
return const ValidationMessage.error('Unable to find the iPhone Simulator SDK.');
}
final List<IOSSimulatorRuntime> runtimes = await _iosSimulatorUtils.getAvailableIOSRuntimes();
if (runtimes.isEmpty) {
return const ValidationMessage.error('Unable to get list of installed Simulator runtimes.');
}
// Verify there is a simulator runtime installed matching the
// iphonesimulator SDK major version.
try {
runtimes.firstWhere(
(IOSSimulatorRuntime runtime) =>
runtime.version?.major == platformSDKVersion.major,
);
} on StateError {
return ValidationMessage.hint(_iOSSimulatorMissing(platformSDKVersion.toString()));
}
return null;
}
}
......@@ -8,6 +8,7 @@ import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
......@@ -771,14 +772,16 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text'''
late FakeProcessManager fakeProcessManager;
Xcode xcode;
late SimControl simControl;
late BufferLogger logger;
const String deviceId = 'smart-phone';
const String appId = 'flutterApp';
setUp(() {
fakeProcessManager = FakeProcessManager.empty();
xcode = Xcode.test(processManager: FakeProcessManager.any());
logger = BufferLogger.test();
simControl = SimControl(
logger: BufferLogger.test(),
logger: logger,
processManager: fakeProcessManager,
xcode: xcode,
);
......@@ -931,6 +934,159 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text'''
expect(await iosSimulator.stopApp(null), isFalse);
});
testWithoutContext('listAvailableIOSRuntimes succeeds', () async {
const String validRuntimesOutput = '''
{
"runtimes" : [
{
"bundlePath" : "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime",
"buildversion" : "19E240",
"platform" : "iOS",
"runtimeRoot" : "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime/Contents/Resources/RuntimeRoot",
"identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-15-4",
"version" : "15.4",
"isInternal" : false,
"isAvailable" : true,
"name" : "iOS 15.4",
"supportedDeviceTypes" : [
{
"bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 6s.simdevicetype",
"name" : "iPhone 6s",
"identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s",
"productFamily" : "iPhone"
},
{
"bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 6s Plus.simdevicetype",
"name" : "iPhone 6s Plus",
"identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus",
"productFamily" : "iPhone"
}
]
},
{
"bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime",
"buildversion" : "20E247",
"platform" : "iOS",
"runtimeRoot" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot",
"identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-16-4",
"version" : "16.4",
"isInternal" : false,
"isAvailable" : true,
"name" : "iOS 16.4",
"supportedDeviceTypes" : [
{
"bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8.simdevicetype",
"name" : "iPhone 8",
"identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8",
"productFamily" : "iPhone"
},
{
"bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8 Plus.simdevicetype",
"name" : "iPhone 8 Plus",
"identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus",
"productFamily" : "iPhone"
}
]
},
{
"bundlePath" : "/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime",
"buildversion" : "21A5268h",
"platform" : "iOS",
"runtimeRoot" : "/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime/Contents/Resources/RuntimeRoot",
"identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-17-0",
"version" : "17.0",
"isInternal" : false,
"isAvailable" : true,
"name" : "iOS 17.0",
"supportedDeviceTypes" : [
{
"bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8.simdevicetype",
"name" : "iPhone 8",
"identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8",
"productFamily" : "iPhone"
},
{
"bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8 Plus.simdevicetype",
"name" : "iPhone 8 Plus",
"identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus",
"productFamily" : "iPhone"
}
]
}
]
}
''';
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'xcrun',
'simctl',
'list',
'runtimes',
'available',
'iOS',
'--json',
],
stdout: validRuntimesOutput,
));
final List<IOSSimulatorRuntime> runtimes = await simControl.listAvailableIOSRuntimes();
final IOSSimulatorRuntime runtime1 = runtimes[0];
expect(runtime1.bundlePath, '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime');
expect(runtime1.buildVersion, '19E240');
expect(runtime1.platform, 'iOS');
expect(runtime1.runtimeRoot, '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime/Contents/Resources/RuntimeRoot');
expect(runtime1.identifier, 'com.apple.CoreSimulator.SimRuntime.iOS-15-4');
expect(runtime1.version, Version(15, 4, null));
expect(runtime1.isInternal, false);
expect(runtime1.isAvailable, true);
expect(runtime1.name, 'iOS 15.4');
final IOSSimulatorRuntime runtime2 = runtimes[1];
expect(runtime2.bundlePath, '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime');
expect(runtime2.buildVersion, '20E247');
expect(runtime2.platform, 'iOS');
expect(runtime2.runtimeRoot, '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot');
expect(runtime2.identifier, 'com.apple.CoreSimulator.SimRuntime.iOS-16-4');
expect(runtime2.version, Version(16, 4, null));
expect(runtime2.isInternal, false);
expect(runtime2.isAvailable, true);
expect(runtime2.name, 'iOS 16.4');
final IOSSimulatorRuntime runtime3 = runtimes[2];
expect(runtime3.bundlePath, '/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime');
expect(runtime3.buildVersion, '21A5268h');
expect(runtime3.platform, 'iOS');
expect(runtime3.runtimeRoot, '/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime/Contents/Resources/RuntimeRoot');
expect(runtime3.identifier, 'com.apple.CoreSimulator.SimRuntime.iOS-17-0');
expect(runtime3.version, Version(17, 0, null));
expect(runtime3.isInternal, false);
expect(runtime3.isAvailable, true);
expect(runtime3.name, 'iOS 17.0');
});
testWithoutContext('listAvailableIOSRuntimes handles bad simctl output', () async {
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'xcrun',
'simctl',
'list',
'runtimes',
'available',
'iOS',
'--json',
],
stdout: 'Install Started',
));
final List<IOSSimulatorRuntime> runtimes = await simControl.listAvailableIOSRuntimes();
expect(runtimes, isEmpty);
expect(logger.errorText, contains('simctl returned non-JSON response:'));
expect(fakeProcessManager, hasNoRemainingExpectations);
});
});
group('startApp', () {
......
......@@ -99,12 +99,15 @@ void main() {
group('macOS', () {
late Xcode xcode;
late BufferLogger logger;
setUp(() {
xcodeProjectInterpreter = FakeXcodeProjectInterpreter();
logger = BufferLogger.test();
xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
logger: logger,
);
});
......@@ -277,6 +280,59 @@ void main() {
expect(fakeProcessManager, hasNoRemainingExpectations);
});
});
group('SDK Platform Version', () {
testWithoutContext('--show-sdk-platform-version iphonesimulator', () async {
fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'],
stdout: '16.4',
));
expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), Version(16, 4, null));
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('--show-sdk-platform-version iphonesimulator with leading and trailing new line', () async {
fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'],
stdout: '\n16.4\n',
));
expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), Version(16, 4, null));
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('--show-sdk-platform-version returns version followed by text', () async {
fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'],
stdout: '13.2 (a) 12344',
));
expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), Version(13, 2, null, text: '13.2 (a) 12344'));
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('--show-sdk-platform-version returns something unexpected', () async {
fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'],
stdout: 'bogus',
));
expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), null);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('--show-sdk-platform-version fails', () async {
fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'],
exitCode: 1,
stderr: 'xcrun: error:',
));
expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), null);
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(logger.errorText, contains('Could not find SDK Platform Version'));
});
});
});
});
......
......@@ -5,9 +5,11 @@
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';
......@@ -20,7 +22,11 @@ void main() {
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager, version: null),
);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
final XcodeValidator validator = XcodeValidator(
xcode: xcode,
userMessages: UserMessages(),
iosSimulatorUtils: FakeIOSSimulatorUtils(),
);
final ValidationResult result = await validator.validate();
expect(result.type, ValidationType.missing);
expect(result.statusInfo, isNull);
......@@ -39,7 +45,11 @@ void main() {
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager, version: null),
);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
final XcodeValidator 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);
......@@ -52,7 +62,11 @@ void main() {
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager, version: Version(7, 0, 1)),
);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
final XcodeValidator 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);
......@@ -65,7 +79,11 @@ void main() {
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager, version: Version(12, 4, null)),
);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
final XcodeValidator 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);
......@@ -105,11 +123,16 @@ void main() {
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager),
);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
final XcodeValidator 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 {
......@@ -143,11 +166,156 @@ void main() {
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager),
);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
final XcodeValidator 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(<FakeCommand>[
const FakeCommand(
command: <String>['/usr/bin/xcode-select', '--print-path'],
stdout: '/Library/Developer/CommandLineTools',
),
const FakeCommand(
command: <String>[
'which',
'sysctl',
],
),
const FakeCommand(
command: <String>[
'sysctl',
'hw.optional.arm64',
],
exitCode: 1,
),
const FakeCommand(
command: <String>['xcrun', 'clang'],
),
const FakeCommand(
command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted'],
),
const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'],
),
]);
final Xcode xcode = Xcode.test(
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager),
);
final XcodeValidator 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(<FakeCommand>[
const FakeCommand(
command: <String>['/usr/bin/xcode-select', '--print-path'],
stdout: '/Library/Developer/CommandLineTools',
),
const FakeCommand(
command: <String>[
'which',
'sysctl',
],
),
const FakeCommand(
command: <String>[
'sysctl',
'hw.optional.arm64',
],
exitCode: 1,
),
const FakeCommand(
command: <String>['xcrun', 'clang'],
),
const FakeCommand(
command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted'],
),
const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'],
stdout: '17.0'
),
]);
final Xcode xcode = Xcode.test(
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager),
);
final XcodeValidator 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(<FakeCommand>[
const FakeCommand(
command: <String>['/usr/bin/xcode-select', '--print-path'],
stdout: '/Library/Developer/CommandLineTools',
),
const FakeCommand(
command: <String>[
'which',
'sysctl',
],
),
const FakeCommand(
command: <String>[
'sysctl',
'hw.optional.arm64',
],
exitCode: 1,
),
const FakeCommand(
command: <String>['xcrun', 'clang'],
),
const FakeCommand(
command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted'],
),
const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'],
stdout: '17.0'
),
]);
final Xcode xcode = Xcode.test(
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager),
);
final FakeIOSSimulatorUtils simulatorUtils = FakeIOSSimulatorUtils(runtimes: <IOSSimulatorRuntime>[
IOSSimulatorRuntime.fromJson(<String, String>{'version': '16.0'}),
]);
final XcodeValidator 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 {
......@@ -175,12 +343,23 @@ void main() {
const FakeCommand(
command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted'],
),
const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'],
stdout: '17.0'
),
]);
final Xcode xcode = Xcode.test(
processManager: processManager,
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager),
);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
final FakeIOSSimulatorUtils simulatorUtils = FakeIOSSimulatorUtils(runtimes: <IOSSimulatorRuntime>[
IOSSimulatorRuntime.fromJson(<String, String>{'version': '17.0'}),
]);
final XcodeValidator validator = XcodeValidator(
xcode: xcode,
userMessages: UserMessages(),
iosSimulatorUtils: simulatorUtils,
);
final ValidationResult result = await validator.validate();
expect(result.type, ValidationType.success);
expect(result.messages.length, 2);
......@@ -189,6 +368,24 @@ void main() {
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<IOSSimulatorRuntime>? runtimes;
List<IOSSimulatorRuntime> get _runtimesList {
return runtimes ?? <IOSSimulatorRuntime>[];
}
@override
Future<List<IOSSimulatorRuntime>> getAvailableIOSRuntimes() async {
return _runtimesList;
}
}
......@@ -319,6 +319,9 @@ class NoopIOSSimulatorUtils implements IOSSimulatorUtils {
@override
Future<List<IOSSimulator>> getAttachedDevices() async => <IOSSimulator>[];
@override
Future<List<IOSSimulatorRuntime>> getAvailableIOSRuntimes() async => <IOSSimulatorRuntime>[];
}
class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment