blob: cefad2011d65ff33e9ccaf7319c0d4c60004bdb3 [file] [log] [blame]
// 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',
enableImpeller: ImpellerStatus.disabled,
nullAssertions: 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',
'--enable-impeller=false',
'--dart-flags=--baz,--null_assertions',
'--vm-service-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;
}