From 898ab2479e85e43af0dc6f8f77bf151eaefe6dcf Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Tue, 25 Feb 2025 14:24:49 -0800 Subject: [PATCH] Check if simctl is installed before trying to list devices or runtimes (#163895) After https://github.com/flutter/flutter/pull/163785 there was still an unexpected `xcrun simctl` output on a machine with Xcode only half installed https://github.com/flutter/flutter/issues/161655. Check `simctl` is installed before trying to list booted simulator devices or runtimes. Should address https://github.com/flutter/flutter/issues/161655. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter_tools/lib/src/ios/simulators.dart | 4 +- .../general.shard/ios/simulators_test.dart | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index ffe426a0a20..8773ac2ed42 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -66,7 +66,7 @@ class IOSSimulatorUtils { final Xcode _xcode; Future> getAttachedDevices() async { - if (!_xcode.isInstalledAndMeetsVersionCheck) { + if (!_xcode.isInstalledAndMeetsVersionCheck || !_xcode.isSimctlInstalled) { return []; } @@ -96,7 +96,7 @@ class IOSSimulatorUtils { } Future> getAvailableIOSRuntimes() async { - if (!_xcode.isInstalledAndMeetsVersionCheck) { + if (!_xcode.isInstalledAndMeetsVersionCheck || !_xcode.isSimctlInstalled) { return []; } diff --git a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart index 82e25f96b5d..edf3d314f6d 100644 --- a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart @@ -894,7 +894,10 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''', late FakeProcessManager fakeProcessManager; Xcode xcode; + Xcode xcodeBadSimctl; late SimControl simControl; + late IOSSimulatorUtils simulatorUtils; + late IOSSimulatorUtils simulatorUtilsBadSimctl; late BufferLogger logger; const String deviceId = 'smart-phone'; const String appId = 'flutterApp'; @@ -902,8 +905,32 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''', setUp(() { fakeProcessManager = FakeProcessManager.empty(); xcode = Xcode.test(processManager: FakeProcessManager.any()); + + final FakeProcessManager fakeProcessManagerBadSimctl = FakeProcessManager.list([ + const FakeCommand(command: ['which', 'sysctl']), + const FakeCommand( + command: ['sysctl', 'hw.optional.arm64'], + stdout: 'hw.optional.arm64: 0', + ), + const FakeCommand( + command: ['xcrun', 'simctl', 'list', 'devices', 'booted'], + stderr: 'failed to run', + exitCode: 1, + ), + ]); + xcodeBadSimctl = Xcode.test(processManager: fakeProcessManagerBadSimctl); logger = BufferLogger.test(); simControl = SimControl(logger: logger, processManager: fakeProcessManager, xcode: xcode); + simulatorUtils = IOSSimulatorUtils( + logger: logger, + processManager: fakeProcessManager, + xcode: xcode, + ); + simulatorUtilsBadSimctl = IOSSimulatorUtils( + logger: logger, + processManager: fakeProcessManager, + xcode: xcodeBadSimctl, + ); }); testWithoutContext('getConnectedDevices succeeds', () async { @@ -933,6 +960,33 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''', expect(fakeProcessManager, hasNoRemainingExpectations); }); + testWithoutContext('IOSSimulatorUtils.getAttachedDevices succeeds', () async { + fakeProcessManager.addCommand( + const FakeCommand( + command: ['xcrun', 'simctl', 'list', 'devices', 'booted', 'iOS', '--json'], + stdout: validSimControlOutput, + ), + ); + + final List devices = await simulatorUtils.getAttachedDevices(); + + final IOSSimulator phone1 = devices[0]; + expect(phone1.category, Category.mobile); + expect(phone1.name, 'iPhone 11'); + expect(phone1.simulatorCategory, 'com.apple.CoreSimulator.SimRuntime.iOS-14-0'); + + final IOSSimulator phone2 = devices[1]; + expect(phone2.category, Category.mobile); + expect(phone2.name, 'Phone w Watch'); + expect(phone2.simulatorCategory, 'com.apple.CoreSimulator.SimRuntime.iOS-16-0'); + + final IOSSimulator phone3 = devices[2]; + expect(phone3.category, Category.mobile); + expect(phone3.name, 'iPhone 13'); + expect(phone3.simulatorCategory, 'com.apple.CoreSimulator.SimRuntime.iOS-16-0'); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + testWithoutContext('getConnectedDevices handles bad simctl output', () async { fakeProcessManager.addCommand( const FakeCommand( @@ -947,6 +1001,16 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''', expect(fakeProcessManager, hasNoRemainingExpectations); }); + testWithoutContext( + 'IOSSimulatorUtils.getAttachedDevices handles simctl not properly installed', + () async { + final List devices = await simulatorUtilsBadSimctl.getAttachedDevices(); + + expect(devices, isEmpty); + expect(fakeProcessManager, hasNoRemainingExpectations); + }, + ); + testWithoutContext('sdkMajorVersion defaults to 11 when sdkNameAndVersion is junk', () async { final IOSSimulator iosSimulatorA = IOSSimulator( 'x', @@ -1186,6 +1250,17 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''', expect(logger.errorText, contains('simctl returned non-JSON response:')); expect(fakeProcessManager, hasNoRemainingExpectations); }); + + testWithoutContext( + 'IOSSimulatorUtils.getAvailableIOSRuntimes handles simctl not properly installed', + () async { + final List runtimes = + await simulatorUtilsBadSimctl.getAvailableIOSRuntimes(); + + expect(runtimes, isEmpty); + expect(fakeProcessManager, hasNoRemainingExpectations); + }, + ); }); group('startApp', () {