Launch named iOS simulators (#72323)

This commit is contained in:
Jenn Magder 2020-12-15 15:06:47 -08:00 committed by GitHub
parent d2d0c73f89
commit 84a7a611b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 244 additions and 111 deletions

View file

@ -142,7 +142,7 @@ class AndroidEmulator extends Emulator {
Category get category => Category.mobile;
@override
PlatformType get platformType => PlatformType.android;
String get platformDisplay => PlatformType.android.toString();
String _prop(String name) => _properties != null ? _properties[name] : null;

View file

@ -933,7 +933,7 @@ Map<String, dynamic> _emulatorToMap(Emulator emulator) {
'id': emulator.id,
'name': emulator.name,
'category': emulator.category?.toString(),
'platformType': emulator.platformType?.toString(),
'platformType': emulator.platformDisplay,
};
}

View file

@ -27,7 +27,7 @@ class EmulatorsCommand extends FlutterCommand {
final String description = 'List, launch and create emulators.';
@override
final List<String> aliases = <String>['emulator'];
final List<String> aliases = <String>['emulator', 'simulators', 'simulator'];
@override
Future<FlutterCommandResult> runCommand() async {

View file

@ -614,10 +614,6 @@ abstract class Device {
/// Check if the device is supported by Flutter.
bool isSupported();
// String meant to be displayed to the user indicating if the device is
// supported by Flutter, and, if not, why.
String supportMessage() => isSupported() ? 'Supported' : 'Unsupported';
/// The device's platform.
Future<TargetPlatform> get targetPlatform;

View file

@ -252,7 +252,7 @@ abstract class Emulator {
String get name;
String get manufacturer;
Category get category;
PlatformType get platformType;
String get platformDisplay;
@override
int get hashCode => id.hashCode;
@ -283,7 +283,7 @@ abstract class Emulator {
emulator.id ?? '',
emulator.name ?? '',
emulator.manufacturer ?? '',
emulator.platformType?.toString() ?? '',
emulator.platformDisplay ?? '',
],
];

View file

@ -16,17 +16,26 @@ class IOSEmulators extends EmulatorDiscovery {
bool get canListAnything => globals.iosWorkflow.canListEmulators;
@override
Future<List<Emulator>> get emulators async => getEmulators();
Future<List<Emulator>> get emulators async {
final List<IOSSimulator> simulators = await globals.iosSimulatorUtils.getAvailableDevices();
return simulators.map<Emulator>((IOSSimulator device) {
return IOSEmulator(device);
}).toList();
}
@override
bool get canLaunchAnything => canListAnything;
}
class IOSEmulator extends Emulator {
const IOSEmulator(String id) : super(id, true);
IOSEmulator(IOSSimulator simulator)
: _simulator = simulator,
super(simulator.id, true);
final IOSSimulator _simulator;
@override
String get name => 'iOS Simulator';
String get name => _simulator.name;
@override
String get manufacturer => 'Apple';
@ -35,43 +44,20 @@ class IOSEmulator extends Emulator {
Category get category => Category.mobile;
@override
PlatformType get platformType => PlatformType.ios;
String get platformDisplay =>
// com.apple.CoreSimulator.SimRuntime.iOS-10-3 => iOS-10-3
_simulator.simulatorCategory?.split('.')?.last ?? 'ios';
@override
Future<void> launch() async {
Future<bool> launchSimulator(List<String> additionalArgs) async {
final List<String> args = <String>[
'open',
...additionalArgs,
'-a',
globals.xcode.getSimulatorPath(),
];
final RunResult launchResult = await globals.processUtils.run(args);
if (launchResult.exitCode != 0) {
globals.printError('$launchResult');
return false;
}
return true;
final RunResult launchResult = await globals.processUtils.run(<String>[
'open',
'-a',
globals.xcode.getSimulatorPath(),
]);
if (launchResult.exitCode != 0) {
globals.printError('$launchResult');
}
// First run with `-n` to force a device to boot if there isn't already one
if (!await launchSimulator(<String>['-n'])) {
return;
}
// Run again to force it to Foreground (using -n doesn't force existing
// devices to the foreground)
await launchSimulator(<String>[]);
return _simulator.boot();
}
}
/// Return the list of iOS Simulators (there can only be zero or one).
List<IOSEmulator> getEmulators() {
final String simulatorPath = globals.xcode.getSimulatorPath();
if (simulatorPath == null) {
return <IOSEmulator>[];
}
return <IOSEmulator>[const IOSEmulator(iosSimulatorId)];
}

View file

@ -66,7 +66,9 @@ class IOSSimulatorUtils {
return <IOSSimulator>[];
}
final List<SimDevice> connected = await _simControl.getConnectedDevices();
final List<SimDevice> connected = (await _simControl.getAvailableDevices())
.where((SimDevice device) => device.isBooted)
.toList();
return connected.map<IOSSimulator>((SimDevice device) {
return IOSSimulator(
device.udid,
@ -77,6 +79,26 @@ class IOSSimulatorUtils {
);
}).toList();
}
Future<List<IOSSimulator>> getAvailableDevices() async {
if (!_xcode.isInstalledAndMeetsVersionCheck) {
return <IOSSimulator>[];
}
final List<SimDevice> available = await _simControl.getAvailableDevices();
return available
.map<IOSSimulator>((SimDevice device) {
return IOSSimulator(
device.udid,
name: device.name,
simControl: _simControl,
simulatorCategory: device.category,
xcode: _xcode,
);
})
.where((IOSSimulator simulator) => simulator.isSupported())
.toList();
}
}
/// A wrapper around the `simctl` command line tool.
@ -89,6 +111,23 @@ class SimControl {
_xcode = xcode,
_processUtils = ProcessUtils(processManager: processManager, logger: logger);
/// Create a [SimControl] for testing.
///
/// Defaults to a buffer logger.
@visibleForTesting
factory SimControl.test({
@required ProcessManager processManager,
Logger logger,
Xcode xcode,
}) {
logger ??= BufferLogger.test();
return SimControl(
logger: logger,
xcode: xcode,
processManager: processManager,
);
}
final Logger _logger;
final ProcessUtils _processUtils;
final Xcode _xcode;
@ -160,10 +199,10 @@ class SimControl {
return devices;
}
/// Returns all the connected simulator devices.
Future<List<SimDevice>> getConnectedDevices() async {
/// Returns all the available simulator devices.
Future<List<SimDevice>> getAvailableDevices() async {
final List<SimDevice> simDevices = await getDevices();
return simDevices.where((SimDevice device) => device.isBooted).toList();
return simDevices.where((SimDevice device) => device.isAvailable).toList();
}
Future<bool> isInstalled(String deviceId, String appId) {
@ -234,6 +273,17 @@ class SimControl {
return result;
}
Future<RunResult> boot(String deviceId) {
return _processUtils.run(
<String>[
..._xcode.xcrunCommand(),
'simctl',
'boot',
deviceId,
],
);
}
Future<void> takeScreenshot(String deviceId, String outputPath) async {
try {
await _processUtils.run(
@ -296,7 +346,11 @@ class SimDevice {
final Map<String, dynamic> data;
String get state => data['state']?.toString();
String get availability => data['availability']?.toString();
bool get isAvailable =>
data['isAvailable'] == true ||
data['availability']?.toString() == '(available)';
String get name => data['name']?.toString();
String get udid => data['udid']?.toString();
@ -394,7 +448,6 @@ class IOSSimulator extends Device {
@override
bool isSupported() {
if (!globals.platform.isMacOS) {
_supportMessage = 'iOS devices require a Mac host machine.';
return false;
}
@ -402,21 +455,25 @@ class IOSSimulator extends Device {
// We do not yet support WatchOS or tvOS devices.
final RegExp blocklist = RegExp(r'Apple (TV|Watch)', caseSensitive: false);
if (blocklist.hasMatch(name)) {
_supportMessage = 'Flutter does not support Apple TV or Apple Watch.';
return false;
}
return true;
}
String _supportMessage;
Future<bool> boot() async {
final RunResult result = await _simControl.boot(id);
@override
String supportMessage() {
if (isSupported()) {
return 'Supported';
if (result.exitCode == 0) {
return true;
}
// 149 exit code means the device is already booted. Ignore this error.
if (result.exitCode == 149) {
globals.printTrace('Simulator "$id" already booted.');
return true;
}
return _supportMessage ?? 'Unknown';
globals.logger.printError('$result');
return false;
}
@override

View file

@ -71,7 +71,7 @@ void main() {
expect(emulator.name, displayName);
expect(emulator.manufacturer, manufacturer);
expect(emulator.category, Category.mobile);
expect(emulator.platformType, PlatformType.android);
expect(emulator.platformDisplay, 'android');
});
testWithoutContext('prefers displayname for name', () {

View file

@ -2,15 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_workflow.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/emulator.dart';
import 'package:flutter_tools/src/ios/ios_emulators.dart';
import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
@ -45,14 +43,10 @@ const FakeCommand kListEmulatorsCommand = FakeCommand(
);
void main() {
MockProcessManager mockProcessManager;
MockAndroidSdk mockSdk;
MockXcode mockXcode;
setUp(() {
mockProcessManager = MockProcessManager();
mockSdk = MockAndroidSdk();
mockXcode = MockXcode();
when(mockSdk.avdManagerPath).thenReturn('avdmanager');
when(mockSdk.getAvdManagerPath()).thenReturn('avdmanager');
@ -299,24 +293,53 @@ void main() {
});
group('ios_emulators', () {
bool didAttemptToRunSimulator = false;
MockXcode mockXcode;
FakeProcessManager fakeProcessManager;
setUp(() {
when(mockXcode.xcodeSelectPath).thenReturn('/fake/Xcode.app/Contents/Developer');
when(mockXcode.getSimulatorPath()).thenAnswer((_) => '/fake/simulator.app');
when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
final List<String> args = invocation.positionalArguments[0] as List<String>;
if (args.length >= 3 && args[0] == 'open' && args[1] == '-a' && args[2] == '/fake/simulator.app') {
didAttemptToRunSimulator = true;
}
return ProcessResult(101, 0, '', '');
});
fakeProcessManager = FakeProcessManager.list(<FakeCommand>[]);
mockXcode = MockXcode();
when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
when(mockXcode.getSimulatorPath())
.thenAnswer((_) => '/fake/simulator.app');
});
testUsingContext('runs correct launch commands', () async {
const Emulator emulator = IOSEmulator('ios');
fakeProcessManager.addCommands(<FakeCommand>[
const FakeCommand(command: <String>[
'open',
'-a',
'/fake/simulator.app',
]),
const FakeCommand(command: <String>[
'xcrun',
'simctl',
'boot',
'1234',
]),
]);
final SimControl simControl = SimControl.test(
processManager: fakeProcessManager,
xcode: mockXcode,
);
final IOSSimulator simulator = IOSSimulator(
'1234',
name: 'iPhone 12',
simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-14-3',
simControl: simControl,
xcode: mockXcode,
);
final IOSEmulator emulator = IOSEmulator(simulator);
expect(emulator.id, '1234');
expect(emulator.name, 'iPhone 12');
expect(emulator.category, Category.mobile);
expect(emulator.platformDisplay, 'iOS-14-3');
await emulator.launch();
expect(didAttemptToRunSimulator, equals(true));
expect(fakeProcessManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
ProcessManager: () => fakeProcessManager,
Xcode: () => mockXcode,
});
});
@ -347,35 +370,11 @@ class FakeEmulator extends Emulator {
Category get category => Category.mobile;
@override
PlatformType get platformType => PlatformType.android;
String get platformDisplay => PlatformType.android.toString();
@override
Future<void> launch() {
throw UnimplementedError('Not implemented in Mock');
}
}
class MockProcessManager extends Mock implements ProcessManager {
@override
ProcessResult runSync(
List<dynamic> command, {
String workingDirectory,
Map<String, String> environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding stdoutEncoding = systemEncoding,
Encoding stderrEncoding = systemEncoding,
}) {
final String program = command[0] as String;
final List<String> args = command.sublist(1) as List<String>;
switch (program) {
case '/usr/bin/xcode-select':
throw ProcessException(program, args);
break;
}
throw StateError('Unexpected process call: $command');
}
}
class MockXcode extends Mock implements Xcode {}

View file

@ -11,6 +11,7 @@ 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/build_system/build_system.dart';
import 'package:flutter_tools/src/devfs.dart';
@ -785,12 +786,19 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text'''
"availability" : "(available)",
"name" : "iPhone 5s",
"udid" : "TEST-PHONE-UDID"
},
{
"state" : "Shutdown",
"isAvailable" : false,
"name" : "iPhone 11",
"udid" : "TEST-PHONE-UNAVAILABLE-UDID",
"availabilityError" : "runtime profile not found"
}
],
"tvOS 11.4" : [
{
"state" : "Shutdown",
"availability" : "(available)",
"isAvailable" : true,
"name" : "Apple TV",
"udid" : "TEST-TV-UDID"
}
@ -824,11 +832,12 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text'''
testWithoutContext('getDevices succeeds', () async {
final List<SimDevice> devices = await simControl.getDevices();
expect(devices.length, 4);
final SimDevice watch = devices[0];
expect(watch.category, 'watchOS 4.3');
expect(watch.state, 'Shutdown');
expect(watch.availability, '(available)');
expect(watch.isAvailable, true);
expect(watch.name, 'Apple Watch - 38mm');
expect(watch.udid, 'TEST-WATCH-UDID');
expect(watch.isBooted, isFalse);
@ -836,20 +845,33 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text'''
final SimDevice phone = devices[1];
expect(phone.category, 'iOS 11.4');
expect(phone.state, 'Booted');
expect(phone.availability, '(available)');
expect(phone.isAvailable, true);
expect(phone.name, 'iPhone 5s');
expect(phone.udid, 'TEST-PHONE-UDID');
expect(phone.isBooted, isTrue);
final SimDevice tv = devices[2];
final SimDevice unavailablePhone = devices[2];
expect(unavailablePhone.category, 'iOS 11.4');
expect(unavailablePhone.state, 'Shutdown');
expect(unavailablePhone.isAvailable, isFalse);
expect(unavailablePhone.name, 'iPhone 11');
expect(unavailablePhone.udid, 'TEST-PHONE-UNAVAILABLE-UDID');
expect(unavailablePhone.isBooted, isFalse);
final SimDevice tv = devices[3];
expect(tv.category, 'tvOS 11.4');
expect(tv.state, 'Shutdown');
expect(tv.availability, '(available)');
expect(tv.isAvailable, true);
expect(tv.name, 'Apple TV');
expect(tv.udid, 'TEST-TV-UDID');
expect(tv.isBooted, isFalse);
});
testWithoutContext('getAvailableDevices succeeds', () async {
final List<SimDevice> devices = await simControl.getAvailableDevices();
expect(devices.length, 3);
});
testWithoutContext('getDevices handles bad simctl output', () async {
when(mockProcessManager.run(any))
.thenAnswer((Invocation _) async => ProcessResult(mockPid, 0, 'Install Started', ''));
@ -905,6 +927,78 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text'''
throwsToolExit(message: r'Unable to launch'),
);
});
testWithoutContext('.boot() calls the right command', () async {
await simControl.boot(deviceId);
verify(mockProcessManager.run(
<String>['xcrun', 'simctl', 'boot', deviceId],
environment: anyNamed('environment'),
workingDirectory: anyNamed('workingDirectory'),
));
});
});
group('boot', () {
SimControl simControl;
MockXcode mockXcode;
setUp(() {
simControl = MockSimControl();
mockXcode = MockXcode();
when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
});
testUsingContext('success', () async {
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 11.2',
simControl: simControl,
xcode: mockXcode,
);
when(simControl.boot(any)).thenAnswer((_) async =>
RunResult(ProcessResult(0, 0, '', ''), <String>['simctl']));
expect(await device.boot(), isTrue);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('already booted', () async {
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 11.2',
simControl: simControl,
xcode: mockXcode,
);
// 149 means the device is already booted.
when(simControl.boot(any)).thenAnswer((_) async =>
RunResult(ProcessResult(0, 149, '', ''), <String>['simctl']));
expect(await device.boot(), isTrue);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('failed', () async {
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 11.2',
simControl: simControl,
xcode: mockXcode,
);
when(simControl.boot(any)).thenAnswer((_) async =>
RunResult(ProcessResult(0, 1, '', ''), <String>['simctl']));
expect(await device.boot(), isFalse);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
});
group('startApp', () {

View file

@ -114,6 +114,7 @@ void testUsingContext(
IOSSimulatorUtils: () {
final MockIOSSimulatorUtils mock = MockIOSSimulatorUtils();
when(mock.getAttachedDevices()).thenAnswer((Invocation _) async => <IOSSimulator>[]);
when(mock.getAvailableDevices()).thenAnswer((Invocation _) async => <IOSSimulator>[]);
return mock;
},
OutputPreferences: () => OutputPreferences.test(),
@ -286,7 +287,7 @@ class FakeDoctor extends Doctor {
class MockSimControl extends Mock implements SimControl {
MockSimControl() {
when(getConnectedDevices()).thenAnswer((Invocation _) async => <SimDevice>[]);
when(getAvailableDevices()).thenAnswer((Invocation _) async => <SimDevice>[]);
}
}