Throw exception if instantiating IOSDevice on non-mac os platform (#36288)

diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 4ed40de..904252c 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -125,14 +125,12 @@
           ephemeral: true,
       ) {
     if (!platform.isMacOS) {
-      printError('Cannot control iOS devices or simulators. ideviceinstaller and iproxy are not available on your platform.');
-      _installerPath = null;
-      _iproxyPath = null;
+      assert(false, 'Control of iOS devices or simulators only supported on Mac OS.');
       return;
     }
     _installerPath = artifacts.getArtifactPath(
       Artifact.ideviceinstaller,
-      platform: TargetPlatform.ios
+      platform: TargetPlatform.ios,
     ) ?? 'ideviceinstaller'; // TODO(fujino): remove fallback once g3 updated
     _iproxyPath = artifacts.getArtifactPath(
       Artifact.iproxy,
@@ -168,6 +166,9 @@
   bool get supportsStartPaused => false;
 
   static Future<List<IOSDevice>> getAttachedDevices() async {
+    if (!platform.isMacOS) {
+      throw UnsupportedError('Control of iOS devices or simulators only supported on Mac OS.');
+    }
     if (!iMobileDevice.isInstalled)
       return <IOSDevice>[];
 
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 f01894e..89075c6 100644
--- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
@@ -38,92 +38,117 @@
 void main() {
   final FakePlatform macPlatform = FakePlatform.fromPlatform(const LocalPlatform());
   macPlatform.operatingSystem = 'macos';
+  final FakePlatform linuxPlatform = FakePlatform.fromPlatform(const LocalPlatform());
+  linuxPlatform.operatingSystem = 'linux';
+  final FakePlatform windowsPlatform = FakePlatform.fromPlatform(const LocalPlatform());
+  windowsPlatform.operatingSystem = 'windows';
 
-  group('Process calls', () {
-    MockIOSApp mockApp;
-    MockArtifacts mockArtifacts;
-    MockCache mockCache;
-    MockFileSystem mockFileSystem;
-    MockProcessManager mockProcessManager;
-    const String installerPath = '/path/to/ideviceinstaller';
-    const String appId = '789';
-    const MapEntry<String, String> libraryEntry = MapEntry<String, String>(
-      'DYLD_LIBRARY_PATH',
-      '/path/to/libraries'
-    );
-    final Map<String, String> env = Map<String, String>.fromEntries(
-      <MapEntry<String, String>>[libraryEntry]
-    );
+  group('IOSDevice', () {
+    final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
 
-    setUp(() {
-      mockApp = MockIOSApp();
-      mockArtifacts = MockArtifacts();
-      mockCache = MockCache();
-      when(mockCache.dyLdLibEntry).thenReturn(libraryEntry);
-      mockFileSystem = MockFileSystem();
-      mockProcessManager = MockProcessManager();
-      when(
-        mockArtifacts.getArtifactPath(
-          Artifact.ideviceinstaller,
-          platform: anyNamed('platform'),
-        )
-      ).thenReturn(installerPath);
+    testUsingContext('successfully instantiates on Mac OS', () {
+      IOSDevice('device-123');
+    }, overrides: <Type, Generator>{
+      Platform: () => macPlatform,
     });
 
-    testUsingContext('installApp() invokes process with correct environment', () async {
-      final IOSDevice device = IOSDevice('123');
-      const String bundlePath = '/path/to/bundle';
-      final List<String> args = <String>[installerPath, '-i', bundlePath];
-      when(mockApp.deviceBundlePath).thenReturn(bundlePath);
-      final MockDirectory directory = MockDirectory();
-      when(mockFileSystem.directory(bundlePath)).thenReturn(directory);
-      when(directory.existsSync()).thenReturn(true);
-      when(mockProcessManager.run(args, environment: env))
-        .thenAnswer(
-          (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
+    for (Platform platform in unsupportedPlatforms) {
+      testUsingContext('throws UnsupportedError exception if instantiated on ${platform.operatingSystem}', () {
+        expect(
+            () { IOSDevice('device-123'); },
+            throwsA(isInstanceOf<AssertionError>())
         );
-      await device.installApp(mockApp);
-      verify(mockProcessManager.run(args, environment: env));
-    }, overrides: <Type, Generator>{
-      Artifacts: () => mockArtifacts,
-      Cache: () => mockCache,
-      FileSystem: () => mockFileSystem,
-      Platform: () => macPlatform,
-      ProcessManager: () => mockProcessManager,
-    });
+      }, overrides: <Type, Generator>{
+        Platform: () => platform,
+      });
+    }
 
-    testUsingContext('isAppInstalled() invokes process with correct environment', () async {
-      final IOSDevice device = IOSDevice('123');
-      final List<String> args = <String>[installerPath, '--list-apps'];
-      when(mockProcessManager.run(args, environment: env))
-        .thenAnswer(
-          (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
-        );
-      when(mockApp.id).thenReturn(appId);
-      await device.isAppInstalled(mockApp);
-      verify(mockProcessManager.run(args, environment: env));
-    }, overrides: <Type, Generator>{
-      Artifacts: () => mockArtifacts,
-      Cache: () => mockCache,
-      Platform: () => macPlatform,
-      ProcessManager: () => mockProcessManager,
-    });
+    group('Process calls', () {
+      MockIOSApp mockApp;
+      MockArtifacts mockArtifacts;
+      MockCache mockCache;
+      MockFileSystem mockFileSystem;
+      MockProcessManager mockProcessManager;
+      const String installerPath = '/path/to/ideviceinstaller';
+      const String appId = '789';
+      const MapEntry<String, String> libraryEntry = MapEntry<String, String>(
+          'DYLD_LIBRARY_PATH',
+          '/path/to/libraries'
+      );
+      final Map<String, String> env = Map<String, String>.fromEntries(
+          <MapEntry<String, String>>[libraryEntry]
+      );
 
-    testUsingContext('uninstallApp() invokes process with correct environment', () async {
-      final IOSDevice device = IOSDevice('123');
-      final List<String> args = <String>[installerPath, '-U', appId];
-      when(mockApp.id).thenReturn(appId);
-      when(mockProcessManager.run(args, environment: env))
-        .thenAnswer(
-          (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
-        );
-      await device.uninstallApp(mockApp);
-      verify(mockProcessManager.run(args, environment: env));
-    }, overrides: <Type, Generator>{
-      Artifacts: () => mockArtifacts,
-      Cache: () => mockCache,
-      Platform: () => macPlatform,
-      ProcessManager: () => mockProcessManager,
+      setUp(() {
+        mockApp = MockIOSApp();
+        mockArtifacts = MockArtifacts();
+        mockCache = MockCache();
+        when(mockCache.dyLdLibEntry).thenReturn(libraryEntry);
+        mockFileSystem = MockFileSystem();
+        mockProcessManager = MockProcessManager();
+        when(
+            mockArtifacts.getArtifactPath(
+                Artifact.ideviceinstaller,
+                platform: anyNamed('platform'),
+            )
+        ).thenReturn(installerPath);
+      });
+
+      testUsingContext('installApp() invokes process with correct environment', () async {
+        final IOSDevice device = IOSDevice('123');
+        const String bundlePath = '/path/to/bundle';
+        final List<String> args = <String>[installerPath, '-i', bundlePath];
+        when(mockApp.deviceBundlePath).thenReturn(bundlePath);
+        final MockDirectory directory = MockDirectory();
+        when(mockFileSystem.directory(bundlePath)).thenReturn(directory);
+        when(directory.existsSync()).thenReturn(true);
+        when(mockProcessManager.run(args, environment: env))
+            .thenAnswer(
+                (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
+            );
+        await device.installApp(mockApp);
+        verify(mockProcessManager.run(args, environment: env));
+      }, overrides: <Type, Generator>{
+        Artifacts: () => mockArtifacts,
+        Cache: () => mockCache,
+        FileSystem: () => mockFileSystem,
+        Platform: () => macPlatform,
+        ProcessManager: () => mockProcessManager,
+      });
+
+      testUsingContext('isAppInstalled() invokes process with correct environment', () async {
+        final IOSDevice device = IOSDevice('123');
+        final List<String> args = <String>[installerPath, '--list-apps'];
+        when(mockProcessManager.run(args, environment: env))
+            .thenAnswer(
+                (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
+            );
+        when(mockApp.id).thenReturn(appId);
+        await device.isAppInstalled(mockApp);
+        verify(mockProcessManager.run(args, environment: env));
+      }, overrides: <Type, Generator>{
+        Artifacts: () => mockArtifacts,
+        Cache: () => mockCache,
+        Platform: () => macPlatform,
+        ProcessManager: () => mockProcessManager,
+      });
+
+      testUsingContext('uninstallApp() invokes process with correct environment', () async {
+        final IOSDevice device = IOSDevice('123');
+        final List<String> args = <String>[installerPath, '-U', appId];
+        when(mockApp.id).thenReturn(appId);
+        when(mockProcessManager.run(args, environment: env))
+            .thenAnswer(
+                (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
+            );
+        await device.uninstallApp(mockApp);
+        verify(mockProcessManager.run(args, environment: env));
+      }, overrides: <Type, Generator>{
+        Artifacts: () => mockArtifacts,
+        Cache: () => mockCache,
+        Platform: () => macPlatform,
+        ProcessManager: () => mockProcessManager,
+      });
     });
   });
 
@@ -139,6 +164,7 @@
       expect(await IOSDevice.getAttachedDevices(), isEmpty);
     }, overrides: <Type, Generator>{
       IMobileDevice: () => mockIMobileDevice,
+      Platform: () => macPlatform,
     });
 
     testUsingContext('returns no devices if none are attached', () async {
@@ -149,8 +175,25 @@
       expect(devices, isEmpty);
     }, overrides: <Type, Generator>{
       IMobileDevice: () => mockIMobileDevice,
+      Platform: () => macPlatform,
     });
 
+    final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
+    for (Platform platform in unsupportedPlatforms) {
+      testUsingContext('throws Unsupported Operation exception on ${platform.operatingSystem}', () async {
+        when(iMobileDevice.isInstalled).thenReturn(false);
+        when(iMobileDevice.getAvailableDeviceIDs())
+            .thenAnswer((Invocation invocation) => Future<String>.value(''));
+        expect(
+            () async { await IOSDevice.getAttachedDevices(); },
+            throwsA(isInstanceOf<UnsupportedError>()),
+        );
+      }, overrides: <Type, Generator>{
+        IMobileDevice: () => mockIMobileDevice,
+        Platform: () => platform,
+      });
+    }
+
     testUsingContext('returns attached devices', () async {
       when(iMobileDevice.isInstalled).thenReturn(true);
       when(iMobileDevice.getAvailableDeviceIDs())
@@ -174,6 +217,7 @@
       expect(devices[1].name, 'Puits sans fond');
     }, overrides: <Type, Generator>{
       IMobileDevice: () => mockIMobileDevice,
+      Platform: () => macPlatform,
     });
 
     testUsingContext('returns attached devices and ignores devices that cannot be found by ideviceinfo', () async {
@@ -193,6 +237,7 @@
       expect(devices[0].name, 'La tele me regarde');
     }, overrides: <Type, Generator>{
       IMobileDevice: () => mockIMobileDevice,
+      Platform: () => macPlatform,
     });
   });
 
@@ -244,8 +289,8 @@
       expect(lines, <String>['A is for ari', 'I is for ichigo']);
     }, overrides: <Type, Generator>{
       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();
@@ -280,9 +325,9 @@
       expect(device.category, Category.mobile);
     }, overrides: <Type, Generator>{
       IMobileDevice: () => mockIMobileDevice,
+      Platform: () => macPlatform,
     });
   });
-
   testUsingContext('IOSDevice.isSupportedForProject is true on module project', () async {
     fs.file('pubspec.yaml')
       ..createSync()
@@ -298,8 +343,8 @@
     expect(IOSDevice('test').isSupportedForProject(flutterProject), true);
   }, overrides: <Type, Generator>{
     FileSystem: () => MemoryFileSystem(),
+    Platform: () => macPlatform,
   });
-
   testUsingContext('IOSDevice.isSupportedForProject is true with editable host app', () async {
     fs.file('pubspec.yaml').createSync();
     fs.file('.packages').createSync();
@@ -309,6 +354,7 @@
     expect(IOSDevice('test').isSupportedForProject(flutterProject), true);
   }, overrides: <Type, Generator>{
     FileSystem: () => MemoryFileSystem(),
+    Platform: () => macPlatform,
   });
 
   testUsingContext('IOSDevice.isSupportedForProject is false with no host app and no module', () async {
@@ -319,5 +365,6 @@
     expect(IOSDevice('test').isSupportedForProject(flutterProject), false);
   }, overrides: <Type, Generator>{
     FileSystem: () => MemoryFileSystem(),
+    Platform: () => macPlatform,
   });
 }