// 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 'dart:async'; import 'dart:io' show ProcessResult, Process; import 'package:file/file.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/dart.dart'; import 'package:flutter_tools/src/build_system/targets/icon_tree_shaker.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/plist_parser.dart'; import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/mocks.dart'; class MockFile extends Mock implements File {} class MockIMobileDevice extends Mock implements IMobileDevice {} class MockProcess extends Mock implements Process {} class MockProcessManager extends Mock implements ProcessManager {} class MockXcode extends Mock implements Xcode {} class MockSimControl extends Mock implements SimControl {} class MockPlistUtils extends Mock implements PlistParser {} void main() { FakePlatform osx; setUp(() { osx = FakePlatform.fromPlatform(const LocalPlatform()); osx.operatingSystem = 'macos'; }); group('_IOSSimulatorDevicePortForwarder', () { testUsingContext('dispose() does not throw an exception', () async { final IOSSimulator simulator = IOSSimulator('123'); final DevicePortForwarder portForwarder = simulator.portForwarder; await portForwarder.forward(123); await portForwarder.forward(124); expect(portForwarder.forwardedPorts.length, 2); try { await portForwarder.dispose(); } on Exception catch (e) { fail('Encountered exception: $e'); } expect(portForwarder.forwardedPorts.length, 0); }, overrides: { Platform: () => osx, }, testOn: 'posix'); }); group('logFilePath', () { testUsingContext('defaults to rooted from HOME', () { osx.environment['HOME'] = '/foo/bar'; expect(IOSSimulator('123').logFilePath, '/foo/bar/Library/Logs/CoreSimulator/123/system.log'); }, overrides: { Platform: () => osx, }, testOn: 'posix'); testUsingContext('respects IOS_SIMULATOR_LOG_FILE_PATH', () { osx.environment['HOME'] = '/foo/bar'; osx.environment['IOS_SIMULATOR_LOG_FILE_PATH'] = '/baz/qux/%{id}/system.log'; expect(IOSSimulator('456').logFilePath, '/baz/qux/456/system.log'); }, overrides: { Platform: () => osx, }); }); group('compareIosVersions', () { test('compares correctly', () { // This list must be sorted in ascending preference order final List testList = [ '8', '8.0', '8.1', '8.2', '9', '9.0', '9.1', '9.2', '10', '10.0', '10.1', ]; for (int i = 0; i < testList.length; i++) { expect(compareIosVersions(testList[i], testList[i]), 0); } for (int i = 0; i < testList.length - 1; i++) { for (int j = i + 1; j < testList.length; j++) { expect(compareIosVersions(testList[i], testList[j]), lessThan(0)); expect(compareIosVersions(testList[j], testList[i]), greaterThan(0)); } } }); }); group('compareIphoneVersions', () { test('compares correctly', () { // This list must be sorted in ascending preference order final List testList = [ 'com.apple.CoreSimulator.SimDeviceType.iPhone-4s', 'com.apple.CoreSimulator.SimDeviceType.iPhone-5', 'com.apple.CoreSimulator.SimDeviceType.iPhone-5s', 'com.apple.CoreSimulator.SimDeviceType.iPhone-6strange', 'com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus', 'com.apple.CoreSimulator.SimDeviceType.iPhone-6', 'com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus', 'com.apple.CoreSimulator.SimDeviceType.iPhone-6s', ]; for (int i = 0; i < testList.length; i++) { expect(compareIphoneVersions(testList[i], testList[i]), 0); } for (int i = 0; i < testList.length - 1; i++) { for (int j = i + 1; j < testList.length; j++) { expect(compareIphoneVersions(testList[i], testList[j]), lessThan(0)); expect(compareIphoneVersions(testList[j], testList[i]), greaterThan(0)); } } }); }); group('sdkMajorVersion', () { // This new version string appears in SimulatorApp-850 CoreSimulator-518.16 beta. test('can be parsed from iOS-11-3', () async { final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3'); expect(await device.sdkMajorVersion, 11); }); test('can be parsed from iOS 11.2', () async { final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2'); expect(await device.sdkMajorVersion, 11); }); test('Has a simulator category', () async { final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2'); expect(device.category, Category.mobile); }); }); group('IOSSimulator.isSupported', () { testUsingContext('Apple TV is unsupported', () { expect(IOSSimulator('x', name: 'Apple TV').isSupported(), false); }, overrides: { Platform: () => osx, }); testUsingContext('Apple Watch is unsupported', () { expect(IOSSimulator('x', name: 'Apple Watch').isSupported(), false); }, overrides: { Platform: () => osx, }); testUsingContext('iPad 2 is supported', () { expect(IOSSimulator('x', name: 'iPad 2').isSupported(), true); }, overrides: { Platform: () => osx, }); testUsingContext('iPad Retina is supported', () { expect(IOSSimulator('x', name: 'iPad Retina').isSupported(), true); }, overrides: { Platform: () => osx, }); testUsingContext('iPhone 5 is supported', () { expect(IOSSimulator('x', name: 'iPhone 5').isSupported(), true); }, overrides: { Platform: () => osx, }); testUsingContext('iPhone 5s is supported', () { expect(IOSSimulator('x', name: 'iPhone 5s').isSupported(), true); }, overrides: { Platform: () => osx, }); testUsingContext('iPhone SE is supported', () { expect(IOSSimulator('x', name: 'iPhone SE').isSupported(), true); }, overrides: { Platform: () => osx, }); testUsingContext('iPhone 7 Plus is supported', () { expect(IOSSimulator('x', name: 'iPhone 7 Plus').isSupported(), true); }, overrides: { Platform: () => osx, }); testUsingContext('iPhone X is supported', () { expect(IOSSimulator('x', name: 'iPhone X').isSupported(), true); }, overrides: { Platform: () => osx, }); }); testUsingContext('builds with targetPlatform', () async { final IOSSimulator simulator = IOSSimulator('x', name: 'iPhone X'); when(buildSystem.build(any, any)).thenAnswer((Invocation invocation) async { return BuildResult(success: true); }); await simulator.sideloadUpdatedAssetsForInstalledApplicationBundle(BuildInfo.debug, 'lib/main.dart'); final VerificationResult result = verify(buildSystem.build(any, captureAny)); final Environment environment = result.captured.single as Environment; expect(environment.defines, { kTargetFile: 'lib/main.dart', kTargetPlatform: 'ios', kBuildMode: 'debug', kTrackWidgetCreation: 'false', kIconTreeShakerFlag: null, }); }, overrides: { BuildSystem: () => MockBuildSystem(), }); group('Simulator screenshot', () { MockXcode mockXcode; MockProcessManager mockProcessManager; IOSSimulator deviceUnderTest; setUp(() { mockXcode = MockXcode(); mockProcessManager = MockProcessManager(); // Let everything else return exit code 0 so process.dart doesn't crash. when( mockProcessManager.run(any, environment: null, workingDirectory: null) ).thenAnswer((Invocation invocation) => Future.value(ProcessResult(2, 0, '', '')) ); // Doesn't matter what the device is. deviceUnderTest = IOSSimulator('x', name: 'iPhone SE'); }); testUsingContext( "old Xcode doesn't support screenshot", () { when(mockXcode.majorVersion).thenReturn(7); when(mockXcode.minorVersion).thenReturn(1); expect(deviceUnderTest.supportsScreenshot, false); }, overrides: {Xcode: () => mockXcode}, ); testUsingContext( 'Xcode 8.2+ supports screenshots', () async { when(mockXcode.majorVersion).thenReturn(8); when(mockXcode.minorVersion).thenReturn(2); expect(deviceUnderTest.supportsScreenshot, true); final MockFile mockFile = MockFile(); when(mockFile.path).thenReturn(globals.fs.path.join('some', 'path', 'to', 'screenshot.png')); await deviceUnderTest.takeScreenshot(mockFile); verify(mockProcessManager.run( [ '/usr/bin/xcrun', 'simctl', 'io', 'x', 'screenshot', globals.fs.path.join('some', 'path', 'to', 'screenshot.png'), ], environment: null, workingDirectory: null, )); }, overrides: { ProcessManager: () => mockProcessManager, // Test a real one. Screenshot doesn't require instance states. SimControl: () => SimControl(), Xcode: () => mockXcode, }, ); }); group('launchDeviceLogTool', () { MockProcessManager mockProcessManager; setUp(() { mockProcessManager = MockProcessManager(); when(mockProcessManager.start(any, environment: null, workingDirectory: null)) .thenAnswer((Invocation invocation) => Future.value(MockProcess())); }); testUsingContext('uses tail on iOS versions prior to iOS 11', () async { final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 9.3'); await launchDeviceLogTool(device); expect( verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single, contains('tail'), ); }, overrides: { ProcessManager: () => mockProcessManager, }); testUsingContext('uses /usr/bin/log on iOS 11 and above', () async { final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.0'); await launchDeviceLogTool(device); expect( verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single, contains('/usr/bin/log'), ); }, overrides: { ProcessManager: () => mockProcessManager, }); }); group('launchSystemLogTool', () { MockProcessManager mockProcessManager; setUp(() { mockProcessManager = MockProcessManager(); when(mockProcessManager.start(any, environment: null, workingDirectory: null)) .thenAnswer((Invocation invocation) => Future.value(MockProcess())); }); testUsingContext('uses tail on iOS versions prior to iOS 11', () async { final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 9.3'); await launchSystemLogTool(device); expect( verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single, contains('tail'), ); }, overrides: { ProcessManager: () => mockProcessManager, }); testUsingContext('uses /usr/bin/log on iOS 11 and above', () async { final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.0'); await launchSystemLogTool(device); verifyNever(mockProcessManager.start(any, environment: null, workingDirectory: null)); }, overrides: { ProcessManager: () => mockProcessManager, }); }); group('log reader', () { MockProcessManager mockProcessManager; MockIosProject mockIosProject; setUp(() { mockProcessManager = MockProcessManager(); mockIosProject = MockIosProject(); }); testUsingContext('simulator can output `)`', () async { when(mockProcessManager.start(any, environment: null, workingDirectory: null)) .thenAnswer((Invocation invocation) { final Process mockProcess = MockProcess(); when(mockProcess.stdout) .thenAnswer((Invocation invocation) { return Stream>.fromIterable(>[''' 2017-09-13 15:26:57.228948-0700 localhost Runner[37195]: (Flutter) Observatory listening on http://127.0.0.1:57701/ 2017-09-13 15:26:57.228948-0700 localhost Runner[37195]: (Flutter) )))))))))) 2017-09-13 15:26:57.228948-0700 localhost Runner[37195]: (Flutter) #0 Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)''' .codeUnits]); }); when(mockProcess.stderr) .thenAnswer((Invocation invocation) => const Stream>.empty()); // Delay return of exitCode until after stdout stream data, since it terminates the logger. when(mockProcess.exitCode) .thenAnswer((Invocation invocation) => Future.delayed(Duration.zero, () => 0)); return Future.value(mockProcess); }); final IOSSimulator device = IOSSimulator('123456', simulatorCategory: 'iOS 11.0'); final DeviceLogReader logReader = device.getLogReader( app: await BuildableIOSApp.fromProject(mockIosProject), ); final List lines = await logReader.logLines.toList(); expect(lines, [ 'Observatory listening on http://127.0.0.1:57701/', '))))))))))', '#0 Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)', ]); }, overrides: { ProcessManager: () => mockProcessManager, }); }); group('SimControl', () { const int mockPid = 123; const String validSimControlOutput = ''' { "devices" : { "watchOS 4.3" : [ { "state" : "Shutdown", "availability" : "(available)", "name" : "Apple Watch - 38mm", "udid" : "TEST-WATCH-UDID" } ], "iOS 11.4" : [ { "state" : "Booted", "availability" : "(available)", "name" : "iPhone 5s", "udid" : "TEST-PHONE-UDID" } ], "tvOS 11.4" : [ { "state" : "Shutdown", "availability" : "(available)", "name" : "Apple TV", "udid" : "TEST-TV-UDID" } ] } } '''; MockProcessManager mockProcessManager; SimControl simControl; setUp(() { mockProcessManager = MockProcessManager(); when(mockProcessManager.run(any)).thenAnswer((Invocation _) async { return ProcessResult(mockPid, 0, validSimControlOutput, ''); }); simControl = SimControl(); }); testUsingContext('getDevices succeeds', () async { final List devices = await simControl.getDevices(); final SimDevice watch = devices[0]; expect(watch.category, 'watchOS 4.3'); expect(watch.state, 'Shutdown'); expect(watch.availability, '(available)'); expect(watch.name, 'Apple Watch - 38mm'); expect(watch.udid, 'TEST-WATCH-UDID'); expect(watch.isBooted, isFalse); final SimDevice phone = devices[1]; expect(phone.category, 'iOS 11.4'); expect(phone.state, 'Booted'); expect(phone.availability, '(available)'); expect(phone.name, 'iPhone 5s'); expect(phone.udid, 'TEST-PHONE-UDID'); expect(phone.isBooted, isTrue); final SimDevice tv = devices[2]; expect(tv.category, 'tvOS 11.4'); expect(tv.state, 'Shutdown'); expect(tv.availability, '(available)'); expect(tv.name, 'Apple TV'); expect(tv.udid, 'TEST-TV-UDID'); expect(tv.isBooted, isFalse); }, overrides: { ProcessManager: () => mockProcessManager, SimControl: () => simControl, }); testUsingContext('getDevices handles bad simctl output', () async { when(mockProcessManager.run(any)) .thenAnswer((Invocation _) async => ProcessResult(mockPid, 0, 'Install Started', '')); final List devices = await simControl.getDevices(); expect(devices, isEmpty); }, overrides: { ProcessManager: () => mockProcessManager, SimControl: () => simControl, }); testUsingContext('sdkMajorVersion defaults to 11 when sdkNameAndVersion is junk', () async { final IOSSimulator iosSimulatorA = IOSSimulator('x', name: 'Testo', simulatorCategory: 'NaN'); expect(await iosSimulatorA.sdkMajorVersion, 11); }); }); group('startApp', () { SimControl simControl; setUp(() { simControl = MockSimControl(); }); testUsingContext("startApp uses compiled app's Info.plist to find CFBundleIdentifier", () async { final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2'); when(globals.plistParser.getValueFromFile(any, any)).thenReturn('correct'); final Directory mockDir = globals.fs.currentDirectory; final IOSApp package = PrebuiltIOSApp(projectBundleId: 'incorrect', bundleName: 'name', bundleDir: mockDir); const BuildInfo mockInfo = BuildInfo(BuildMode.debug, 'flavor', treeShakeIcons: false); final DebuggingOptions mockOptions = DebuggingOptions.disabled(mockInfo); await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions); verify(simControl.launch(any, 'correct', any)); }, overrides: { SimControl: () => simControl, PlistParser: () => MockPlistUtils(), }); }); testUsingContext('IOSDevice.isSupportedForProject is true on module project', () async { globals.fs.file('pubspec.yaml') ..createSync() ..writeAsStringSync(r''' name: example flutter: module: {} '''); globals.fs.file('.packages').createSync(); final FlutterProject flutterProject = FlutterProject.current(); expect(IOSSimulator('test').isSupportedForProject(flutterProject), true); }, overrides: { FileSystem: () => MemoryFileSystem(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('IOSDevice.isSupportedForProject is true with editable host app', () async { globals.fs.file('pubspec.yaml').createSync(); globals.fs.file('.packages').createSync(); globals.fs.directory('ios').createSync(); final FlutterProject flutterProject = FlutterProject.current(); expect(IOSSimulator('test').isSupportedForProject(flutterProject), true); }, overrides: { FileSystem: () => MemoryFileSystem(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('IOSDevice.isSupportedForProject is false with no host app and no module', () async { globals.fs.file('pubspec.yaml').createSync(); globals.fs.file('.packages').createSync(); final FlutterProject flutterProject = FlutterProject.current(); expect(IOSSimulator('test').isSupportedForProject(flutterProject), false); }, overrides: { FileSystem: () => MemoryFileSystem(), ProcessManager: () => FakeProcessManager.any(), }); } class MockBuildSystem extends Mock implements BuildSystem {}