Commit 6757515d authored by Yegor's avatar Yegor

Merge pull request #2204 from yjbanov/driver-ios-emulator

decouple `flutter drive` from `flutter start`; make things in `flutter_tools` more testable
parents 76b9d8d1 677e63b7
...@@ -54,6 +54,8 @@ class AndroidDevice extends Device { ...@@ -54,6 +54,8 @@ class AndroidDevice extends Device {
bool _connected; bool _connected;
bool get isLocalEmulator => false;
List<String> adbCommandForDevice(List<String> args) { List<String> adbCommandForDevice(List<String> args) {
return <String>[androidSdk.adbPath, '-s', id]..addAll(args); return <String>[androidSdk.adbPath, '-s', id]..addAll(args);
} }
......
...@@ -5,7 +5,10 @@ ...@@ -5,7 +5,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
final OperatingSystemUtils os = new OperatingSystemUtils._(); import 'context.dart';
/// Returns [OperatingSystemUtils] active in the current app context (i.e. zone).
OperatingSystemUtils get os => context[OperatingSystemUtils] ?? (context[OperatingSystemUtils] = new OperatingSystemUtils._());
abstract class OperatingSystemUtils { abstract class OperatingSystemUtils {
factory OperatingSystemUtils._() { factory OperatingSystemUtils._() {
...@@ -16,6 +19,14 @@ abstract class OperatingSystemUtils { ...@@ -16,6 +19,14 @@ abstract class OperatingSystemUtils {
} }
} }
OperatingSystemUtils._private();
String get operatingSystem => Platform.operatingSystem;
bool get isMacOS => operatingSystem == 'macos';
bool get isWindows => operatingSystem == 'windows';
bool get isLinux => operatingSystem == 'linux';
/// Make the given file executable. This may be a no-op on some platforms. /// Make the given file executable. This may be a no-op on some platforms.
ProcessResult makeExecutable(File file); ProcessResult makeExecutable(File file);
...@@ -24,7 +35,9 @@ abstract class OperatingSystemUtils { ...@@ -24,7 +35,9 @@ abstract class OperatingSystemUtils {
File which(String execName); File which(String execName);
} }
class _PosixUtils implements OperatingSystemUtils { class _PosixUtils extends OperatingSystemUtils {
_PosixUtils() : super._private();
ProcessResult makeExecutable(File file) { ProcessResult makeExecutable(File file) {
return Process.runSync('chmod', ['u+x', file.path]); return Process.runSync('chmod', ['u+x', file.path]);
} }
...@@ -40,7 +53,9 @@ class _PosixUtils implements OperatingSystemUtils { ...@@ -40,7 +53,9 @@ class _PosixUtils implements OperatingSystemUtils {
} }
} }
class _WindowsUtils implements OperatingSystemUtils { class _WindowsUtils extends OperatingSystemUtils {
_WindowsUtils() : super._private();
// This is a no-op. // This is a no-op.
ProcessResult makeExecutable(File file) { ProcessResult makeExecutable(File file) {
return new ProcessResult(0, 0, null, null); return new ProcessResult(0, 0, null, null);
......
...@@ -420,7 +420,7 @@ Future<int> buildAndroid({ ...@@ -420,7 +420,7 @@ Future<int> buildAndroid({
// TODO(mpcomplete): move this to Device? // TODO(mpcomplete): move this to Device?
/// This is currently Android specific. /// This is currently Android specific.
Future buildAll( Future<int> buildAll(
DeviceStore devices, DeviceStore devices,
ApplicationPackageStore applicationPackages, ApplicationPackageStore applicationPackages,
Toolchain toolchain, Toolchain toolchain,
...@@ -434,31 +434,44 @@ Future buildAll( ...@@ -434,31 +434,44 @@ Future buildAll(
continue; continue;
// TODO(mpcomplete): Temporary hack. We only support the apk builder atm. // TODO(mpcomplete): Temporary hack. We only support the apk builder atm.
if (package == applicationPackages.android) { if (package != applicationPackages.android)
// TODO(devoncarew): Remove this warning after a few releases. continue;
if (FileSystemEntity.isDirectorySync('apk') && !FileSystemEntity.isDirectorySync('android')) {
// Tell people the android directory location changed. // TODO(devoncarew): Remove this warning after a few releases.
printStatus( if (FileSystemEntity.isDirectorySync('apk') && !FileSystemEntity.isDirectorySync('android')) {
"Warning: Flutter now looks for Android resources in the android/ directory; " // Tell people the android directory location changed.
"consider renaming your 'apk/' directory to 'android/'."); printStatus(
} "Warning: Flutter now looks for Android resources in the android/ directory; "
"consider renaming your 'apk/' directory to 'android/'.");
if (!FileSystemEntity.isFileSync(_kDefaultAndroidManifestPath)) {
printStatus('Using pre-built SkyShell.apk.');
continue;
}
int result = await buildAndroid(
toolchain: toolchain,
configs: configs,
enginePath: enginePath,
force: false,
target: target
);
if (result != 0)
return result;
} }
int result = await build(toolchain, configs, enginePath: enginePath,
target: target);
if (result != 0)
return result;
} }
return 0; return 0;
} }
Future<int> build(
Toolchain toolchain,
List<BuildConfiguration> configs, {
String enginePath,
String target: ''
}) async {
if (!FileSystemEntity.isFileSync(_kDefaultAndroidManifestPath)) {
printStatus('Using pre-built SkyShell.apk.');
return 0;
}
int result = await buildAndroid(
toolchain: toolchain,
configs: configs,
enginePath: enginePath,
force: false,
target: target
);
return result;
}
...@@ -7,15 +7,18 @@ import 'dart:async'; ...@@ -7,15 +7,18 @@ import 'dart:async';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:test/src/executable.dart' as executable; import 'package:test/src/executable.dart' as executable;
import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/os.dart';
import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils;
import '../android/android_device.dart' show AndroidDevice;
import '../application_package.dart';
import 'apk.dart' as apk;
import 'run.dart'; import 'run.dart';
import 'stop.dart'; import 'stop.dart';
typedef Future<int> RunAppFunction();
typedef Future<Null> RunTestsFunction(List<String> testArgs);
typedef Future<int> StopAppFunction();
/// Runs integration (a.k.a. end-to-end) tests. /// Runs integration (a.k.a. end-to-end) tests.
/// ///
/// An integration test is a program that runs in a separate process from your /// An integration test is a program that runs in a separate process from your
...@@ -36,31 +39,8 @@ typedef Future<int> StopAppFunction(); ...@@ -36,31 +39,8 @@ typedef Future<int> StopAppFunction();
/// the application is stopped and the command exits. If all these steps are /// the application is stopped and the command exits. If all these steps are
/// successful the exit code will be `0`. Otherwise, you will see a non-zero /// successful the exit code will be `0`. Otherwise, you will see a non-zero
/// exit code. /// exit code.
class DriveCommand extends RunCommand { class DriveCommand extends RunCommandBase {
final String name = 'drive'; DriveCommand() {
final String description = 'Runs Flutter Driver tests for the current project.';
final List<String> aliases = <String>['driver'];
RunAppFunction _runApp;
RunTestsFunction _runTests;
StopAppFunction _stopApp;
/// Creates a drive command with custom process management functions.
///
/// [runAppFn] starts a Flutter application.
///
/// [runTestsFn] runs tests.
///
/// [stopAppFn] stops the test app after tests are finished.
DriveCommand.custom({
RunAppFunction runAppFn,
RunTestsFunction runTestsFn,
StopAppFunction stopAppFn
}) {
_runApp = runAppFn ?? super.runInProject;
_runTests = runTestsFn ?? executable.main;
_stopApp = stopAppFn ?? this.stop;
argParser.addFlag( argParser.addFlag(
'keep-app-running', 'keep-app-running',
negatable: true, negatable: true,
...@@ -79,19 +59,35 @@ class DriveCommand extends RunCommand { ...@@ -79,19 +59,35 @@ class DriveCommand extends RunCommand {
'already running instance. This will also cause the driver to keep ' 'already running instance. This will also cause the driver to keep '
'the application running after tests are done.' 'the application running after tests are done.'
); );
argParser.addOption('debug-port',
defaultsTo: observatoryDefaultPort.toString(),
help: 'Listen to the given port for a debug connection.');
} }
DriveCommand() : this.custom(); final String name = 'drive';
final String description = 'Runs Flutter Driver tests for the current project.';
final List<String> aliases = <String>['driver'];
Device _device;
Device get device => _device;
bool get requiresDevice => true; int get debugPort => int.parse(argResults['debug-port']);
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
await toolchainDownloader(this);
String testFile = _getTestFile(); String testFile = _getTestFile();
if (testFile == null) { if (testFile == null) {
return 1; return 1;
} }
this._device = await targetDeviceFinder();
if (device == null) {
return 1;
}
if (await fs.type(testFile) != FileSystemEntityType.FILE) { if (await fs.type(testFile) != FileSystemEntityType.FILE) {
printError('Test file not found: $testFile'); printError('Test file not found: $testFile');
return 1; return 1;
...@@ -99,17 +95,17 @@ class DriveCommand extends RunCommand { ...@@ -99,17 +95,17 @@ class DriveCommand extends RunCommand {
if (!argResults['use-existing-app']) { if (!argResults['use-existing-app']) {
printStatus('Starting application: ${argResults["target"]}'); printStatus('Starting application: ${argResults["target"]}');
int result = await _runApp(); int result = await appStarter(this);
if (result != 0) { if (result != 0) {
printError('Application failed to start. Will not run test. Quitting.'); printError('Application failed to start. Will not run test. Quitting.');
return result; return result;
} }
} else { } else {
printStatus('Will connect to already running application instance'); printStatus('Will connect to already running application instance.');
} }
try { try {
return await _runTests([testFile]) return await testRunner([testFile])
.then((_) => 0) .then((_) => 0)
.catchError((error, stackTrace) { .catchError((error, stackTrace) {
printError('CAUGHT EXCEPTION: $error\n$stackTrace'); printError('CAUGHT EXCEPTION: $error\n$stackTrace');
...@@ -117,10 +113,15 @@ class DriveCommand extends RunCommand { ...@@ -117,10 +113,15 @@ class DriveCommand extends RunCommand {
}); });
} finally { } finally {
if (!argResults['keep-app-running'] && !argResults['use-existing-app']) { if (!argResults['keep-app-running'] && !argResults['use-existing-app']) {
printStatus('Stopping application instance'); printStatus('Stopping application instance.');
await _stopApp(); try {
await appStopper(this);
} catch(error, stackTrace) {
// TODO(yjbanov): remove this guard when this bug is fixed: https://github.com/dart-lang/sdk/issues/25862
printStatus('Could not stop application: $error\n$stackTrace');
}
} else { } else {
printStatus('Leaving the application running'); printStatus('Leaving the application running.');
} }
} }
} }
...@@ -130,7 +131,7 @@ class DriveCommand extends RunCommand { ...@@ -130,7 +131,7 @@ class DriveCommand extends RunCommand {
} }
String _getTestFile() { String _getTestFile() {
String appFile = path.normalize(argResults['target']); String appFile = path.normalize(target);
// This command extends `flutter start` and therefore CWD == package dir // This command extends `flutter start` and therefore CWD == package dir
String packageDir = getCurrentDirectory(); String packageDir = getCurrentDirectory();
...@@ -166,3 +167,159 @@ class DriveCommand extends RunCommand { ...@@ -166,3 +167,159 @@ class DriveCommand extends RunCommand {
return '${pathWithNoExtension}_test${path.extension(appFile)}'; return '${pathWithNoExtension}_test${path.extension(appFile)}';
} }
} }
/// Finds a device to test on. May launch a simulator, if necessary.
typedef Future<Device> TargetDeviceFinder();
TargetDeviceFinder targetDeviceFinder = findTargetDevice;
void restoreTargetDeviceFinder() {
targetDeviceFinder = findTargetDevice;
}
Future<Device> findTargetDevice() async {
if (deviceManager.hasSpecifiedDeviceId) {
return deviceManager.getDeviceById(deviceManager.specifiedDeviceId);
}
List<Device> devices = await deviceManager.getAllConnectedDevices();
if (os.isMacOS) {
// On Mac we look for the iOS Simulator. If available, we use that. Then
// we look for an Android device. If there's one, we use that. Otherwise,
// we launch a new iOS Simulator.
Device reusableDevice = devices.firstWhere(
(d) => d.isLocalEmulator,
orElse: () {
return devices.firstWhere((d) => d is AndroidDevice,
orElse: () => null);
}
);
if (reusableDevice != null) {
printStatus('Found connected ${reusableDevice.isLocalEmulator ? "emulator" : "device"} "${reusableDevice.name}"; will reuse it.');
return reusableDevice;
}
// No running emulator found. Attempt to start one.
printStatus('Starting iOS Simulator, because did not find existing connected devices.');
bool started = await SimControl.instance.boot();
if (started) {
return IOSSimulatorUtils.instance.getAttachedDevices().first;
} else {
printError('Failed to start iOS Simulator.');
return null;
}
} else if (os.isLinux) {
// On Linux, for now, we just grab the first connected device we can find.
if (devices.isEmpty) {
printError('No devices found.');
return null;
} else if (devices.length > 1) {
printStatus('Found multiple connected devices:');
printStatus(devices.map((d) => ' - ${d.name}\n').join(''));
}
printStatus('Using device ${devices.first.name}.');
return devices.first;
} else if (os.isWindows) {
printError('Windows is not yet supported.');
return null;
} else {
printError('The operating system on this computer is not supported.');
return null;
}
}
/// Starts the application on the device given command configuration.
typedef Future<int> AppStarter(DriveCommand command);
AppStarter appStarter = startApp;
void restoreAppStarter() {
appStarter = startApp;
}
Future<int> startApp(DriveCommand command) async {
String mainPath = findMainDartFile(command.target);
if (await fs.type(mainPath) != FileSystemEntityType.FILE) {
printError('Tried to run $mainPath, but that file does not exist.');
return 1;
}
if (command.device is AndroidDevice) {
printTrace('Building an APK.');
int result = await apk.build(command.toolchain, command.buildConfigurations,
enginePath: command.runner.enginePath, target: command.target);
if (result != 0)
return result;
}
printTrace('Stopping previously running application, if any.');
await appStopper(command);
printTrace('Installing application package.');
ApplicationPackage package = command.applicationPackages
.getPackageForPlatform(command.device.platform);
await command.device.installApp(package);
printTrace('Starting application.');
bool started = await command.device.startApp(
package,
command.toolchain,
mainPath: mainPath,
route: command.route,
checked: command.checked,
clearLogs: true,
startPaused: true,
debugPort: command.debugPort,
platformArgs: <String, dynamic>{
'trace-startup': command.traceStartup,
}
);
if (command.device.supportsStartPaused) {
await delayUntilObservatoryAvailable('localhost', command.debugPort);
}
return started ? 0 : 2;
}
/// Runs driver tests.
typedef Future<Null> TestRunner(List<String> testArgs);
TestRunner testRunner = runTests;
void restoreTestRunner() {
testRunner = runTests;
}
Future<Null> runTests(List<String> testArgs) {
printTrace('Running driver tests.');
return executable.main(testArgs);
}
/// Stops the application.
typedef Future<int> AppStopper(DriveCommand command);
AppStopper appStopper = stopApp;
void restoreAppStopper() {
appStopper = stopApp;
}
Future<int> stopApp(DriveCommand command) async {
printTrace('Stopping application.');
ApplicationPackage package = command.applicationPackages
.getPackageForPlatform(command.device.platform);
bool stopped = await command.device.stopApp(package);
return stopped ? 0 : 1;
}
/// Downloads Flutter toolchain.
typedef Future<Null> ToolchainDownloader(DriveCommand command);
ToolchainDownloader toolchainDownloader = downloadToolchain;
void restoreToolchainDownloader() {
toolchainDownloader = downloadToolchain;
}
Future<Null> downloadToolchain(DriveCommand command) async {
printTrace('Downloading toolchain.');
await Future.wait([
command.downloadToolchain(),
command.downloadApplicationPackagesAndConnectToDevices(),
], eagerError: true);
}
...@@ -51,6 +51,11 @@ abstract class RunCommandBase extends FlutterCommand { ...@@ -51,6 +51,11 @@ abstract class RunCommandBase extends FlutterCommand {
argParser.addOption('route', argParser.addOption('route',
help: 'Which route to load when starting the app.'); help: 'Which route to load when starting the app.');
} }
bool get checked => argResults['checked'];
bool get traceStartup => argResults['trace-startup'];
String get target => argResults['target'];
String get route => argResults['route'];
} }
class RunCommand extends RunCommandBase { class RunCommand extends RunCommandBase {
...@@ -219,7 +224,7 @@ Future<int> startApp( ...@@ -219,7 +224,7 @@ Future<int> startApp(
// wait for the observatory port to become available before returning from // wait for the observatory port to become available before returning from
// `startApp()`. // `startApp()`.
if (startPaused && device.supportsStartPaused) { if (startPaused && device.supportsStartPaused) {
await _delayUntilObservatoryAvailable('localhost', debugPort); await delayUntilObservatoryAvailable('localhost', debugPort);
} }
} }
} }
...@@ -242,7 +247,7 @@ Future<int> startApp( ...@@ -242,7 +247,7 @@ Future<int> startApp(
/// ///
/// This does not fail if we're unable to connect, and times out after the given /// This does not fail if we're unable to connect, and times out after the given
/// [timeout]. /// [timeout].
Future _delayUntilObservatoryAvailable(String host, int port, { Future delayUntilObservatoryAvailable(String host, int port, {
Duration timeout: const Duration(seconds: 10) Duration timeout: const Duration(seconds: 10)
}) async { }) async {
Stopwatch stopwatch = new Stopwatch()..start(); Stopwatch stopwatch = new Stopwatch()..start();
......
...@@ -130,6 +130,9 @@ abstract class Device { ...@@ -130,6 +130,9 @@ abstract class Device {
bool get supportsStartPaused => true; bool get supportsStartPaused => true;
/// Whether it is an emulated device running on localhost.
bool get isLocalEmulator;
/// Install an app package on the current device /// Install an app package on the current device
bool installApp(ApplicationPackage app); bool installApp(ApplicationPackage app);
...@@ -259,7 +262,7 @@ class DeviceStore { ...@@ -259,7 +262,7 @@ class DeviceStore {
break; break;
case TargetPlatform.iOSSimulator: case TargetPlatform.iOSSimulator:
assert(iOSSimulator == null); assert(iOSSimulator == null);
iOSSimulator = _deviceForConfig(config, IOSSimulator.getAttachedDevices()); iOSSimulator = _deviceForConfig(config, IOSSimulatorUtils.instance.getAttachedDevices());
break; break;
case TargetPlatform.mac: case TargetPlatform.mac:
case TargetPlatform.linux: case TargetPlatform.linux:
......
...@@ -62,6 +62,8 @@ class IOSDevice extends Device { ...@@ -62,6 +62,8 @@ class IOSDevice extends Device {
final String name; final String name;
bool get isLocalEmulator => false;
bool get supportsStartPaused => false; bool get supportsStartPaused => false;
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) { static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
......
...@@ -10,6 +10,7 @@ import 'package:path/path.dart' as path; ...@@ -10,6 +10,7 @@ import 'package:path/path.dart' as path;
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/context.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../build_configuration.dart'; import '../build_configuration.dart';
import '../device.dart'; import '../device.dart';
...@@ -26,12 +27,29 @@ class IOSSimulators extends PollingDeviceDiscovery { ...@@ -26,12 +27,29 @@ class IOSSimulators extends PollingDeviceDiscovery {
IOSSimulators() : super('IOSSimulators'); IOSSimulators() : super('IOSSimulators');
bool get supportsPlatform => Platform.isMacOS; bool get supportsPlatform => Platform.isMacOS;
List<Device> pollingGetDevices() => IOSSimulator.getAttachedDevices(); List<Device> pollingGetDevices() => IOSSimulatorUtils.instance.getAttachedDevices();
}
class IOSSimulatorUtils {
/// Returns [IOSSimulatorUtils] active in the current app context (i.e. zone).
static IOSSimulatorUtils get instance => context[IOSSimulatorUtils] ?? (context[IOSSimulatorUtils] = new IOSSimulatorUtils());
List<IOSSimulator> getAttachedDevices() {
if (!xcode.isInstalledAndMeetsVersionCheck)
return <IOSSimulator>[];
return SimControl.instance.getConnectedDevices().map((SimDevice device) {
return new IOSSimulator(device.udid, name: device.name);
}).toList();
}
} }
/// A wrapper around the `simctl` command line tool. /// A wrapper around the `simctl` command line tool.
class SimControl { class SimControl {
static Future<bool> boot({String deviceId}) async { /// Returns [SimControl] active in the current app context (i.e. zone).
static SimControl get instance => context[SimControl] ?? (context[SimControl] = new SimControl());
Future<bool> boot({String deviceId}) async {
if (_isAnyConnected()) if (_isAnyConnected())
return true; return true;
...@@ -65,7 +83,7 @@ class SimControl { ...@@ -65,7 +83,7 @@ class SimControl {
} }
/// Returns a list of all available devices, both potential and connected. /// Returns a list of all available devices, both potential and connected.
static List<SimDevice> getDevices() { List<SimDevice> getDevices() {
// { // {
// "devices" : { // "devices" : {
// "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [ // "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [
...@@ -102,18 +120,18 @@ class SimControl { ...@@ -102,18 +120,18 @@ class SimControl {
} }
/// Returns all the connected simulator devices. /// Returns all the connected simulator devices.
static List<SimDevice> getConnectedDevices() { List<SimDevice> getConnectedDevices() {
return getDevices().where((SimDevice device) => device.isBooted).toList(); return getDevices().where((SimDevice device) => device.isBooted).toList();
} }
static StreamController<List<SimDevice>> _trackDevicesControler; StreamController<List<SimDevice>> _trackDevicesControler;
/// Listens to changes in the set of connected devices. The implementation /// Listens to changes in the set of connected devices. The implementation
/// currently uses polling. Callers should be careful to call cancel() on any /// currently uses polling. Callers should be careful to call cancel() on any
/// stream subscription when finished. /// stream subscription when finished.
/// ///
/// TODO(devoncarew): We could investigate using the usbmuxd protocol directly. /// TODO(devoncarew): We could investigate using the usbmuxd protocol directly.
static Stream<List<SimDevice>> trackDevices() { Stream<List<SimDevice>> trackDevices() {
if (_trackDevicesControler == null) { if (_trackDevicesControler == null) {
Timer timer; Timer timer;
Set<String> deviceIds = new Set<String>(); Set<String> deviceIds = new Set<String>();
...@@ -138,7 +156,7 @@ class SimControl { ...@@ -138,7 +156,7 @@ class SimControl {
} }
/// Update the cached set of device IDs and return whether there were any changes. /// Update the cached set of device IDs and return whether there were any changes.
static bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) { bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) {
Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid)); Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid));
bool changed = false; bool changed = false;
...@@ -159,13 +177,13 @@ class SimControl { ...@@ -159,13 +177,13 @@ class SimControl {
return changed; return changed;
} }
static bool _isAnyConnected() => getConnectedDevices().isNotEmpty; bool _isAnyConnected() => getConnectedDevices().isNotEmpty;
static void install(String deviceId, String appPath) { void install(String deviceId, String appPath) {
runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]); runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]);
} }
static void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) { void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
List<String> args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier]; List<String> args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
if (launchArgs != null) if (launchArgs != null)
args.addAll(launchArgs); args.addAll(launchArgs);
...@@ -190,17 +208,10 @@ class SimDevice { ...@@ -190,17 +208,10 @@ class SimDevice {
class IOSSimulator extends Device { class IOSSimulator extends Device {
IOSSimulator(String id, { this.name }) : super(id); IOSSimulator(String id, { this.name }) : super(id);
static List<IOSSimulator> getAttachedDevices() {
if (!xcode.isInstalledAndMeetsVersionCheck)
return <IOSSimulator>[];
return SimControl.getConnectedDevices().map((SimDevice device) {
return new IOSSimulator(device.udid, name: device.name);
}).toList();
}
final String name; final String name;
bool get isLocalEmulator => true;
String get xcrunPath => path.join('/usr', 'bin', 'xcrun'); String get xcrunPath => path.join('/usr', 'bin', 'xcrun');
String _getSimulatorPath() { String _getSimulatorPath() {
...@@ -220,7 +231,7 @@ class IOSSimulator extends Device { ...@@ -220,7 +231,7 @@ class IOSSimulator extends Device {
return false; return false;
try { try {
SimControl.install(id, app.localPath); SimControl.instance.install(id, app.localPath);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
...@@ -231,7 +242,7 @@ class IOSSimulator extends Device { ...@@ -231,7 +242,7 @@ class IOSSimulator extends Device {
bool isConnected() { bool isConnected() {
if (!Platform.isMacOS) if (!Platform.isMacOS)
return false; return false;
return SimControl.getConnectedDevices().any((SimDevice device) => device.udid == id); return SimControl.instance.getConnectedDevices().any((SimDevice device) => device.udid == id);
} }
@override @override
...@@ -333,7 +344,7 @@ class IOSSimulator extends Device { ...@@ -333,7 +344,7 @@ class IOSSimulator extends Device {
} }
// Step 3: Install the updated bundle to the simulator. // Step 3: Install the updated bundle to the simulator.
SimControl.install(id, path.absolute(bundle.path)); SimControl.instance.install(id, path.absolute(bundle.path));
// Step 4: Prepare launch arguments. // Step 4: Prepare launch arguments.
List<String> args = <String>[]; List<String> args = <String>[];
...@@ -349,7 +360,7 @@ class IOSSimulator extends Device { ...@@ -349,7 +360,7 @@ class IOSSimulator extends Device {
// Step 5: Launch the updated application in the simulator. // Step 5: Launch the updated application in the simulator.
try { try {
SimControl.launch(id, app.id, args); SimControl.instance.launch(id, app.id, args);
} catch (error) { } catch (error) {
printError('$error'); printError('$error');
return false; return false;
......
...@@ -5,10 +5,15 @@ ...@@ -5,10 +5,15 @@
import 'dart:async'; import 'dart:async';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:flutter_tools/src/commands/drive.dart'; import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/commands/drive.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart'; import 'package:flutter_tools/src/globals.dart';
import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'src/common.dart'; import 'src/common.dart';
...@@ -19,18 +24,45 @@ main() => defineTests(); ...@@ -19,18 +24,45 @@ main() => defineTests();
defineTests() { defineTests() {
group('drive', () { group('drive', () {
DriveCommand command;
Device mockDevice;
void withMockDevice([Device mock]) {
mockDevice = mock ?? new MockDevice();
targetDeviceFinder = () async => mockDevice;
testDeviceManager.addDevice(mockDevice);
}
setUp(() { setUp(() {
command = new DriveCommand();
applyMocksToCommand(command);
useInMemoryFileSystem(cwd: '/some/app'); useInMemoryFileSystem(cwd: '/some/app');
toolchainDownloader = (_) async { };
targetDeviceFinder = () {
throw 'Unexpected call to targetDeviceFinder';
};
appStarter = (_) {
throw 'Unexpected call to appStarter';
};
testRunner = (_) {
throw 'Unexpected call to testRunner';
};
appStopper = (_) {
throw 'Unexpected call to appStopper';
};
}); });
tearDown(() { tearDown(() {
command = null;
restoreFileSystem(); restoreFileSystem();
restoreAppStarter();
restoreAppStopper();
restoreTestRunner();
restoreTargetDeviceFinder();
}); });
testUsingContext('returns 1 when test file is not found', () { testUsingContext('returns 1 when test file is not found', () {
DriveCommand command = new DriveCommand(); withMockDevice();
applyMocksToCommand(command);
List<String> args = [ List<String> args = [
'drive', 'drive',
'--target=/some/app/test/e2e.dart', '--target=/some/app/test/e2e.dart',
...@@ -44,10 +76,8 @@ defineTests() { ...@@ -44,10 +76,8 @@ defineTests() {
}); });
testUsingContext('returns 1 when app fails to run', () async { testUsingContext('returns 1 when app fails to run', () async {
DriveCommand command = new DriveCommand.custom(runAppFn: expectAsync(() { withMockDevice();
return new Future.value(1); appStarter = expectAsync((_) async => 1);
}));
applyMocksToCommand(command);
String testApp = '/some/app/test_driver/e2e.dart'; String testApp = '/some/app/test_driver/e2e.dart';
String testFile = '/some/app/test_driver/e2e_test.dart'; String testFile = '/some/app/test_driver/e2e_test.dart';
...@@ -72,8 +102,6 @@ defineTests() { ...@@ -72,8 +102,6 @@ defineTests() {
testUsingContext('returns 1 when app file is outside package', () async { testUsingContext('returns 1 when app file is outside package', () async {
String packageDir = '/my/app'; String packageDir = '/my/app';
useInMemoryFileSystem(cwd: packageDir); useInMemoryFileSystem(cwd: packageDir);
DriveCommand command = new DriveCommand();
applyMocksToCommand(command);
String appFile = '/not/in/my/app.dart'; String appFile = '/not/in/my/app.dart';
List<String> args = [ List<String> args = [
...@@ -92,8 +120,6 @@ defineTests() { ...@@ -92,8 +120,6 @@ defineTests() {
testUsingContext('returns 1 when app file is in the root dir', () async { testUsingContext('returns 1 when app file is in the root dir', () async {
String packageDir = '/my/app'; String packageDir = '/my/app';
useInMemoryFileSystem(cwd: packageDir); useInMemoryFileSystem(cwd: packageDir);
DriveCommand command = new DriveCommand();
applyMocksToCommand(command);
String appFile = '/my/app/main.dart'; String appFile = '/my/app/main.dart';
List<String> args = [ List<String> args = [
...@@ -111,22 +137,21 @@ defineTests() { ...@@ -111,22 +137,21 @@ defineTests() {
}); });
testUsingContext('returns 0 when test ends successfully', () async { testUsingContext('returns 0 when test ends successfully', () async {
withMockDevice();
String testApp = '/some/app/test/e2e.dart'; String testApp = '/some/app/test/e2e.dart';
String testFile = '/some/app/test_driver/e2e_test.dart'; String testFile = '/some/app/test_driver/e2e_test.dart';
DriveCommand command = new DriveCommand.custom( appStarter = expectAsync((_) {
runAppFn: expectAsync(() { return new Future<int>.value(0);
return new Future<int>.value(0); });
}), testRunner = expectAsync((List<String> testArgs) {
runTestsFn: expectAsync((List<String> testArgs) { expect(testArgs, [testFile]);
expect(testArgs, [testFile]); return new Future<Null>.value();
return new Future<Null>.value(); });
}), appStopper = expectAsync((_) {
stopAppFn: expectAsync(() { return new Future<int>.value(0);
return new Future<int>.value(0); });
})
);
applyMocksToCommand(command);
MemoryFileSystem memFs = fs; MemoryFileSystem memFs = fs;
await memFs.file(testApp).writeAsString('main() {}'); await memFs.file(testApp).writeAsString('main() {}');
...@@ -142,5 +167,88 @@ defineTests() { ...@@ -142,5 +167,88 @@ defineTests() {
expect(buffer.errorText, isEmpty); expect(buffer.errorText, isEmpty);
}); });
}); });
group('findTargetDevice', () {
testUsingContext('uses specified device', () async {
testDeviceManager.specifiedDeviceId = '123';
withMockDevice();
when(mockDevice.name).thenReturn('specified-device');
when(mockDevice.id).thenReturn('123');
Device device = await findTargetDevice();
expect(device.name, 'specified-device');
});
});
group('findTargetDevice on iOS', () {
setOs() {
when(os.isMacOS).thenReturn(true);
when(os.isLinux).thenReturn(false);
}
testUsingContext('uses existing emulator', () async {
setOs();
withMockDevice();
when(mockDevice.name).thenReturn('mock-simulator');
when(mockDevice.isLocalEmulator).thenReturn(true);
Device device = await findTargetDevice();
expect(device.name, 'mock-simulator');
});
testUsingContext('uses existing Android device if and there are no simulators', () async {
setOs();
mockDevice = new MockAndroidDevice();
when(mockDevice.name).thenReturn('mock-android-device');
when(mockDevice.isLocalEmulator).thenReturn(false);
withMockDevice(mockDevice);
Device device = await findTargetDevice();
expect(device.name, 'mock-android-device');
});
testUsingContext('launches emulator', () async {
setOs();
when(SimControl.instance.boot()).thenReturn(true);
Device emulator = new MockDevice();
when(emulator.name).thenReturn('new-simulator');
when(IOSSimulatorUtils.instance.getAttachedDevices())
.thenReturn([emulator]);
Device device = await findTargetDevice();
expect(device.name, 'new-simulator');
});
});
group('findTargetDevice on Linux', () {
setOs() {
when(os.isMacOS).thenReturn(false);
when(os.isLinux).thenReturn(true);
}
testUsingContext('returns null if no devices found', () async {
setOs();
expect(await findTargetDevice(), isNull);
});
testUsingContext('uses existing Android device', () async {
setOs();
mockDevice = new MockAndroidDevice();
when(mockDevice.name).thenReturn('mock-android-device');
withMockDevice(mockDevice);
Device device = await findTargetDevice();
expect(device.name, 'mock-android-device');
});
});
}); });
} }
class MockDevice extends Mock implements Device {
MockDevice() {
when(this.isSupported()).thenReturn(true);
}
}
class MockAndroidDevice extends Mock implements AndroidDevice { }
...@@ -15,7 +15,7 @@ defineTests() { ...@@ -15,7 +15,7 @@ defineTests() {
group('listen', () { group('listen', () {
testUsingContext('returns 1 when no device is connected', () { testUsingContext('returns 1 when no device is connected', () {
ListenCommand command = new ListenCommand(singleRun: true); ListenCommand command = new ListenCommand(singleRun: true);
applyMocksToCommand(command, noDevices: true); applyMocksToCommand(command);
return createTestCommandRunner(command).run(['listen']).then((int code) { return createTestCommandRunner(command).run(['listen']).then((int code) {
expect(code, equals(1)); expect(code, equals(1));
}); });
......
...@@ -7,9 +7,13 @@ import 'dart:io'; ...@@ -7,9 +7,13 @@ import 'dart:io';
import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
/// Return the test logger. This assumes that the current Logger is a BufferLogger. /// Return the test logger. This assumes that the current Logger is a BufferLogger.
...@@ -38,6 +42,15 @@ void testUsingContext(String description, dynamic testMethod(), { ...@@ -38,6 +42,15 @@ void testUsingContext(String description, dynamic testMethod(), {
if (!overrides.containsKey(Doctor)) if (!overrides.containsKey(Doctor))
testContext[Doctor] = new MockDoctor(); testContext[Doctor] = new MockDoctor();
if (!overrides.containsKey(SimControl))
testContext[SimControl] = new MockSimControl();
if (!overrides.containsKey(OperatingSystemUtils))
testContext[OperatingSystemUtils] = new MockOperatingSystemUtils();
if (!overrides.containsKey(IOSSimulatorUtils))
testContext[IOSSimulatorUtils] = new MockIOSSimulatorUtils();
if (Platform.isMacOS) { if (Platform.isMacOS) {
if (!overrides.containsKey(XCode)) if (!overrides.containsKey(XCode))
testContext[XCode] = new XCode(); testContext[XCode] = new XCode();
...@@ -76,3 +89,13 @@ class MockDoctor extends Doctor { ...@@ -76,3 +89,13 @@ class MockDoctor extends Doctor {
// True for testing. // True for testing.
bool get canLaunchAnything => true; bool get canLaunchAnything => true;
} }
class MockSimControl extends Mock implements SimControl {
MockSimControl() {
when(this.getConnectedDevices()).thenReturn([]);
}
}
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
class MockIOSSimulatorUtils extends Mock implements IOSSimulatorUtils {}
...@@ -12,8 +12,6 @@ import 'package:flutter_tools/src/runner/flutter_command.dart'; ...@@ -12,8 +12,6 @@ import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/toolchain.dart'; import 'package:flutter_tools/src/toolchain.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'context.dart';
class MockApplicationPackageStore extends ApplicationPackageStore { class MockApplicationPackageStore extends ApplicationPackageStore {
MockApplicationPackageStore() : super( MockApplicationPackageStore() : super(
android: new AndroidApk(localPath: '/mock/path/to/android/SkyShell.apk'), android: new AndroidApk(localPath: '/mock/path/to/android/SkyShell.apk'),
...@@ -53,13 +51,10 @@ class MockDeviceStore extends DeviceStore { ...@@ -53,13 +51,10 @@ class MockDeviceStore extends DeviceStore {
iOSSimulator: new MockIOSSimulator()); iOSSimulator: new MockIOSSimulator());
} }
void applyMocksToCommand(FlutterCommand command, { bool noDevices: false }) { void applyMocksToCommand(FlutterCommand command) {
command command
..applicationPackages = new MockApplicationPackageStore() ..applicationPackages = new MockApplicationPackageStore()
..toolchain = new MockToolchain() ..toolchain = new MockToolchain()
..devices = new MockDeviceStore() ..devices = new MockDeviceStore()
..projectRootValidator = () => true; ..projectRootValidator = () => true;
if (!noDevices)
testDeviceManager.addDevice(command.devices.android);
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment