Add --create option to `flutter emulators` command (#18235)
* Add --create option to flutter emulators
* Tweaks to error message
* Simplify emulator search logic
* Make name optional
* Add a note about this option being used with --create
* Tweaks to help information
* Switch to processManager for easier testing
* Don't crash on missing files or missing properties in Android Emulator
* Move name suffixing into emulator manager
This allows it to be tested in the EmulatorManager tests and also used by daemon later if desired.
* Pass the context's android SDK through so it can be mocked by tests
* Misc fixes
* Add tests around emulator creation
Process calls are mocked to avoid needing a real SDK (and to be fast). Full integration tests may be useful, but may require ensuring all build environments etc. are set up correctly.
* Simplify avdManagerPath
Previous changes were to emulatorPath!
* Fix lint errors
* Fix incorrect file exgtension for Windows
* Fix an issue where no system images would crash
reduce throws on an empty collection.
* Fix "null" appearing in error messages
The name we attempted to use will now always be returned, even in the case of failure.
* Add additional info to missing-system-image failure message
On Windows after installing Andriod Studio I didn't have any of these and got this message. Installing with sdkmanager fixed the issue.
* Fix thrown errors
runResult had a toString() but we moved to ProcessResult when switching to ProcessManager to this ended up throwing "Instance of ProcessResult".
* Fix package import
* Fix more package imports
* Move mock implementation into Mock class
There seemed to be issues using Lists in args with Mockito that I couldn't figure out (docs say to use typed() but I couldn't make this compile with these lists still)..
* Rename method that's ambigious now we have create
* Handle where there's no avd path
* Add another toList() :(
* Remove comment that was rewritten
* Fix forbidden import
* Make optional arg more obviously optional
* Reformat doc
* Note that we create a pixel device in help text
* Make this a named arg
diff --git a/packages/flutter_tools/lib/src/android/android_emulator.dart b/packages/flutter_tools/lib/src/android/android_emulator.dart
index adb1ee7..582bf0f 100644
--- a/packages/flutter_tools/lib/src/android/android_emulator.dart
+++ b/packages/flutter_tools/lib/src/android/android_emulator.dart
@@ -9,7 +9,8 @@
import '../android/android_sdk.dart';
import '../android/android_workflow.dart';
import '../base/file_system.dart';
-import '../base/process.dart';
+import '../base/io.dart';
+import '../base/process_manager.dart';
import '../emulator.dart';
import 'android_sdk.dart';
@@ -31,21 +32,23 @@
Map<String, String> _properties;
@override
- String get name => _properties['hw.device.name'];
+ String get name => _prop('hw.device.name');
@override
- String get manufacturer => _properties['hw.device.manufacturer'];
+ String get manufacturer => _prop('hw.device.manufacturer');
@override
String get label => _properties['avd.ini.displayname'];
+ String _prop(String name) => _properties != null ? _properties[name] : null;
+
@override
Future<void> launch() async {
final Future<void> launchResult =
- runAsync(<String>[getEmulatorPath(), '-avd', id])
- .then((RunResult runResult) {
+ processManager.run(<String>[getEmulatorPath(), '-avd', id])
+ .then((ProcessResult runResult) {
if (runResult.exitCode != 0) {
- throw '$runResult';
+ throw '${runResult.stdout}\n${runResult.stderr}'.trimRight();
}
});
// emulator continues running on a successful launch so if we
@@ -65,10 +68,12 @@
return <AndroidEmulator>[];
}
- final String listAvdsOutput = runSync(<String>[emulatorPath, '-list-avds']);
+ final String listAvdsOutput = processManager.runSync(<String>[emulatorPath, '-list-avds']).stdout;
final List<AndroidEmulator> emulators = <AndroidEmulator>[];
- extractEmulatorAvdInfo(listAvdsOutput, emulators);
+ if (listAvdsOutput != null) {
+ extractEmulatorAvdInfo(listAvdsOutput, emulators);
+ }
return emulators;
}
@@ -76,21 +81,26 @@
/// of emulators by reading information from the relevant ini files.
void extractEmulatorAvdInfo(String text, List<AndroidEmulator> emulators) {
for (String id in text.trim().split('\n').where((String l) => l != '')) {
- emulators.add(_createEmulator(id));
+ emulators.add(_loadEmulatorInfo(id));
}
}
-AndroidEmulator _createEmulator(String id) {
+AndroidEmulator _loadEmulatorInfo(String id) {
id = id.trim();
- final File iniFile = fs.file(fs.path.join(getAvdPath(), '$id.ini'));
- final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync());
-
- if (ini['path'] != null) {
- final File configFile = fs.file(fs.path.join(ini['path'], 'config.ini'));
- if (configFile.existsSync()) {
- final Map<String, String> properties =
- parseIniLines(configFile.readAsLinesSync());
- return new AndroidEmulator(id, properties);
+ final String avdPath = getAvdPath();
+ if (avdPath != null) {
+ final File iniFile = fs.file(fs.path.join(avdPath, '$id.ini'));
+ if (iniFile.existsSync()) {
+ final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync());
+ if (ini['path'] != null) {
+ final File configFile =
+ fs.file(fs.path.join(ini['path'], 'config.ini'));
+ if (configFile.existsSync()) {
+ final Map<String, String> properties =
+ parseIniLines(configFile.readAsLinesSync());
+ return new AndroidEmulator(id, properties);
+ }
+ }
}
}
diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart
index 79a5d67..2dcc5bf 100644
--- a/packages/flutter_tools/lib/src/android/android_sdk.dart
+++ b/packages/flutter_tools/lib/src/android/android_sdk.dart
@@ -64,16 +64,8 @@
/// will work for those users who have Android Tools installed but
/// not the full SDK.
String getEmulatorPath([AndroidSdk existingSdk]) {
- if (existingSdk?.emulatorPath != null)
- return existingSdk.emulatorPath;
-
- final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
-
- if (sdk?.latestVersion == null) {
- return os.which('emulator')?.path;
- } else {
- return sdk.emulatorPath;
- }
+ return existingSdk?.emulatorPath ??
+ AndroidSdk.locateAndroidSdk()?.emulatorPath;
}
/// Locate the path for storing AVD emulator images. Returns null if none found.
@@ -104,6 +96,15 @@
);
}
+/// Locate 'avdmanager'. Prefer to use one from an Android SDK, if we can locate that.
+/// This should be used over accessing androidSdk.avdManagerPath directly because it
+/// will work for those users who have Android Tools installed but
+/// not the full SDK.
+String getAvdManagerPath([AndroidSdk existingSdk]) {
+ return existingSdk?.avdManagerPath ??
+ AndroidSdk.locateAndroidSdk()?.avdManagerPath;
+}
+
class AndroidNdkSearchError {
AndroidNdkSearchError(this.reason);
@@ -314,6 +315,8 @@
String get emulatorPath => getEmulatorPath();
+ String get avdManagerPath => getAvdManagerPath();
+
/// Validate the Android SDK. This returns an empty list if there are no
/// issues; otherwise, it returns a list of issues found.
List<String> validateSdkWellFormed() {
@@ -343,6 +346,14 @@
return null;
}
+ String getAvdManagerPath() {
+ final String binaryName = platform.isWindows ? 'avdmanager.bat' : 'avdmanager';
+ final String path = fs.path.join(directory, 'tools', 'bin', binaryName);
+ if (fs.file(path).existsSync())
+ return path;
+ return null;
+ }
+
void _init() {
Iterable<Directory> platforms = <Directory>[]; // android-22, ...
diff --git a/packages/flutter_tools/lib/src/commands/emulators.dart b/packages/flutter_tools/lib/src/commands/emulators.dart
index 78b1284..0904bc4 100644
--- a/packages/flutter_tools/lib/src/commands/emulators.dart
+++ b/packages/flutter_tools/lib/src/commands/emulators.dart
@@ -16,13 +16,18 @@
EmulatorsCommand() {
argParser.addOption('launch',
help: 'The full or partial ID of the emulator to launch.');
+ argParser.addFlag('create',
+ help: 'Creates a new Android emulator based on a Pixel device.',
+ negatable: false);
+ argParser.addOption('name',
+ help: 'Used with flag --create. Specifies a name for the emulator being created.');
}
@override
final String name = 'emulators';
@override
- final String description = 'List and launch available emulators.';
+ final String description = 'List, launch and create emulators.';
@override
final List<String> aliases = <String>['emulator'];
@@ -40,6 +45,8 @@
if (argResults.wasParsed('launch')) {
await _launchEmulator(argResults['launch']);
+ } else if (argResults.wasParsed('create')) {
+ await _createEmulator(name: argResults['name']);
} else {
final String searchText =
argResults.rest != null && argResults.rest.isNotEmpty
@@ -70,17 +77,27 @@
}
}
+ Future<Null> _createEmulator({String name}) async {
+ final CreateEmulatorResult createResult =
+ await emulatorManager.createEmulator(name: name);
+
+ if (createResult.success) {
+ printStatus("Emulator '${createResult.emulatorName}' created successfully.");
+ } else {
+ printStatus("Failed to create emulator '${createResult.emulatorName}'.\n");
+ printStatus(createResult.error.trim());
+ _printAdditionalInfo();
+ }
+ }
+
Future<void> _listEmulators(String searchText) async {
final List<Emulator> emulators = searchText == null
? await emulatorManager.getAllAvailableEmulators()
: await emulatorManager.getEmulatorsMatching(searchText);
if (emulators.isEmpty) {
- printStatus('No emulators available.\n\n'
- // TODO(dantup): Change these when we support creation
- // 'You may need to create images using "flutter emulators --create"\n'
- 'You may need to create one using Android Studio '
- 'or visit https://flutter.io/setup/ for troubleshooting tips.');
+ printStatus('No emulators available.');
+ _printAdditionalInfo(showCreateInstruction: true);
} else {
_printEmulatorList(
emulators,
@@ -92,7 +109,28 @@
void _printEmulatorList(List<Emulator> emulators, String message) {
printStatus('$message\n');
Emulator.printEmulators(emulators);
- printStatus(
- "\nTo run an emulator, run 'flutter emulators --launch <emulator id>'.");
+ _printAdditionalInfo(showCreateInstruction: true, showRunInstruction: true);
+ }
+
+ void _printAdditionalInfo({ bool showRunInstruction = false,
+ bool showCreateInstruction = false }) {
+ printStatus('');
+ if (showRunInstruction) {
+ printStatus(
+ "To run an emulator, run 'flutter emulators --launch <emulator id>'.");
+ }
+ if (showCreateInstruction) {
+ printStatus(
+ "To create a new emulator, run 'flutter emulators --create [--name xyz]'.");
+ }
+
+ if (showRunInstruction || showCreateInstruction) {
+ printStatus('');
+ }
+ // TODO(dantup): Update this link to flutter.io if/when we have a better page.
+ // That page can then link out to these places if required.
+ printStatus('You can find more information on managing emulators at the links below:\n'
+ ' https://developer.android.com/studio/run/managing-avds\n'
+ ' https://developer.android.com/studio/command-line/avdmanager');
}
}
diff --git a/packages/flutter_tools/lib/src/emulator.dart b/packages/flutter_tools/lib/src/emulator.dart
index 81bbe2a..5fa6b2a 100644
--- a/packages/flutter_tools/lib/src/emulator.dart
+++ b/packages/flutter_tools/lib/src/emulator.dart
@@ -6,7 +6,10 @@
import 'dart:math' as math;
import 'android/android_emulator.dart';
+import 'android/android_sdk.dart';
import 'base/context.dart';
+import 'base/io.dart' show ProcessResult;
+import 'base/process_manager.dart';
import 'globals.dart';
import 'ios/ios_emulators.dart';
@@ -34,8 +37,8 @@
emulator.id?.toLowerCase()?.startsWith(searchText) == true ||
emulator.name?.toLowerCase()?.startsWith(searchText) == true;
- final Emulator exactMatch = emulators.firstWhere(
- exactlyMatchesEmulatorId, orElse: () => null);
+ final Emulator exactMatch =
+ emulators.firstWhere(exactlyMatchesEmulatorId, orElse: () => null);
if (exactMatch != null) {
return <Emulator>[exactMatch];
}
@@ -57,6 +60,133 @@
return emulators;
}
+ /// Return the list of all available emulators.
+ Future<CreateEmulatorResult> createEmulator({String name}) async {
+ if (name == null || name == '') {
+ const String autoName = 'flutter_emulator';
+ // Don't use getEmulatorsMatching here, as it will only return one
+ // if there's an exact match and we need all those with this prefix
+ // so we can keep adding suffixes until we miss.
+ final List<Emulator> all = await getAllAvailableEmulators();
+ final Set<String> takenNames = all
+ .map((Emulator e) => e.id)
+ .where((String id) => id.startsWith(autoName))
+ .toSet();
+ int suffix = 1;
+ name = autoName;
+ while (takenNames.contains(name)) {
+ name = '${autoName}_${++suffix}';
+ }
+ }
+
+ final String device = await _getPreferredAvailableDevice();
+ if (device == null)
+ return new CreateEmulatorResult(name,
+ success: false, error: 'No device definitions are available');
+
+ final String sdkId = await _getPreferredSdkId();
+ if (sdkId == null)
+ return new CreateEmulatorResult(name,
+ success: false,
+ error:
+ 'No suitable Android AVD system images are available. You may need to install these'
+ ' using sdkmanager, for example:\n'
+ ' sdkmanager "system-images;android-27;google_apis_playstore;x86"');
+
+ // Cleans up error output from avdmanager to make it more suitable to show
+ // to flutter users. Specifically:
+ // - Removes lines that say "null" (!)
+ // - Removes lines that tell the user to use '--force' to overwrite emulators
+ String cleanError(String error) {
+ return (error ?? '')
+ .split('\n')
+ .where((String l) => l.trim() != 'null')
+ .where((String l) =>
+ l.trim() != 'Use --force if you want to replace it.')
+ .join('\n');
+ }
+
+ final List<String> args = <String>[
+ getAvdManagerPath(androidSdk),
+ 'create',
+ 'avd',
+ '-n', name,
+ '-k', sdkId,
+ '-d', device
+ ];
+ final ProcessResult runResult = processManager.runSync(args);
+ return new CreateEmulatorResult(
+ name,
+ success: runResult.exitCode == 0,
+ output: runResult.stdout,
+ error: cleanError(runResult.stderr),
+ );
+ }
+
+ static const List<String> preferredDevices = const <String>[
+ 'pixel',
+ 'pixel_xl',
+ ];
+ Future<String> _getPreferredAvailableDevice() async {
+ final List<String> args = <String>[
+ getAvdManagerPath(androidSdk),
+ 'list',
+ 'device',
+ '-c'
+ ];
+ final ProcessResult runResult = processManager.runSync(args);
+ if (runResult.exitCode != 0)
+ return null;
+
+ final List<String> availableDevices = runResult.stdout
+ .split('\n')
+ .where((String l) => preferredDevices.contains(l.trim()))
+ .toList();
+
+ return preferredDevices.firstWhere(
+ (String d) => availableDevices.contains(d),
+ orElse: () => null,
+ );
+ }
+
+ RegExp androidApiVersion = new RegExp(r';android-(\d+);');
+ Future<String> _getPreferredSdkId() async {
+ // It seems that to get the available list of images, we need to send a
+ // request to create without the image and it'll provide us a list :-(
+ final List<String> args = <String>[
+ getAvdManagerPath(androidSdk),
+ 'create',
+ 'avd',
+ '-n', 'temp',
+ ];
+ final ProcessResult runResult = processManager.runSync(args);
+
+ // Get the list of IDs that match our criteria
+ final List<String> availableIDs = runResult.stderr
+ .split('\n')
+ .where((String l) => androidApiVersion.hasMatch(l))
+ .where((String l) => l.contains('system-images'))
+ .where((String l) => l.contains('google_apis_playstore'))
+ .toList();
+
+ final List<int> availableApiVersions = availableIDs
+ .map((String id) => androidApiVersion.firstMatch(id).group(1))
+ .map((String apiVersion) => int.parse(apiVersion))
+ .toList();
+
+ // Get the highest Android API version or whats left
+ final int apiVersion = availableApiVersions.isNotEmpty
+ ? availableApiVersions.reduce(math.max)
+ : -1; // Don't match below
+
+ // We're out of preferences, we just have to return the first one with the high
+ // API version.
+ return availableIDs.firstWhere(
+ (String id) => id.contains(';android-$apiVersion;'),
+ orElse: () => null,
+ );
+ }
+
/// Whether we're capable of listing any emulators given the current environment configuration.
bool get canListAnything {
return _platformDiscoverers.any((EmulatorDiscovery discoverer) => discoverer.canListAnything);
@@ -124,11 +254,13 @@
// Join columns into lines of text
final RegExp whiteSpaceAndDots = new RegExp(r'[•\s]+$');
- return table.map((List<String> row) {
- return indices
- .map((int i) => row[i].padRight(widths[i]))
- .join(' • ') + ' • ${row.last}';
- })
+ return table
+ .map((List<String> row) {
+ return indices
+ .map((int i) => row[i].padRight(widths[i]))
+ .join(' • ') +
+ ' • ${row.last}';
+ })
.map((String line) => line.replaceAll(whiteSpaceAndDots, ''))
.toList();
}
@@ -137,3 +269,12 @@
descriptions(emulators).forEach(printStatus);
}
}
+
+class CreateEmulatorResult {
+ final bool success;
+ final String emulatorName;
+ final String output;
+ final String error;
+
+ CreateEmulatorResult(this.emulatorName, {this.success, this.output, this.error});
+}
diff --git a/packages/flutter_tools/lib/src/ios/ios_emulators.dart b/packages/flutter_tools/lib/src/ios/ios_emulators.dart
index cd85c2f..3608bad 100644
--- a/packages/flutter_tools/lib/src/ios/ios_emulators.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_emulators.dart
@@ -71,6 +71,8 @@
}
String getSimulatorPath() {
+ if (xcode.xcodeSelectPath == null)
+ return null;
final List<String> searchPaths = <String>[
fs.path.join(xcode.xcodeSelectPath, 'Applications', 'Simulator.app'),
];
diff --git a/packages/flutter_tools/test/emulator_test.dart b/packages/flutter_tools/test/emulator_test.dart
index be36d5f..9aad551 100644
--- a/packages/flutter_tools/test/emulator_test.dart
+++ b/packages/flutter_tools/test/emulator_test.dart
@@ -3,31 +3,61 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:convert';
+import 'package:collection/collection.dart' show ListEquality;
+import 'package:flutter_tools/src/android/android_sdk.dart';
+import 'package:flutter_tools/src/base/config.dart';
+import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/emulator.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
import 'package:test/test.dart';
import 'src/context.dart';
+import 'src/mocks.dart';
void main() {
+ MockProcessManager mockProcessManager;
+ MockConfig mockConfig;
+ MockAndroidSdk mockSdk;
+
+ setUp(() {
+ mockProcessManager = new MockProcessManager();
+ mockConfig = new MockConfig();
+ mockSdk = new MockAndroidSdk();
+
+ when(mockSdk.avdManagerPath).thenReturn('avdmanager');
+ when(mockSdk.emulatorPath).thenReturn('emulator');
+ });
+
group('EmulatorManager', () {
testUsingContext('getEmulators', () async {
// Test that EmulatorManager.getEmulators() doesn't throw.
- final EmulatorManager emulatorManager = new EmulatorManager();
- final List<Emulator> emulators = await emulatorManager.getAllAvailableEmulators();
+ final List<Emulator> emulators =
+ await emulatorManager.getAllAvailableEmulators();
expect(emulators, isList);
});
testUsingContext('getEmulatorsById', () async {
- final _MockEmulator emulator1 = new _MockEmulator('Nexus_5', 'Nexus 5', 'Google', '');
- final _MockEmulator emulator2 = new _MockEmulator('Nexus_5X_API_27_x86', 'Nexus 5X', 'Google', '');
- final _MockEmulator emulator3 = new _MockEmulator('iOS Simulator', 'iOS Simulator', 'Apple', '');
- final List<Emulator> emulators = <Emulator>[emulator1, emulator2, emulator3];
- final EmulatorManager emulatorManager = new TestEmulatorManager(emulators);
+ final _MockEmulator emulator1 =
+ new _MockEmulator('Nexus_5', 'Nexus 5', 'Google', '');
+ final _MockEmulator emulator2 =
+ new _MockEmulator('Nexus_5X_API_27_x86', 'Nexus 5X', 'Google', '');
+ final _MockEmulator emulator3 =
+ new _MockEmulator('iOS Simulator', 'iOS Simulator', 'Apple', '');
+ final List<Emulator> emulators = <Emulator>[
+ emulator1,
+ emulator2,
+ emulator3
+ ];
+ final TestEmulatorManager testEmulatorManager =
+ new TestEmulatorManager(emulators);
Future<Null> expectEmulator(String id, List<Emulator> expected) async {
- expect(await emulatorManager.getEmulatorsMatching(id), expected);
+ expect(await testEmulatorManager.getEmulatorsMatching(id), expected);
}
+
expectEmulator('Nexus_5', <Emulator>[emulator1]);
expectEmulator('Nexus_5X', <Emulator>[emulator2]);
expectEmulator('Nexus_5X_API_27_x86', <Emulator>[emulator2]);
@@ -35,6 +65,61 @@
expectEmulator('iOS Simulator', <Emulator>[emulator3]);
expectEmulator('ios', <Emulator>[emulator3]);
});
+
+ testUsingContext('create emulator with an empty name does not fail',
+ () async {
+ final CreateEmulatorResult res = await emulatorManager.createEmulator();
+ expect(res.success, equals(true));
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ Config: () => mockConfig,
+ AndroidSdk: () => mockSdk,
+ });
+
+ testUsingContext('create emulator with a unique name does not throw',
+ () async {
+ final CreateEmulatorResult res =
+ await emulatorManager.createEmulator(name: 'test');
+ expect(res.success, equals(true));
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ Config: () => mockConfig,
+ AndroidSdk: () => mockSdk,
+ });
+
+ testUsingContext('create emulator with an existing name errors', () async {
+ final CreateEmulatorResult res =
+ await emulatorManager.createEmulator(name: 'existing-avd-1');
+ expect(res.success, equals(false));
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ Config: () => mockConfig,
+ AndroidSdk: () => mockSdk,
+ });
+
+ testUsingContext(
+ 'create emulator without a name but when default exists adds a suffix',
+ () async {
+ // First will get default name.
+ CreateEmulatorResult res = await emulatorManager.createEmulator();
+ expect(res.success, equals(true));
+
+ final String defaultName = res.emulatorName;
+
+ // Second...
+ res = await emulatorManager.createEmulator();
+ expect(res.success, equals(true));
+ expect(res.emulatorName, equals('${defaultName}_2'));
+
+ // Third...
+ res = await emulatorManager.createEmulator();
+ expect(res.success, equals(true));
+ expect(res.emulatorName, equals('${defaultName}_3'));
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ Config: () => mockConfig,
+ AndroidSdk: () => mockSdk,
+ });
});
}
@@ -50,14 +135,15 @@
}
class _MockEmulator extends Emulator {
- _MockEmulator(String id, this.name, this.manufacturer, this.label) : super(id, true);
+ _MockEmulator(String id, this.name, this.manufacturer, this.label)
+ : super(id, true);
@override
final String name;
-
+
@override
final String manufacturer;
-
+
@override
final String label;
@@ -66,3 +152,81 @@
throw new UnimplementedError('Not implemented in Mock');
}
}
+
+class MockConfig extends Mock implements Config {}
+
+class MockProcessManager extends Mock implements ProcessManager {
+ /// We have to send a command that fails in order to get the list of valid
+ /// system images paths. This is an example of the output to use in the mock.
+ static const String mockCreateFailureOutput =
+ 'Error: Package path (-k) not specified. Valid system image paths are:\n'
+ 'system-images;android-27;google_apis;x86\n'
+ 'system-images;android-P;google_apis;x86\n'
+ 'system-images;android-27;google_apis_playstore;x86\n'
+ 'null\n'; // Yep, these really end with null (on dantup's machine at least)
+
+ static const ListEquality<String> _equality = const ListEquality<String>();
+ final List<String> _existingAvds = <String>['existing-avd-1'];
+
+ @override
+ ProcessResult runSync(
+ List<dynamic> command, {
+ String workingDirectory,
+ Map<String, String> environment,
+ bool includeParentEnvironment = true,
+ bool runInShell = false,
+ Encoding stdoutEncoding,
+ Encoding stderrEncoding
+ }) {
+ final String program = command[0];
+ final List<String> args = command.sublist(1);
+ switch (command[0]) {
+ case '/usr/bin/xcode-select':
+ throw new ProcessException(program, args);
+ break;
+ case 'emulator':
+ return _handleEmulator(args);
+ case 'avdmanager':
+ return _handleAvdManager(args);
+ }
+ throw new StateError('Unexpected process call: $command');
+ }
+
+ ProcessResult _handleEmulator(List<String> args) {
+ if (_equality.equals(args, <String>['-list-avds'])) {
+ return new ProcessResult(101, 0, '${_existingAvds.join('\n')}\n', '');
+ }
+ throw new ProcessException('emulator', args);
+ }
+
+ ProcessResult _handleAvdManager(List<String> args) {
+ if (_equality.equals(args, <String>['list', 'device', '-c'])) {
+ return new ProcessResult(101, 0, 'test\ntest2\npixel\npixel-xl\n', '');
+ }
+ if (_equality.equals(args, <String>['create', 'avd', '-n', 'temp'])) {
+ return new ProcessResult(101, 1, '', mockCreateFailureOutput);
+ }
+ if (args.length == 8 &&
+ _equality.equals(args,
+ <String>['create', 'avd', '-n', args[3], '-k', args[5], '-d', args[7]])) {
+ // In order to support testing auto generation of names we need to support
+ // tracking any created emulators and reject when they already exist so this
+ // mock will compare the name of the AVD being created with the fake existing
+ // list and either reject if it exists, or add it to the list and return success.
+ final String name = args[3];
+ // Error if this AVD already existed
+ if (_existingAvds.contains(name)) {
+ return new ProcessResult(
+ 101,
+ 1,
+ '',
+ "Error: Android Virtual Device '$name' already exists.\n"
+ 'Use --force if you want to replace it.');
+ } else {
+ _existingAvds.add(name);
+ return new ProcessResult(101, 0, '', '');
+ }
+ }
+ throw new ProcessException('emulator', args);
+ }
+}