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 {
bool _connected;
bool get isLocalEmulator => false;
List<String> adbCommandForDevice(List<String> args) {
return <String>[androidSdk.adbPath, '-s', id]..addAll(args);
}
......
......@@ -5,7 +5,10 @@
import 'dart:async';
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 {
factory 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.
ProcessResult makeExecutable(File file);
......@@ -24,7 +35,9 @@ abstract class OperatingSystemUtils {
File which(String execName);
}
class _PosixUtils implements OperatingSystemUtils {
class _PosixUtils extends OperatingSystemUtils {
_PosixUtils() : super._private();
ProcessResult makeExecutable(File file) {
return Process.runSync('chmod', ['u+x', file.path]);
}
......@@ -40,7 +53,9 @@ class _PosixUtils implements OperatingSystemUtils {
}
}
class _WindowsUtils implements OperatingSystemUtils {
class _WindowsUtils extends OperatingSystemUtils {
_WindowsUtils() : super._private();
// This is a no-op.
ProcessResult makeExecutable(File file) {
return new ProcessResult(0, 0, null, null);
......
......@@ -420,7 +420,7 @@ Future<int> buildAndroid({
// TODO(mpcomplete): move this to Device?
/// This is currently Android specific.
Future buildAll(
Future<int> buildAll(
DeviceStore devices,
ApplicationPackageStore applicationPackages,
Toolchain toolchain,
......@@ -434,31 +434,44 @@ Future buildAll(
continue;
// TODO(mpcomplete): Temporary hack. We only support the apk builder atm.
if (package == applicationPackages.android) {
// TODO(devoncarew): Remove this warning after a few releases.
if (FileSystemEntity.isDirectorySync('apk') && !FileSystemEntity.isDirectorySync('android')) {
// Tell people the android directory location changed.
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;
if (package != applicationPackages.android)
continue;
// TODO(devoncarew): Remove this warning after a few releases.
if (FileSystemEntity.isDirectorySync('apk') && !FileSystemEntity.isDirectorySync('android')) {
// Tell people the android directory location changed.
printStatus(
"Warning: Flutter now looks for Android resources in the android/ directory; "
"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)) {
printStatus('Using pre-built SkyShell.apk.');
return 0;
}
int result = await buildAndroid(
toolchain: toolchain,
configs: configs,
enginePath: enginePath,
force: false,
target: target
);
return result;
}
......@@ -51,6 +51,11 @@ abstract class RunCommandBase extends FlutterCommand {
argParser.addOption('route',
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 {
......@@ -219,7 +224,7 @@ Future<int> startApp(
// wait for the observatory port to become available before returning from
// `startApp()`.
if (startPaused && device.supportsStartPaused) {
await _delayUntilObservatoryAvailable('localhost', debugPort);
await delayUntilObservatoryAvailable('localhost', debugPort);
}
}
}
......@@ -242,7 +247,7 @@ Future<int> startApp(
///
/// This does not fail if we're unable to connect, and times out after the given
/// [timeout].
Future _delayUntilObservatoryAvailable(String host, int port, {
Future delayUntilObservatoryAvailable(String host, int port, {
Duration timeout: const Duration(seconds: 10)
}) async {
Stopwatch stopwatch = new Stopwatch()..start();
......
......@@ -130,6 +130,9 @@ abstract class Device {
bool get supportsStartPaused => true;
/// Whether it is an emulated device running on localhost.
bool get isLocalEmulator;
/// Install an app package on the current device
bool installApp(ApplicationPackage app);
......@@ -259,7 +262,7 @@ class DeviceStore {
break;
case TargetPlatform.iOSSimulator:
assert(iOSSimulator == null);
iOSSimulator = _deviceForConfig(config, IOSSimulator.getAttachedDevices());
iOSSimulator = _deviceForConfig(config, IOSSimulatorUtils.instance.getAttachedDevices());
break;
case TargetPlatform.mac:
case TargetPlatform.linux:
......
......@@ -62,6 +62,8 @@ class IOSDevice extends Device {
final String name;
bool get isLocalEmulator => false;
bool get supportsStartPaused => false;
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
......
......@@ -10,6 +10,7 @@ import 'package:path/path.dart' as path;
import '../application_package.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/process.dart';
import '../build_configuration.dart';
import '../device.dart';
......@@ -26,12 +27,29 @@ class IOSSimulators extends PollingDeviceDiscovery {
IOSSimulators() : super('IOSSimulators');
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.
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())
return true;
......@@ -65,7 +83,7 @@ class SimControl {
}
/// Returns a list of all available devices, both potential and connected.
static List<SimDevice> getDevices() {
List<SimDevice> getDevices() {
// {
// "devices" : {
// "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [
......@@ -102,18 +120,18 @@ class SimControl {
}
/// Returns all the connected simulator devices.
static List<SimDevice> getConnectedDevices() {
List<SimDevice> getConnectedDevices() {
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
/// currently uses polling. Callers should be careful to call cancel() on any
/// stream subscription when finished.
///
/// TODO(devoncarew): We could investigate using the usbmuxd protocol directly.
static Stream<List<SimDevice>> trackDevices() {
Stream<List<SimDevice>> trackDevices() {
if (_trackDevicesControler == null) {
Timer timer;
Set<String> deviceIds = new Set<String>();
......@@ -138,7 +156,7 @@ class SimControl {
}
/// 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));
bool changed = false;
......@@ -159,13 +177,13 @@ class SimControl {
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]);
}
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];
if (launchArgs != null)
args.addAll(launchArgs);
......@@ -190,17 +208,10 @@ class SimDevice {
class IOSSimulator extends Device {
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;
bool get isLocalEmulator => true;
String get xcrunPath => path.join('/usr', 'bin', 'xcrun');
String _getSimulatorPath() {
......@@ -220,7 +231,7 @@ class IOSSimulator extends Device {
return false;
try {
SimControl.install(id, app.localPath);
SimControl.instance.install(id, app.localPath);
return true;
} catch (e) {
return false;
......@@ -231,7 +242,7 @@ class IOSSimulator extends Device {
bool isConnected() {
if (!Platform.isMacOS)
return false;
return SimControl.getConnectedDevices().any((SimDevice device) => device.udid == id);
return SimControl.instance.getConnectedDevices().any((SimDevice device) => device.udid == id);
}
@override
......@@ -333,7 +344,7 @@ class IOSSimulator extends Device {
}
// 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.
List<String> args = <String>[];
......@@ -349,7 +360,7 @@ class IOSSimulator extends Device {
// Step 5: Launch the updated application in the simulator.
try {
SimControl.launch(id, app.id, args);
SimControl.instance.launch(id, app.id, args);
} catch (error) {
printError('$error');
return false;
......
......@@ -5,10 +5,15 @@
import 'dart:async';
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/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/ios/simulators.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'src/common.dart';
......@@ -19,18 +24,45 @@ main() => defineTests();
defineTests() {
group('drive', () {
DriveCommand command;
Device mockDevice;
void withMockDevice([Device mock]) {
mockDevice = mock ?? new MockDevice();
targetDeviceFinder = () async => mockDevice;
testDeviceManager.addDevice(mockDevice);
}
setUp(() {
command = new DriveCommand();
applyMocksToCommand(command);
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(() {
command = null;
restoreFileSystem();
restoreAppStarter();
restoreAppStopper();
restoreTestRunner();
restoreTargetDeviceFinder();
});
testUsingContext('returns 1 when test file is not found', () {
DriveCommand command = new DriveCommand();
applyMocksToCommand(command);
withMockDevice();
List<String> args = [
'drive',
'--target=/some/app/test/e2e.dart',
......@@ -44,10 +76,8 @@ defineTests() {
});
testUsingContext('returns 1 when app fails to run', () async {
DriveCommand command = new DriveCommand.custom(runAppFn: expectAsync(() {
return new Future.value(1);
}));
applyMocksToCommand(command);
withMockDevice();
appStarter = expectAsync((_) async => 1);
String testApp = '/some/app/test_driver/e2e.dart';
String testFile = '/some/app/test_driver/e2e_test.dart';
......@@ -72,8 +102,6 @@ defineTests() {
testUsingContext('returns 1 when app file is outside package', () async {
String packageDir = '/my/app';
useInMemoryFileSystem(cwd: packageDir);
DriveCommand command = new DriveCommand();
applyMocksToCommand(command);
String appFile = '/not/in/my/app.dart';
List<String> args = [
......@@ -92,8 +120,6 @@ defineTests() {
testUsingContext('returns 1 when app file is in the root dir', () async {
String packageDir = '/my/app';
useInMemoryFileSystem(cwd: packageDir);
DriveCommand command = new DriveCommand();
applyMocksToCommand(command);
String appFile = '/my/app/main.dart';
List<String> args = [
......@@ -111,22 +137,21 @@ defineTests() {
});
testUsingContext('returns 0 when test ends successfully', () async {
withMockDevice();
String testApp = '/some/app/test/e2e.dart';
String testFile = '/some/app/test_driver/e2e_test.dart';
DriveCommand command = new DriveCommand.custom(
runAppFn: expectAsync(() {
return new Future<int>.value(0);
}),
runTestsFn: expectAsync((List<String> testArgs) {
expect(testArgs, [testFile]);
return new Future<Null>.value();
}),
stopAppFn: expectAsync(() {
return new Future<int>.value(0);
})
);
applyMocksToCommand(command);
appStarter = expectAsync((_) {
return new Future<int>.value(0);
});
testRunner = expectAsync((List<String> testArgs) {
expect(testArgs, [testFile]);
return new Future<Null>.value();
});
appStopper = expectAsync((_) {
return new Future<int>.value(0);
});
MemoryFileSystem memFs = fs;
await memFs.file(testApp).writeAsString('main() {}');
......@@ -142,5 +167,88 @@ defineTests() {
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() {
group('listen', () {
testUsingContext('returns 1 when no device is connected', () {
ListenCommand command = new ListenCommand(singleRun: true);
applyMocksToCommand(command, noDevices: true);
applyMocksToCommand(command);
return createTestCommandRunner(command).run(['listen']).then((int code) {
expect(code, equals(1));
});
......
......@@ -7,9 +7,13 @@ import 'dart:io';
import 'package:flutter_tools/src/base/context.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/doctor.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';
/// Return the test logger. This assumes that the current Logger is a BufferLogger.
......@@ -38,6 +42,15 @@ void testUsingContext(String description, dynamic testMethod(), {
if (!overrides.containsKey(Doctor))
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 (!overrides.containsKey(XCode))
testContext[XCode] = new XCode();
......@@ -76,3 +89,13 @@ class MockDoctor extends Doctor {
// True for testing.
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';
import 'package:flutter_tools/src/toolchain.dart';
import 'package:mockito/mockito.dart';
import 'context.dart';
class MockApplicationPackageStore extends ApplicationPackageStore {
MockApplicationPackageStore() : super(
android: new AndroidApk(localPath: '/mock/path/to/android/SkyShell.apk'),
......@@ -53,13 +51,10 @@ class MockDeviceStore extends DeviceStore {
iOSSimulator: new MockIOSSimulator());
}
void applyMocksToCommand(FlutterCommand command, { bool noDevices: false }) {
void applyMocksToCommand(FlutterCommand command) {
command
..applicationPackages = new MockApplicationPackageStore()
..toolchain = new MockToolchain()
..devices = new MockDeviceStore()
..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