| // 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 'package:file/memory.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/platform.dart'; |
| import 'package:flutter_tools/src/base/process.dart'; |
| import 'package:flutter_tools/src/build_info.dart'; |
| import 'package:flutter_tools/src/devfs.dart'; |
| import 'package:flutter_tools/src/device.dart'; |
| import 'package:flutter_tools/src/device_port_forwarder.dart'; |
| import 'package:flutter_tools/src/globals.dart' as globals; |
| import 'package:flutter_tools/src/ios/application_package.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:test/fake.dart'; |
| |
| import '../../src/common.dart'; |
| import '../../src/context.dart'; |
| import '../../src/fake_process_manager.dart'; |
| import '../../src/fakes.dart'; |
| |
| final Platform macosPlatform = FakePlatform( |
| operatingSystem: 'macos', |
| environment: <String, String>{ |
| 'HOME': '/', |
| }, |
| ); |
| |
| void main() { |
| late FakePlatform osx; |
| late FileSystemUtils fsUtils; |
| late MemoryFileSystem fileSystem; |
| |
| setUp(() { |
| osx = FakePlatform( |
| environment: <String, String>{}, |
| operatingSystem: 'macos', |
| ); |
| fileSystem = MemoryFileSystem.test(); |
| fsUtils = FileSystemUtils(fileSystem: fileSystem, platform: osx); |
| }); |
| |
| group('_IOSSimulatorDevicePortForwarder', () { |
| late FakeSimControl simControl; |
| late Xcode xcode; |
| |
| setUp(() { |
| simControl = FakeSimControl(); |
| xcode = Xcode.test(processManager: FakeProcessManager.any()); |
| }); |
| |
| testUsingContext('dispose() does not throw an exception', () async { |
| final IOSSimulator simulator = IOSSimulator( |
| '123', |
| name: 'iPhone 11', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-14-4', |
| ); |
| 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: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Xcode: () => xcode, |
| }, testOn: 'posix'); |
| }); |
| |
| testUsingContext('simulators only support debug mode', () async { |
| final IOSSimulator simulator = IOSSimulator( |
| '123', |
| name: 'iPhone 11', |
| simControl: FakeSimControl(), |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-14-4', |
| ); |
| |
| expect(simulator.supportsRuntimeMode(BuildMode.debug), true); |
| expect(simulator.supportsRuntimeMode(BuildMode.profile), false); |
| expect(simulator.supportsRuntimeMode(BuildMode.release), false); |
| expect(simulator.supportsRuntimeMode(BuildMode.jitRelease), false); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| group('logFilePath', () { |
| late FakeSimControl simControl; |
| |
| setUp(() { |
| simControl = FakeSimControl(); |
| }); |
| |
| testUsingContext('defaults to rooted from HOME', () { |
| osx.environment['HOME'] = '/foo/bar'; |
| final IOSSimulator simulator = IOSSimulator( |
| '123', |
| name: 'iPhone 11', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-14-4', |
| ); |
| expect(simulator.logFilePath, '/foo/bar/Library/Logs/CoreSimulator/123/system.log'); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystemUtils: () => fsUtils, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }, 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'; |
| final IOSSimulator simulator = IOSSimulator( |
| '456', |
| name: 'iPhone 11', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-14-4', |
| ); |
| expect(simulator.logFilePath, '/baz/qux/456/system.log'); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystemUtils: () => fsUtils, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| }); |
| |
| group('compareIosVersions', () { |
| testWithoutContext('compares correctly', () { |
| // This list must be sorted in ascending preference order |
| final List<String> testList = <String>[ |
| '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('sdkMajorVersion', () { |
| late FakeSimControl simControl; |
| |
| setUp(() { |
| simControl = FakeSimControl(); |
| }); |
| |
| // This new version string appears in SimulatorApp-850 CoreSimulator-518.16 beta. |
| testWithoutContext('can be parsed from iOS-11-3', () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| simControl: simControl, |
| ); |
| |
| expect(await device.sdkMajorVersion, 11); |
| }); |
| |
| testWithoutContext('can be parsed from iOS 11.2', () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'iOS 11.2', |
| simControl: simControl, |
| ); |
| |
| expect(await device.sdkMajorVersion, 11); |
| }); |
| |
| testWithoutContext('Has a simulator category', () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'iOS 11.2', |
| simControl: simControl, |
| ); |
| |
| expect(device.category, Category.mobile); |
| }); |
| }); |
| |
| group('IOSSimulator.isSupported', () { |
| late FakeSimControl simControl; |
| |
| setUp(() { |
| simControl = FakeSimControl(); |
| }); |
| |
| testUsingContext('Apple TV is unsupported', () { |
| final IOSSimulator simulator = IOSSimulator( |
| 'x', |
| name: 'Apple TV', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.tvOS-14-5', |
| ); |
| expect(simulator.isSupported(), false); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| testUsingContext('Apple Watch is unsupported', () { |
| expect(IOSSimulator( |
| 'x', |
| name: 'Apple Watch', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.watchOS-8-0', |
| ).isSupported(), false); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| testUsingContext('iPad 2 is supported', () { |
| expect(IOSSimulator( |
| 'x', |
| name: 'iPad 2', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ).isSupported(), true); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| testUsingContext('iPad Retina is supported', () { |
| expect(IOSSimulator( |
| 'x', |
| name: 'iPad Retina', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ).isSupported(), true); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| testUsingContext('iPhone 5 is supported', () { |
| expect(IOSSimulator( |
| 'x', |
| name: 'iPhone 5', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ).isSupported(), true); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| testUsingContext('iPhone 5s is supported', () { |
| expect(IOSSimulator( |
| 'x', |
| name: 'iPhone 5s', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ).isSupported(), true); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| testUsingContext('iPhone SE is supported', () { |
| expect(IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ).isSupported(), true); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| testUsingContext('iPhone 7 Plus is supported', () { |
| expect(IOSSimulator( |
| 'x', |
| name: 'iPhone 7 Plus', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ).isSupported(), true); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| |
| testUsingContext('iPhone X is supported', () { |
| expect(IOSSimulator( |
| 'x', |
| name: 'iPhone X', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ).isSupported(), true); |
| }, overrides: <Type, Generator>{ |
| Platform: () => osx, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| }); |
| }); |
| |
| group('Simulator screenshot', () { |
| testWithoutContext('supports screenshots', () async { |
| final Xcode xcode = Xcode.test(processManager: FakeProcessManager.any()); |
| final Logger logger = BufferLogger.test(); |
| final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'io', |
| 'x', |
| 'screenshot', |
| 'screenshot.png', |
| ], |
| ), |
| ]); |
| |
| // Test a real one. Screenshot doesn't require instance states. |
| final SimControl simControl = SimControl( |
| processManager: fakeProcessManager, |
| logger: logger, |
| xcode: xcode, |
| ); |
| // Doesn't matter what the device is. |
| final IOSSimulator deviceUnderTest = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ); |
| |
| final File screenshot = MemoryFileSystem.test().file('screenshot.png'); |
| await deviceUnderTest.takeScreenshot(screenshot); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }); |
| }); |
| |
| group('device log tool', () { |
| late FakeProcessManager fakeProcessManager; |
| late FakeSimControl simControl; |
| |
| setUp(() { |
| fakeProcessManager = FakeProcessManager.empty(); |
| simControl = FakeSimControl(); |
| }); |
| |
| testUsingContext('syslog uses tail', () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'iOS 9.3', |
| simControl: simControl, |
| ); |
| fakeProcessManager.addCommand(const FakeCommand(command: <String>[ |
| 'tail', |
| '-n', |
| '0', |
| '-F', |
| '/Library/Logs/CoreSimulator/x/system.log', |
| ])); |
| await launchDeviceSystemLogTool(device); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }, |
| overrides: <Type, Generator>{ |
| ProcessManager: () => fakeProcessManager, |
| FileSystem: () => fileSystem, |
| Platform: () => macosPlatform, |
| FileSystemUtils: () => FileSystemUtils( |
| fileSystem: fileSystem, |
| platform: macosPlatform, |
| ), |
| }); |
| |
| testUsingContext('unified logging with app name', () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'iOS 11.0', |
| simControl: simControl, |
| ); |
| const String expectedPredicate = 'eventType = logEvent AND ' |
| 'processImagePath ENDSWITH "My Super Awesome App" AND ' |
| '(senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" OR processImageUUID == senderImageUUID) AND ' |
| 'NOT(eventMessage CONTAINS ": could not find icon for representation -> com.apple.") AND ' |
| 'NOT(eventMessage BEGINSWITH "assertion failed: ") AND ' |
| 'NOT(eventMessage CONTAINS " libxpc.dylib ")'; |
| fakeProcessManager.addCommand(const FakeCommand(command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'spawn', |
| 'x', |
| 'log', |
| 'stream', |
| '--style', |
| 'json', |
| '--predicate', |
| expectedPredicate, |
| ])); |
| |
| await launchDeviceUnifiedLogging(device, 'My Super Awesome App'); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }, |
| overrides: <Type, Generator>{ |
| ProcessManager: () => fakeProcessManager, |
| FileSystem: () => fileSystem, |
| }); |
| |
| testUsingContext('unified logging without app name', () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'iOS 11.0', |
| simControl: simControl, |
| ); |
| const String expectedPredicate = 'eventType = logEvent AND ' |
| '(senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" OR processImageUUID == senderImageUUID) AND ' |
| 'NOT(eventMessage CONTAINS ": could not find icon for representation -> com.apple.") AND ' |
| 'NOT(eventMessage BEGINSWITH "assertion failed: ") AND ' |
| 'NOT(eventMessage CONTAINS " libxpc.dylib ")'; |
| fakeProcessManager.addCommand(const FakeCommand(command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'spawn', |
| 'x', |
| 'log', |
| 'stream', |
| '--style', |
| 'json', |
| '--predicate', |
| expectedPredicate, |
| ])); |
| |
| await launchDeviceUnifiedLogging(device, null); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }, |
| overrides: <Type, Generator>{ |
| ProcessManager: () => fakeProcessManager, |
| FileSystem: () => fileSystem, |
| }); |
| }); |
| |
| group('log reader', () { |
| late FakeProcessManager fakeProcessManager; |
| late FakeIosProject mockIosProject; |
| late FakeSimControl simControl; |
| late Xcode xcode; |
| |
| setUp(() { |
| fakeProcessManager = FakeProcessManager.empty(); |
| mockIosProject = FakeIosProject(); |
| simControl = FakeSimControl(); |
| xcode = Xcode.test(processManager: FakeProcessManager.any()); |
| }); |
| |
| group('syslog', () { |
| setUp(() { |
| final File syslog = fileSystem.file('system.log')..createSync(); |
| osx.environment['IOS_SIMULATOR_LOG_FILE_PATH'] = syslog.path; |
| }); |
| |
| testUsingContext('simulator can parse Xcode 8/iOS 10-style logs', () async { |
| fakeProcessManager |
| ..addCommand(const FakeCommand( |
| command: <String>['tail', '-n', '0', '-F', 'system.log'], |
| stdout: ''' |
| Dec 20 17:04:32 md32-11-vm1 My Super Awesome App[88374]: flutter: The Dart VM service is listening on http://127.0.0.1:64213/1Uoeu523990=/ |
| Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''' |
| )) |
| ..addCommand(const FakeCommand( |
| command: <String>['tail', '-n', '0', '-F', '/private/var/log/system.log'] |
| )); |
| |
| final IOSSimulator device = IOSSimulator( |
| '123456', |
| name: 'iPhone 11', |
| simulatorCategory: 'iOS 10.0', |
| simControl: simControl, |
| ); |
| final DeviceLogReader logReader = device.getLogReader( |
| app: await BuildableIOSApp.fromProject(mockIosProject, null), |
| ); |
| |
| final List<String> lines = await logReader.logLines.toList(); |
| expect(lines, <String>[ |
| 'flutter: The Dart VM service is listening on http://127.0.0.1:64213/1Uoeu523990=/', |
| ]); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }, overrides: <Type, Generator>{ |
| ProcessManager: () => fakeProcessManager, |
| FileSystem: () => fileSystem, |
| Platform: () => osx, |
| Xcode: () => xcode, |
| }); |
| |
| testUsingContext('simulator can output `)`', () async { |
| fakeProcessManager |
| ..addCommand(const FakeCommand( |
| command: <String>['tail', '-n', '0', '-F', 'system.log'], |
| stdout: ''' |
| 2017-09-13 15:26:57.228948-0700 localhost My Super Awesome App[37195]: (Flutter) The Dart VM service is listening on http://127.0.0.1:57701/ |
| 2017-09-13 15:26:57.228948-0700 localhost My Super Awesome App[37195]: (Flutter) )))))))))) |
| 2017-09-13 15:26:57.228948-0700 localhost My Super Awesome App[37195]: (Flutter) #0 Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)''' |
| )) |
| ..addCommand(const FakeCommand( |
| command: <String>['tail', '-n', '0', '-F', '/private/var/log/system.log'] |
| )); |
| |
| final IOSSimulator device = IOSSimulator( |
| '123456', |
| name: 'iPhone 11', |
| simulatorCategory: 'iOS 10.3', |
| simControl: simControl, |
| ); |
| final DeviceLogReader logReader = device.getLogReader( |
| app: await BuildableIOSApp.fromProject(mockIosProject, null), |
| ); |
| |
| final List<String> lines = await logReader.logLines.toList(); |
| expect(lines, <String>[ |
| 'The Dart VM service is listening on http://127.0.0.1:57701/', |
| '))))))))))', |
| '#0 Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)', |
| ]); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }, overrides: <Type, Generator>{ |
| ProcessManager: () => fakeProcessManager, |
| FileSystem: () => fileSystem, |
| Platform: () => osx, |
| Xcode: () => xcode, |
| }); |
| |
| testUsingContext('multiline messages', () async { |
| fakeProcessManager |
| ..addCommand(const FakeCommand( |
| command: <String>['tail', '-n', '0', '-F', 'system.log'], |
| stdout: ''' |
| 2017-09-13 15:26:57.228948-0700 localhost My Super Awesome App[37195]: (Flutter) Single line message |
| 2017-09-13 15:26:57.228948-0700 localhost My Super Awesome App[37195]: (Flutter) Multi line message |
| continues... |
| continues... |
| 2020-03-11 15:58:28.207175-0700 localhost My Super Awesome App[72166]: (libnetwork.dylib) [com.apple.network:] [28 www.googleapis.com:443 stream, pid: 72166, tls] cancelled |
| [28.1 64A98447-EABF-4983-A387-7DB9D0C1785F 10.0.1.200.57912<->172.217.6.74:443] |
| Connected Path: satisfied (Path is satisfied), interface: en18 |
| Duration: 0.271s, DNS @0.000s took 0.001s, TCP @0.002s took 0.019s, TLS took 0.046s |
| bytes in/out: 4468/1933, packets in/out: 11/10, rtt: 0.016s, retransmitted packets: 0, out-of-order packets: 0 |
| 2017-09-13 15:36:57.228948-0700 localhost My Super Awesome App[37195]: (Flutter) Multi line message again |
| and it goes... |
| and goes... |
| 2017-09-13 15:36:57.228948-0700 localhost My Super Awesome App[37195]: (Flutter) Single line message, not the part of the above |
| ''' |
| )) |
| ..addCommand(const FakeCommand( |
| command: <String>['tail', '-n', '0', '-F', '/private/var/log/system.log'] |
| )); |
| |
| final IOSSimulator device = IOSSimulator( |
| '123456', |
| name: 'iPhone 11', |
| simulatorCategory: 'iOS 10.3', |
| simControl: simControl, |
| ); |
| final DeviceLogReader logReader = device.getLogReader( |
| app: await BuildableIOSApp.fromProject(mockIosProject, null), |
| ); |
| |
| final List<String> lines = await logReader.logLines.toList(); |
| expect(lines, <String>[ |
| 'Single line message', |
| 'Multi line message', |
| ' continues...', |
| ' continues...', |
| 'Multi line message again', |
| ' and it goes...', |
| ' and goes...', |
| 'Single line message, not the part of the above', |
| ]); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }, overrides: <Type, Generator>{ |
| ProcessManager: () => fakeProcessManager, |
| FileSystem: () => fileSystem, |
| Platform: () => osx, |
| Xcode: () => xcode, |
| }); |
| }); |
| |
| group('unified logging', () { |
| late BufferLogger logger; |
| |
| setUp(() { |
| logger = BufferLogger.test(); |
| }); |
| |
| testUsingContext('log reader handles escaped multiline messages', () async { |
| const String logPredicate = 'eventType = logEvent AND processImagePath ENDSWITH "My Super Awesome App" ' |
| 'AND (senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" ' |
| 'OR processImageUUID == senderImageUUID) AND NOT(eventMessage CONTAINS ": could not find icon ' |
| 'for representation -> com.apple.") AND NOT(eventMessage BEGINSWITH "assertion failed: ") ' |
| 'AND NOT(eventMessage CONTAINS " libxpc.dylib ")'; |
| fakeProcessManager.addCommand(const FakeCommand( |
| command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'spawn', |
| '123456', |
| 'log', |
| 'stream', |
| '--style', |
| 'json', |
| '--predicate', |
| logPredicate, |
| ], |
| stdout: r''' |
| },{ |
| "traceID" : 37579774151491588, |
| "eventMessage" : "Single line message", |
| "eventType" : "logEvent" |
| },{ |
| "traceID" : 37579774151491588, |
| "eventMessage" : "Multi line message\n continues...\n continues..." |
| },{ |
| "traceID" : 37579774151491588, |
| "eventMessage" : "Single line message, not the part of the above", |
| "eventType" : "logEvent" |
| },{ |
| ''' |
| )); |
| |
| final IOSSimulator device = IOSSimulator( |
| '123456', |
| name: 'iPhone 11', |
| simulatorCategory: 'iOS 11.0', |
| simControl: simControl, |
| ); |
| final DeviceLogReader logReader = device.getLogReader( |
| app: await BuildableIOSApp.fromProject(mockIosProject, null), |
| ); |
| |
| final List<String> lines = await logReader.logLines.toList(); |
| expect(lines, <String>[ |
| 'Single line message', 'Multi line message\n continues...\n continues...', |
| 'Single line message, not the part of the above', |
| ]); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }, overrides: <Type, Generator>{ |
| ProcessManager: () => fakeProcessManager, |
| FileSystem: () => fileSystem, |
| }); |
| |
| testUsingContext('log reader handles bad output', () async { |
| const String logPredicate = 'eventType = logEvent AND processImagePath ENDSWITH "My Super Awesome App" ' |
| 'AND (senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" ' |
| 'OR processImageUUID == senderImageUUID) AND NOT(eventMessage CONTAINS ": could not find icon ' |
| 'for representation -> com.apple.") AND NOT(eventMessage BEGINSWITH "assertion failed: ") ' |
| 'AND NOT(eventMessage CONTAINS " libxpc.dylib ")'; |
| fakeProcessManager.addCommand(const FakeCommand( |
| command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'spawn', |
| '123456', |
| 'log', |
| 'stream', |
| '--style', |
| 'json', |
| '--predicate', |
| logPredicate, |
| ], |
| stdout: '"eventMessage" : "message with incorrect escaping""', |
| )); |
| |
| final IOSSimulator device = IOSSimulator( |
| '123456', |
| name: 'iPhone 11', |
| simulatorCategory: 'iOS 11.0', |
| simControl: simControl, |
| ); |
| final DeviceLogReader logReader = device.getLogReader( |
| app: await BuildableIOSApp.fromProject(mockIosProject, null), |
| ); |
| |
| final List<String> lines = await logReader.logLines.toList(); |
| expect(lines, isEmpty); |
| expect(logger.errorText, contains('Logger returned non-JSON response')); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }, overrides: <Type, Generator>{ |
| ProcessManager: () => fakeProcessManager, |
| FileSystem: () => fileSystem, |
| Logger: () => logger, |
| }); |
| }); |
| }); |
| |
| group('SimControl', () { |
| const String validSimControlOutput = ''' |
| { |
| "devices" : { |
| "com.apple.CoreSimulator.SimRuntime.iOS-14-0" : [ |
| { |
| "dataPathSize" : 1734569984, |
| "udid" : "iPhone 11-UDID", |
| "isAvailable" : true, |
| "logPathSize" : 9506816, |
| "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-11", |
| "state" : "Booted", |
| "name" : "iPhone 11" |
| } |
| ], |
| "com.apple.CoreSimulator.SimRuntime.iOS-13-0" : [ |
| ], |
| "com.apple.CoreSimulator.SimRuntime.iOS-12-4" : [ |
| ], |
| "com.apple.CoreSimulator.SimRuntime.tvOS-16-0" : [ |
| ], |
| "com.apple.CoreSimulator.SimRuntime.watchOS-9-0" : [ |
| ], |
| "com.apple.CoreSimulator.SimRuntime.iOS-16-0" : [ |
| { |
| "dataPathSize" : 552366080, |
| "udid" : "Phone w Watch-UDID", |
| "isAvailable" : true, |
| "logPathSize" : 90112, |
| "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-11", |
| "state" : "Booted", |
| "name" : "Phone w Watch" |
| }, |
| { |
| "dataPathSize" : 2186457088, |
| "udid" : "iPhone 13-UDID", |
| "isAvailable" : true, |
| "logPathSize" : 151552, |
| "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-13", |
| "state" : "Booted", |
| "name" : "iPhone 13" |
| } |
| ] |
| } |
| } |
| '''; |
| |
| late FakeProcessManager fakeProcessManager; |
| Xcode xcode; |
| late SimControl simControl; |
| const String deviceId = 'smart-phone'; |
| const String appId = 'flutterApp'; |
| |
| setUp(() { |
| fakeProcessManager = FakeProcessManager.empty(); |
| xcode = Xcode.test(processManager: FakeProcessManager.any()); |
| simControl = SimControl( |
| logger: BufferLogger.test(), |
| processManager: fakeProcessManager, |
| xcode: xcode, |
| ); |
| }); |
| |
| testWithoutContext('getConnectedDevices succeeds', () async { |
| fakeProcessManager.addCommand(const FakeCommand( |
| command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'list', |
| 'devices', |
| 'booted', |
| 'iOS', |
| '--json', |
| ], |
| stdout: validSimControlOutput, |
| )); |
| |
| final List<BootedSimDevice> devices = await simControl.getConnectedDevices(); |
| |
| final BootedSimDevice phone1 = devices[0]; |
| expect(phone1.category, 'com.apple.CoreSimulator.SimRuntime.iOS-14-0'); |
| expect(phone1.name, 'iPhone 11'); |
| expect(phone1.udid, 'iPhone 11-UDID'); |
| |
| final BootedSimDevice phone2 = devices[1]; |
| expect(phone2.category, 'com.apple.CoreSimulator.SimRuntime.iOS-16-0'); |
| expect(phone2.name, 'Phone w Watch'); |
| expect(phone2.udid, 'Phone w Watch-UDID'); |
| |
| final BootedSimDevice phone3 = devices[2]; |
| expect(phone3.category, 'com.apple.CoreSimulator.SimRuntime.iOS-16-0'); |
| expect(phone3.name, 'iPhone 13'); |
| expect(phone3.udid, 'iPhone 13-UDID'); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('getConnectedDevices handles bad simctl output', () async { |
| fakeProcessManager.addCommand(const FakeCommand( |
| command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'list', |
| 'devices', |
| 'booted', |
| 'iOS', |
| '--json', |
| ], |
| stdout: 'Install Started', |
| )); |
| |
| final List<BootedSimDevice> devices = await simControl.getConnectedDevices(); |
| |
| expect(devices, isEmpty); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('sdkMajorVersion defaults to 11 when sdkNameAndVersion is junk', () async { |
| final IOSSimulator iosSimulatorA = IOSSimulator( |
| 'x', |
| name: 'Testo', |
| simulatorCategory: 'NaN', |
| simControl: simControl, |
| ); |
| |
| expect(await iosSimulatorA.sdkMajorVersion, 11); |
| }); |
| |
| testWithoutContext('.install() handles exceptions', () async { |
| fakeProcessManager.addCommand(const FakeCommand( |
| command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'install', |
| deviceId, |
| appId, |
| ], |
| exception: ProcessException('xcrun', <String>[]), |
| )); |
| |
| expect( |
| () async => simControl.install(deviceId, appId), |
| throwsToolExit(message: r'Unable to install'), |
| ); |
| }); |
| |
| testWithoutContext('.uninstall() handles exceptions', () async { |
| fakeProcessManager.addCommand(const FakeCommand( |
| command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'uninstall', |
| deviceId, |
| appId, |
| ], |
| exception: ProcessException('xcrun', <String>[]), |
| )); |
| |
| expect( |
| () async => simControl.uninstall(deviceId, appId), |
| throwsToolExit(message: r'Unable to uninstall'), |
| ); |
| }); |
| |
| testWithoutContext('.launch() handles exceptions', () async { |
| fakeProcessManager.addCommand(const FakeCommand( |
| command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'launch', |
| deviceId, |
| appId, |
| ], |
| exception: ProcessException('xcrun', <String>[]), |
| )); |
| |
| expect( |
| () async => simControl.launch(deviceId, appId), |
| throwsToolExit(message: r'Unable to launch'), |
| ); |
| }); |
| |
| testWithoutContext('.stopApp() handles exceptions', () async { |
| fakeProcessManager.addCommand(const FakeCommand( |
| command: <String>[ |
| 'xcrun', |
| 'simctl', |
| 'terminate', |
| deviceId, |
| appId, |
| ], |
| exception: ProcessException('xcrun', <String>[]), |
| )); |
| |
| expect( |
| () async => simControl.stopApp(deviceId, appId), |
| throwsToolExit(message: 'Unable to terminate'), |
| ); |
| expect(fakeProcessManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('simulator stopApp handles null app package', () async { |
| final IOSSimulator iosSimulator = IOSSimulator( |
| 'x', |
| name: 'Testo', |
| simulatorCategory: 'NaN', |
| simControl: simControl, |
| ); |
| |
| expect(await iosSimulator.stopApp(null), isFalse); |
| }); |
| }); |
| |
| group('startApp', () { |
| late FakePlistParser testPlistParser; |
| late FakeSimControl simControl; |
| late Xcode xcode; |
| late BufferLogger logger; |
| |
| setUp(() { |
| simControl = FakeSimControl(); |
| xcode = Xcode.test(processManager: FakeProcessManager.any()); |
| testPlistParser = FakePlistParser(); |
| logger = BufferLogger.test(); |
| }); |
| |
| testUsingContext("startApp uses compiled app's Info.plist to find CFBundleIdentifier", () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'iOS 11.2', |
| simControl: simControl, |
| ); |
| testPlistParser.setProperty('CFBundleIdentifier', 'correct'); |
| |
| final Directory mockDir = globals.fs.currentDirectory; |
| final IOSApp package = PrebuiltIOSApp( |
| projectBundleId: 'incorrect', |
| bundleName: 'name', |
| uncompressedBundle: mockDir, |
| applicationPackage: mockDir, |
| ); |
| |
| const BuildInfo mockInfo = BuildInfo(BuildMode.debug, 'flavor', treeShakeIcons: false); |
| final DebuggingOptions mockOptions = DebuggingOptions.disabled(mockInfo); |
| await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions); |
| |
| expect(simControl.requests.single.appIdentifier, 'correct'); |
| }, overrides: <Type, Generator>{ |
| PlistParser: () => testPlistParser, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Xcode: () => xcode, |
| }); |
| |
| testUsingContext('startApp fails when cannot find CFBundleIdentifier', () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'iOS 11.2', |
| simControl: simControl, |
| ); |
| |
| final Directory mockDir = globals.fs.currentDirectory; |
| final IOSApp package = PrebuiltIOSApp( |
| projectBundleId: 'incorrect', |
| bundleName: 'name', |
| uncompressedBundle: mockDir, |
| applicationPackage: mockDir, |
| ); |
| |
| const BuildInfo mockInfo = BuildInfo(BuildMode.debug, 'flavor', treeShakeIcons: false); |
| final DebuggingOptions mockOptions = DebuggingOptions.disabled(mockInfo); |
| final LaunchResult result = await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions); |
| |
| expect(result.started, isFalse); |
| expect(simControl.requests, isEmpty); |
| expect(logger.errorText, contains('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier')); |
| }, overrides: <Type, Generator>{ |
| PlistParser: () => testPlistParser, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| Xcode: () => xcode, |
| }); |
| |
| testUsingContext('startApp forwards all supported debugging options', () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'iOS 11.2', |
| simControl: simControl, |
| ); |
| testPlistParser.setProperty('CFBundleIdentifier', 'correct'); |
| |
| final Directory mockDir = globals.fs.currentDirectory; |
| final IOSApp package = PrebuiltIOSApp( |
| projectBundleId: 'correct', |
| bundleName: 'name', |
| uncompressedBundle: mockDir, |
| applicationPackage: mockDir, |
| ); |
| |
| const BuildInfo mockInfo = BuildInfo(BuildMode.debug, 'flavor', treeShakeIcons: false); |
| final DebuggingOptions mockOptions = DebuggingOptions.enabled( |
| mockInfo, |
| enableSoftwareRendering: true, |
| traceSystrace: true, |
| startPaused: true, |
| disableServiceAuthCodes: true, |
| skiaDeterministicRendering: true, |
| useTestFonts: true, |
| traceSkia: true, |
| traceAllowlist: 'foo,bar', |
| traceSkiaAllowlist: 'skia.a,skia.b', |
| endlessTraceBuffer: true, |
| dumpSkpOnShaderCompilation: true, |
| verboseSystemLogs: true, |
| cacheSkSL: true, |
| purgePersistentCache: true, |
| dartFlags: '--baz', |
| nullAssertions: true, |
| enableImpeller: true, |
| hostVmServicePort: 0, |
| ); |
| |
| await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions); |
| expect(simControl.requests.single.launchArgs, unorderedEquals(<String>[ |
| '--enable-dart-profiling', |
| '--enable-checked-mode', |
| '--verify-entry-points', |
| '--enable-software-rendering', |
| '--trace-systrace', |
| '--start-paused', |
| '--disable-service-auth-codes', |
| '--skia-deterministic-rendering', |
| '--use-test-fonts', |
| '--trace-skia', |
| '--trace-allowlist="foo,bar"', |
| '--trace-skia-allowlist="skia.a,skia.b"', |
| '--endless-trace-buffer', |
| '--dump-skp-on-shader-compilation', |
| '--verbose-logging', |
| '--cache-sksl', |
| '--purge-persistent-cache', |
| '--dart-flags=--baz,--null_assertions', |
| '--enable-impeller', |
| '--observatory-port=0', |
| ])); |
| }, overrides: <Type, Generator>{ |
| PlistParser: () => testPlistParser, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Xcode: () => xcode, |
| }); |
| |
| testUsingContext('startApp using route', () async { |
| final IOSSimulator device = IOSSimulator( |
| 'x', |
| name: 'iPhone SE', |
| simulatorCategory: 'iOS 11.2', |
| simControl: simControl, |
| ); |
| testPlistParser.setProperty('CFBundleIdentifier', 'correct'); |
| |
| final Directory mockDir = globals.fs.currentDirectory; |
| final IOSApp package = PrebuiltIOSApp( |
| projectBundleId: 'correct', |
| bundleName: 'name', |
| uncompressedBundle: mockDir, |
| applicationPackage: mockDir, |
| ); |
| |
| const BuildInfo mockInfo = BuildInfo(BuildMode.debug, 'flavor', treeShakeIcons: false); |
| final DebuggingOptions mockOptions = DebuggingOptions.enabled(mockInfo, enableSoftwareRendering: true); |
| await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions, route: '/animation'); |
| |
| expect(simControl.requests.single.launchArgs, contains('--route=/animation')); |
| }, overrides: <Type, Generator>{ |
| PlistParser: () => testPlistParser, |
| FileSystem: () => fileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Xcode: () => xcode, |
| }); |
| }); |
| |
| group('IOSDevice.isSupportedForProject', () { |
| late FakeSimControl simControl; |
| late Xcode xcode; |
| |
| setUp(() { |
| simControl = FakeSimControl(); |
| xcode = Xcode.test(processManager: FakeProcessManager.any()); |
| }); |
| |
| testUsingContext('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.fromDirectoryTest(globals.fs.currentDirectory); |
| |
| final IOSSimulator simulator = IOSSimulator( |
| 'test', |
| name: 'iPhone 11', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ); |
| expect(simulator.isSupportedForProject(flutterProject), true); |
| }, overrides: <Type, Generator>{ |
| FileSystem: () => MemoryFileSystem.test(), |
| ProcessManager: () => FakeProcessManager.any(), |
| Xcode: () => xcode, |
| }); |
| |
| |
| testUsingContext('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.fromDirectoryTest(globals.fs.currentDirectory); |
| |
| final IOSSimulator simulator = IOSSimulator( |
| 'test', |
| name: 'iPhone 11', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ); |
| expect(simulator.isSupportedForProject(flutterProject), true); |
| }, overrides: <Type, Generator>{ |
| FileSystem: () => MemoryFileSystem.test(), |
| ProcessManager: () => FakeProcessManager.any(), |
| Xcode: () => xcode, |
| }); |
| |
| testUsingContext('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.fromDirectoryTest(globals.fs.currentDirectory); |
| |
| final IOSSimulator simulator = IOSSimulator( |
| 'test', |
| name: 'iPhone 11', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ); |
| expect(simulator.isSupportedForProject(flutterProject), false); |
| }, overrides: <Type, Generator>{ |
| FileSystem: () => MemoryFileSystem.test(), |
| ProcessManager: () => FakeProcessManager.any(), |
| Xcode: () => xcode, |
| }); |
| |
| testUsingContext('createDevFSWriter returns a LocalDevFSWriter', () { |
| final IOSSimulator simulator = IOSSimulator( |
| 'test', |
| name: 'iPhone 11', |
| simControl: simControl, |
| simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3', |
| ); |
| |
| expect(simulator.createDevFSWriter(null, ''), isA<LocalDevFSWriter>()); |
| }); |
| }); |
| } |
| |
| class FakeIosProject extends Fake implements IosProject { |
| @override |
| Future<String> productBundleIdentifier(BuildInfo? buildInfo) async => 'com.example.test'; |
| |
| @override |
| Future<String> hostAppBundleName(BuildInfo? buildInfo) async => 'My Super Awesome App.app'; |
| } |
| |
| class FakeSimControl extends Fake implements SimControl { |
| final List<LaunchRequest> requests = <LaunchRequest>[]; |
| |
| @override |
| Future<RunResult> launch(String deviceId, String appIdentifier, [ List<String>? launchArgs ]) async { |
| requests.add(LaunchRequest(deviceId, appIdentifier, launchArgs)); |
| return RunResult(ProcessResult(0, 0, '', ''), <String>['test']); |
| } |
| |
| @override |
| Future<RunResult> install(String deviceId, String appPath) async { |
| return RunResult(ProcessResult(0, 0, '', ''), <String>['test']); |
| } |
| } |
| |
| class LaunchRequest { |
| const LaunchRequest(this.deviceId, this.appIdentifier, this.launchArgs); |
| |
| final String deviceId; |
| final String appIdentifier; |
| final List<String>? launchArgs; |
| } |