Commit 677e63b7 authored by Yegor Jbanov's avatar Yegor Jbanov

decouple `flutter drive` from `flutter start`

flutter start's method of finding devices to run the app on is not suitable for flutter drive.

This commit also refactors several tool services to allow mocking in unit tests.
parent 0c05666e
...@@ -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,7 +434,9 @@ Future buildAll( ...@@ -434,7 +434,9 @@ 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)
continue;
// TODO(devoncarew): Remove this warning after a few releases. // TODO(devoncarew): Remove this warning after a few releases.
if (FileSystemEntity.isDirectorySync('apk') && !FileSystemEntity.isDirectorySync('android')) { if (FileSystemEntity.isDirectorySync('apk') && !FileSystemEntity.isDirectorySync('android')) {
// Tell people the android directory location changed. // Tell people the android directory location changed.
...@@ -443,9 +445,24 @@ Future buildAll( ...@@ -443,9 +445,24 @@ Future buildAll(
"consider renaming your 'apk/' directory to 'android/'."); "consider renaming your 'apk/' directory to 'android/'.");
} }
int result = await build(toolchain, configs, enginePath: enginePath,
target: target);
if (result != 0)
return result;
}
return 0;
}
Future<int> build(
Toolchain toolchain,
List<BuildConfiguration> configs, {
String enginePath,
String target: ''
}) async {
if (!FileSystemEntity.isFileSync(_kDefaultAndroidManifestPath)) { if (!FileSystemEntity.isFileSync(_kDefaultAndroidManifestPath)) {
printStatus('Using pre-built SkyShell.apk.'); printStatus('Using pre-built SkyShell.apk.');
continue; return 0;
} }
int result = await buildAndroid( int result = await buildAndroid(
...@@ -455,10 +472,6 @@ Future buildAll( ...@@ -455,10 +472,6 @@ Future buildAll(
force: false, force: false,
target: target target: target
); );
if (result != 0)
return result;
}
}
return 0; 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);
}), });
runTestsFn: expectAsync((List<String> testArgs) { testRunner = expectAsync((List<String> testArgs) {
expect(testArgs, [testFile]); expect(testArgs, [testFile]);
return new Future<Null>.value(); return new Future<Null>.value();
}), });
stopAppFn: expectAsync(() { appStopper = 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