| // 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. |
| |
| // @dart = 2.8 |
| |
| import 'dart:async'; |
| |
| import 'package:file/file.dart'; |
| import 'package:file/memory.dart'; |
| import 'package:flutter_tools/src/application_package.dart'; |
| import 'package:flutter_tools/src/artifacts.dart'; |
| import 'package:flutter_tools/src/base/file_system.dart'; |
| import 'package:flutter_tools/src/base/io.dart'; |
| import 'package:flutter_tools/src/base/logger.dart'; |
| import 'package:flutter_tools/src/base/os.dart'; |
| import 'package:flutter_tools/src/base/platform.dart'; |
| import 'package:flutter_tools/src/build_info.dart'; |
| import 'package:flutter_tools/src/cache.dart'; |
| import 'package:flutter_tools/src/device.dart'; |
| import 'package:flutter_tools/src/device_port_forwader.dart'; |
| import 'package:flutter_tools/src/ios/devices.dart'; |
| import 'package:flutter_tools/src/ios/ios_deploy.dart'; |
| import 'package:flutter_tools/src/ios/ios_workflow.dart'; |
| import 'package:flutter_tools/src/ios/iproxy.dart'; |
| import 'package:flutter_tools/src/ios/mac.dart'; |
| import 'package:flutter_tools/src/macos/xcode.dart'; |
| import 'package:mockito/mockito.dart'; |
| |
| import '../../src/common.dart'; |
| import '../../src/context.dart'; |
| |
| void main() { |
| final FakePlatform macPlatform = FakePlatform(operatingSystem: 'macos'); |
| final FakePlatform linuxPlatform = FakePlatform(operatingSystem: 'linux'); |
| final FakePlatform windowsPlatform = FakePlatform(operatingSystem: 'windows'); |
| |
| group('IOSDevice', () { |
| final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform]; |
| Cache cache; |
| Logger logger; |
| IOSDeploy iosDeploy; |
| IMobileDevice iMobileDevice; |
| FileSystem nullFileSystem; |
| |
| setUp(() { |
| final Artifacts artifacts = Artifacts.test(); |
| cache = Cache.test(); |
| logger = BufferLogger.test(); |
| iosDeploy = IOSDeploy( |
| artifacts: artifacts, |
| cache: cache, |
| logger: logger, |
| platform: macPlatform, |
| processManager: FakeProcessManager.any(), |
| ); |
| iMobileDevice = IMobileDevice( |
| artifacts: artifacts, |
| cache: cache, |
| logger: logger, |
| processManager: FakeProcessManager.any(), |
| ); |
| }); |
| |
| testWithoutContext('successfully instantiates on Mac OS', () { |
| IOSDevice( |
| 'device-123', |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: macPlatform, |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| name: 'iPhone 1', |
| sdkVersion: '13.3', |
| cpuArchitecture: DarwinArch.arm64, |
| interfaceType: IOSDeviceInterface.usb, |
| ); |
| }); |
| |
| testWithoutContext('parses major version', () { |
| expect(IOSDevice( |
| 'device-123', |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: macPlatform, |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| name: 'iPhone 1', |
| cpuArchitecture: DarwinArch.arm64, |
| sdkVersion: '1.0.0', |
| interfaceType: IOSDeviceInterface.usb, |
| ).majorSdkVersion, 1); |
| expect(IOSDevice( |
| 'device-123', |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: macPlatform, |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| name: 'iPhone 1', |
| cpuArchitecture: DarwinArch.arm64, |
| sdkVersion: '13.1.1', |
| interfaceType: IOSDeviceInterface.usb, |
| ).majorSdkVersion, 13); |
| expect(IOSDevice( |
| 'device-123', |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: macPlatform, |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| name: 'iPhone 1', |
| cpuArchitecture: DarwinArch.arm64, |
| sdkVersion: '10', |
| interfaceType: IOSDeviceInterface.usb, |
| ).majorSdkVersion, 10); |
| expect(IOSDevice( |
| 'device-123', |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: macPlatform, |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| name: 'iPhone 1', |
| cpuArchitecture: DarwinArch.arm64, |
| sdkVersion: '0', |
| interfaceType: IOSDeviceInterface.usb, |
| ).majorSdkVersion, 0); |
| expect(IOSDevice( |
| 'device-123', |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: macPlatform, |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| name: 'iPhone 1', |
| cpuArchitecture: DarwinArch.arm64, |
| sdkVersion: 'bogus', |
| interfaceType: IOSDeviceInterface.usb, |
| ).majorSdkVersion, 0); |
| }); |
| |
| testWithoutContext('Supports debug, profile, and release modes', () { |
| final IOSDevice device = IOSDevice( |
| 'device-123', |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: macPlatform, |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| name: 'iPhone 1', |
| sdkVersion: '13.3', |
| cpuArchitecture: DarwinArch.arm64, |
| interfaceType: IOSDeviceInterface.usb, |
| ); |
| |
| expect(device.supportsRuntimeMode(BuildMode.debug), true); |
| expect(device.supportsRuntimeMode(BuildMode.profile), true); |
| expect(device.supportsRuntimeMode(BuildMode.release), true); |
| expect(device.supportsRuntimeMode(BuildMode.jitRelease), false); |
| }); |
| |
| for (final Platform platform in unsupportedPlatforms) { |
| testWithoutContext('throws UnsupportedError exception if instantiated on ${platform.operatingSystem}', () { |
| expect( |
| () { |
| IOSDevice( |
| 'device-123', |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: platform, |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| name: 'iPhone 1', |
| sdkVersion: '13.3', |
| cpuArchitecture: DarwinArch.arm64, |
| interfaceType: IOSDeviceInterface.usb, |
| ); |
| }, |
| throwsAssertionError, |
| ); |
| }); |
| } |
| |
| group('.dispose()', () { |
| IOSDevice device; |
| MockIOSApp appPackage1; |
| MockIOSApp appPackage2; |
| IOSDeviceLogReader logReader1; |
| IOSDeviceLogReader logReader2; |
| MockProcess mockProcess1; |
| MockProcess mockProcess2; |
| MockProcess mockProcess3; |
| IOSDevicePortForwarder portForwarder; |
| ForwardedPort forwardedPort; |
| Cache cache; |
| Logger logger; |
| IOSDeploy iosDeploy; |
| FileSystem nullFileSystem; |
| IProxy iproxy; |
| |
| IOSDevicePortForwarder createPortForwarder( |
| ForwardedPort forwardedPort, |
| IOSDevice device) { |
| iproxy = IProxy.test(logger: logger, processManager: FakeProcessManager.any()); |
| final IOSDevicePortForwarder portForwarder = IOSDevicePortForwarder( |
| id: device.id, |
| logger: logger, |
| operatingSystemUtils: OperatingSystemUtils( |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: FakePlatform(operatingSystem: 'macos'), |
| processManager: FakeProcessManager.any(), |
| ), |
| iproxy: iproxy, |
| ); |
| portForwarder.addForwardedPorts(<ForwardedPort>[forwardedPort]); |
| return portForwarder; |
| } |
| |
| IOSDeviceLogReader createLogReader( |
| IOSDevice device, |
| IOSApp appPackage, |
| Process process) { |
| final IOSDeviceLogReader logReader = IOSDeviceLogReader.create( |
| device: device, |
| app: appPackage, |
| iMobileDevice: null, // not used by this test. |
| ); |
| logReader.idevicesyslogProcess = process; |
| return logReader; |
| } |
| |
| setUp(() { |
| appPackage1 = MockIOSApp(); |
| appPackage2 = MockIOSApp(); |
| when(appPackage1.name).thenReturn('flutterApp1'); |
| when(appPackage2.name).thenReturn('flutterApp2'); |
| mockProcess1 = MockProcess(); |
| mockProcess2 = MockProcess(); |
| mockProcess3 = MockProcess(); |
| forwardedPort = ForwardedPort.withContext(123, 456, mockProcess3); |
| cache = Cache.test(); |
| iosDeploy = IOSDeploy( |
| artifacts: Artifacts.test(), |
| cache: cache, |
| logger: logger, |
| platform: macPlatform, |
| processManager: FakeProcessManager.any(), |
| ); |
| }); |
| |
| testWithoutContext('kills all log readers & port forwarders', () async { |
| device = IOSDevice( |
| '123', |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| fileSystem: nullFileSystem, |
| logger: logger, |
| platform: macPlatform, |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| name: 'iPhone 1', |
| sdkVersion: '13.3', |
| cpuArchitecture: DarwinArch.arm64, |
| interfaceType: IOSDeviceInterface.usb, |
| ); |
| logReader1 = createLogReader(device, appPackage1, mockProcess1); |
| logReader2 = createLogReader(device, appPackage2, mockProcess2); |
| portForwarder = createPortForwarder(forwardedPort, device); |
| device.setLogReader(appPackage1, logReader1); |
| device.setLogReader(appPackage2, logReader2); |
| device.portForwarder = portForwarder; |
| |
| await device.dispose(); |
| |
| verify(mockProcess1.kill()); |
| verify(mockProcess2.kill()); |
| verify(mockProcess3.kill()); |
| }); |
| }); |
| }); |
| |
| group('polling', () { |
| MockXcdevice mockXcdevice; |
| Cache cache; |
| FakeProcessManager fakeProcessManager; |
| BufferLogger logger; |
| IOSDeploy iosDeploy; |
| IMobileDevice iMobileDevice; |
| IOSWorkflow mockIosWorkflow; |
| IOSDevice device1; |
| IOSDevice device2; |
| |
| setUp(() { |
| mockXcdevice = MockXcdevice(); |
| final Artifacts artifacts = Artifacts.test(); |
| cache = Cache.test(); |
| logger = BufferLogger.test(); |
| mockIosWorkflow = MockIOSWorkflow(); |
| fakeProcessManager = FakeProcessManager.any(); |
| iosDeploy = IOSDeploy( |
| artifacts: artifacts, |
| cache: cache, |
| logger: logger, |
| platform: macPlatform, |
| processManager: fakeProcessManager, |
| ); |
| iMobileDevice = IMobileDevice( |
| artifacts: artifacts, |
| cache: cache, |
| processManager: fakeProcessManager, |
| logger: logger, |
| ); |
| |
| device1 = IOSDevice( |
| 'd83d5bc53967baa0ee18626ba87b6254b2ab5418', |
| name: 'Paired iPhone', |
| sdkVersion: '13.3', |
| cpuArchitecture: DarwinArch.arm64, |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| logger: logger, |
| platform: macPlatform, |
| fileSystem: MemoryFileSystem.test(), |
| interfaceType: IOSDeviceInterface.usb, |
| ); |
| |
| device2 = IOSDevice( |
| '00008027-00192736010F802E', |
| name: 'iPad Pro', |
| sdkVersion: '13.3', |
| cpuArchitecture: DarwinArch.arm64, |
| iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), |
| iosDeploy: iosDeploy, |
| iMobileDevice: iMobileDevice, |
| logger: logger, |
| platform: macPlatform, |
| fileSystem: MemoryFileSystem.test(), |
| interfaceType: IOSDeviceInterface.usb, |
| ); |
| }); |
| |
| testWithoutContext('start polling without Xcode', () async { |
| final IOSDevices iosDevices = IOSDevices( |
| platform: macPlatform, |
| xcdevice: mockXcdevice, |
| iosWorkflow: mockIosWorkflow, |
| logger: logger, |
| ); |
| when(mockXcdevice.isInstalled).thenReturn(false); |
| |
| await iosDevices.startPolling(); |
| verifyNever(mockXcdevice.getAvailableIOSDevices()); |
| }); |
| |
| testWithoutContext('start polling', () async { |
| final IOSDevices iosDevices = IOSDevices( |
| platform: macPlatform, |
| xcdevice: mockXcdevice, |
| iosWorkflow: mockIosWorkflow, |
| logger: logger, |
| ); |
| when(mockXcdevice.isInstalled).thenReturn(true); |
| |
| int fetchDevicesCount = 0; |
| when(mockXcdevice.getAvailableIOSDevices()) |
| .thenAnswer((Invocation invocation) { |
| if (fetchDevicesCount == 0) { |
| // Initial time, no devices. |
| fetchDevicesCount++; |
| return Future<List<IOSDevice>>.value(<IOSDevice>[]); |
| } else if (fetchDevicesCount == 1) { |
| // Simulate 2 devices added later. |
| fetchDevicesCount++; |
| return Future<List<IOSDevice>>.value(<IOSDevice>[device1, device2]); |
| } |
| fail('Too many calls to getAvailableTetheredIOSDevices'); |
| }); |
| |
| int addedCount = 0; |
| final Completer<void> added = Completer<void>(); |
| iosDevices.onAdded.listen((Device device) { |
| addedCount++; |
| // 2 devices will be added. |
| // Will throw over-completion if called more than twice. |
| if (addedCount >= 2) { |
| added.complete(); |
| } |
| }); |
| |
| final Completer<void> removed = Completer<void>(); |
| iosDevices.onRemoved.listen((Device device) { |
| // Will throw over-completion if called more than once. |
| removed.complete(); |
| }); |
| |
| final StreamController<Map<XCDeviceEvent, String>> eventStream = StreamController<Map<XCDeviceEvent, String>>(); |
| when(mockXcdevice.observedDeviceEvents()).thenAnswer((_) => eventStream.stream); |
| |
| await iosDevices.startPolling(); |
| verify(mockXcdevice.getAvailableIOSDevices()).called(1); |
| |
| expect(iosDevices.deviceNotifier.items, isEmpty); |
| expect(eventStream.hasListener, isTrue); |
| |
| eventStream.add(<XCDeviceEvent, String>{ |
| XCDeviceEvent.attach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418' |
| }); |
| await added.future; |
| expect(iosDevices.deviceNotifier.items.length, 2); |
| expect(iosDevices.deviceNotifier.items, contains(device1)); |
| expect(iosDevices.deviceNotifier.items, contains(device2)); |
| |
| eventStream.add(<XCDeviceEvent, String>{ |
| XCDeviceEvent.detach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418' |
| }); |
| await removed.future; |
| expect(iosDevices.deviceNotifier.items, <Device>[device2]); |
| |
| // Remove stream will throw over-completion if called more than once |
| // which proves this is ignored. |
| eventStream.add(<XCDeviceEvent, String>{ |
| XCDeviceEvent.detach: 'bogus' |
| }); |
| |
| expect(addedCount, 2); |
| |
| await iosDevices.stopPolling(); |
| |
| expect(eventStream.hasListener, isFalse); |
| }); |
| |
| testWithoutContext('polling can be restarted if stream is closed', () async { |
| final IOSDevices iosDevices = IOSDevices( |
| platform: macPlatform, |
| xcdevice: mockXcdevice, |
| iosWorkflow: mockIosWorkflow, |
| logger: logger, |
| ); |
| when(mockXcdevice.isInstalled).thenReturn(true); |
| |
| when(mockXcdevice.getAvailableIOSDevices()) |
| .thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[])); |
| |
| final StreamController<Map<XCDeviceEvent, String>> eventStream = StreamController<Map<XCDeviceEvent, String>>(); |
| final StreamController<Map<XCDeviceEvent, String>> rescheduledStream = StreamController<Map<XCDeviceEvent, String>>(); |
| |
| bool reschedule = false; |
| when(mockXcdevice.observedDeviceEvents()).thenAnswer((Invocation invocation) { |
| if (!reschedule) { |
| reschedule = true; |
| return eventStream.stream; |
| } |
| return rescheduledStream.stream; |
| }); |
| |
| await iosDevices.startPolling(); |
| expect(eventStream.hasListener, isTrue); |
| verify(mockXcdevice.getAvailableIOSDevices()).called(1); |
| |
| // Pretend xcdevice crashed. |
| await eventStream.close(); |
| expect(logger.traceText, contains('xcdevice observe stopped')); |
| |
| // Confirm a restart still gets streamed events. |
| await iosDevices.startPolling(); |
| |
| expect(eventStream.hasListener, isFalse); |
| expect(rescheduledStream.hasListener, isTrue); |
| |
| await iosDevices.stopPolling(); |
| expect(rescheduledStream.hasListener, isFalse); |
| }); |
| |
| testWithoutContext('dispose cancels polling subscription', () async { |
| final IOSDevices iosDevices = IOSDevices( |
| platform: macPlatform, |
| xcdevice: mockXcdevice, |
| iosWorkflow: mockIosWorkflow, |
| logger: logger, |
| ); |
| when(mockXcdevice.isInstalled).thenReturn(true); |
| when(mockXcdevice.getAvailableIOSDevices()) |
| .thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[])); |
| |
| final StreamController<Map<XCDeviceEvent, String>> eventStream = StreamController<Map<XCDeviceEvent, String>>(); |
| when(mockXcdevice.observedDeviceEvents()).thenAnswer((_) => eventStream.stream); |
| |
| await iosDevices.startPolling(); |
| expect(iosDevices.deviceNotifier.items, isEmpty); |
| expect(eventStream.hasListener, isTrue); |
| |
| iosDevices.dispose(); |
| expect(eventStream.hasListener, isFalse); |
| }); |
| |
| final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform]; |
| for (final Platform unsupportedPlatform in unsupportedPlatforms) { |
| testWithoutContext('pollingGetDevices throws Unsupported Operation exception on ${unsupportedPlatform.operatingSystem}', () async { |
| final IOSDevices iosDevices = IOSDevices( |
| platform: unsupportedPlatform, |
| xcdevice: mockXcdevice, |
| iosWorkflow: mockIosWorkflow, |
| logger: logger, |
| ); |
| when(mockXcdevice.isInstalled).thenReturn(false); |
| expect( |
| () async { await iosDevices.pollingGetDevices(); }, |
| throwsA(isA<UnsupportedError>()), |
| ); |
| }); |
| } |
| |
| testWithoutContext('pollingGetDevices returns attached devices', () async { |
| final IOSDevices iosDevices = IOSDevices( |
| platform: macPlatform, |
| xcdevice: mockXcdevice, |
| iosWorkflow: mockIosWorkflow, |
| logger: logger, |
| ); |
| when(mockXcdevice.isInstalled).thenReturn(true); |
| |
| when(mockXcdevice.getAvailableIOSDevices()) |
| .thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[device1])); |
| |
| final List<Device> devices = await iosDevices.pollingGetDevices(); |
| expect(devices, hasLength(1)); |
| expect(identical(devices.first, device1), isTrue); |
| }); |
| }); |
| |
| group('getDiagnostics', () { |
| MockXcdevice mockXcdevice; |
| IOSWorkflow mockIosWorkflow; |
| Logger logger; |
| |
| setUp(() { |
| mockXcdevice = MockXcdevice(); |
| mockIosWorkflow = MockIOSWorkflow(); |
| logger = BufferLogger.test(); |
| }); |
| |
| final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform]; |
| for (final Platform unsupportedPlatform in unsupportedPlatforms) { |
| testWithoutContext('throws returns platform diagnostic exception on ${unsupportedPlatform.operatingSystem}', () async { |
| final IOSDevices iosDevices = IOSDevices( |
| platform: unsupportedPlatform, |
| xcdevice: mockXcdevice, |
| iosWorkflow: mockIosWorkflow, |
| logger: logger, |
| ); |
| when(mockXcdevice.isInstalled).thenReturn(false); |
| expect((await iosDevices.getDiagnostics()).first, 'Control of iOS devices or simulators only supported on macOS.'); |
| }); |
| } |
| |
| testWithoutContext('returns diagnostics', () async { |
| final IOSDevices iosDevices = IOSDevices( |
| platform: macPlatform, |
| xcdevice: mockXcdevice, |
| iosWorkflow: mockIosWorkflow, |
| logger: logger, |
| ); |
| when(mockXcdevice.isInstalled).thenReturn(true); |
| when(mockXcdevice.getDiagnostics()) |
| .thenAnswer((Invocation invocation) => Future<List<String>>.value(<String>['Generic pairing error'])); |
| |
| final List<String> diagnostics = await iosDevices.getDiagnostics(); |
| expect(diagnostics, hasLength(1)); |
| expect(diagnostics.first, 'Generic pairing error'); |
| }); |
| }); |
| } |
| |
| class MockIOSApp extends Mock implements IOSApp {} |
| class MockIOSWorkflow extends Mock implements IOSWorkflow {} |
| class MockXcdevice extends Mock implements XCDevice {} |
| class MockProcess extends Mock implements Process {} |