Commit dc4830b0 authored by Devon Carew's avatar Devon Carew

Merge pull request #2123 from devoncarew/device_commands

additional validation for device commands
parents 21057de9 37290d86
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io' as dart_io; import 'dart:io' as dart_io;
import 'package:file/io.dart'; import 'package:file/io.dart';
import 'package:file/sync_io.dart'; import 'package:file/sync_io.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
......
...@@ -83,6 +83,8 @@ class DriveCommand extends RunCommand { ...@@ -83,6 +83,8 @@ class DriveCommand extends RunCommand {
DriveCommand() : this.custom(); DriveCommand() : this.custom();
bool get requiresDevice => true;
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
String testFile = _getTestFile(); String testFile = _getTestFile();
......
...@@ -3,41 +3,29 @@ ...@@ -3,41 +3,29 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:io';
import '../application_package.dart'; import '../application_package.dart';
import '../device.dart'; import '../device.dart';
import '../ios/simulators.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
class InstallCommand extends FlutterCommand { class InstallCommand extends FlutterCommand {
final String name = 'install'; final String name = 'install';
final String description = 'Install Flutter apps on attached devices.'; final String description = 'Install Flutter apps on attached devices.';
InstallCommand() { bool get requiresDevice => true;
argParser.addFlag('boot', help: 'Boot the iOS Simulator if it isn\'t already running.');
}
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
await downloadApplicationPackagesAndConnectToDevices(); await downloadApplicationPackagesAndConnectToDevices();
bool installedAny = await installApp( bool installedAny = await installApp(devices, applicationPackages);
devices,
applicationPackages,
boot: argResults['boot']
);
return installedAny ? 0 : 2; return installedAny ? 0 : 2;
} }
} }
Future<bool> installApp( Future<bool> installApp(
DeviceStore devices, DeviceStore devices,
ApplicationPackageStore applicationPackages, { ApplicationPackageStore applicationPackages
bool boot: false ) async {
}) async {
if (boot && Platform.isMacOS)
await SimControl.boot();
bool installedSomewhere = false; bool installedSomewhere = false;
for (Device device in devices.all) { for (Device device in devices.all) {
......
...@@ -22,6 +22,10 @@ class ListenCommand extends RunCommandBase { ...@@ -22,6 +22,10 @@ class ListenCommand extends RunCommandBase {
ListenCommand({ this.singleRun: false }); ListenCommand({ this.singleRun: false });
bool get androidOnly => true;
bool get requiresDevice => true;
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
await downloadApplicationPackagesAndConnectToDevices(); await downloadApplicationPackagesAndConnectToDevices();
......
...@@ -22,6 +22,8 @@ class LogsCommand extends FlutterCommand { ...@@ -22,6 +22,8 @@ class LogsCommand extends FlutterCommand {
bool get requiresProjectRoot => false; bool get requiresProjectRoot => false;
bool get requiresDevice => true;
Future<int> runInProject() async { Future<int> runInProject() async {
List<Device> devices = await deviceManager.getDevices(); List<Device> devices = await deviceManager.getDevices();
......
...@@ -23,6 +23,10 @@ class RefreshCommand extends FlutterCommand { ...@@ -23,6 +23,10 @@ class RefreshCommand extends FlutterCommand {
); );
} }
bool get androidOnly => true;
bool get requiresDevice => true;
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
printTrace('Downloading toolchain.'); printTrace('Downloading toolchain.');
......
...@@ -55,8 +55,7 @@ abstract class RunCommandBase extends FlutterCommand { ...@@ -55,8 +55,7 @@ abstract class RunCommandBase extends FlutterCommand {
class RunCommand extends RunCommandBase { class RunCommand extends RunCommandBase {
final String name = 'run'; final String name = 'run';
final String description = final String description = 'Run your Flutter app on an attached device.';
'Run your Flutter app on an attached device (defaults to checked/debug mode).';
final List<String> aliases = <String>['start']; final List<String> aliases = <String>['start'];
RunCommand() { RunCommand() {
...@@ -78,6 +77,8 @@ class RunCommand extends RunCommandBase { ...@@ -78,6 +77,8 @@ class RunCommand extends RunCommandBase {
help: 'Listen to the given port for a debug connection.'); help: 'Listen to the given port for a debug connection.');
} }
bool get requiresDevice => true;
@override @override
Future<int> run() async { Future<int> run() async {
if (argResults['pub']) if (argResults['pub'])
......
...@@ -51,12 +51,17 @@ class TestCommand extends FlutterCommand { ...@@ -51,12 +51,17 @@ class TestCommand extends FlutterCommand {
} }
TestCommand() { TestCommand() {
argParser.addFlag('flutter-repo', help: 'Run tests from the \'flutter\' package in the Flutter repository instead of the current directory.', defaultsTo: false); argParser.addFlag(
'flutter-repo',
help: 'Run tests from the \'flutter\' package in the Flutter repository instead of the current directory.',
defaultsTo: false
);
} }
Iterable<String> _findTests(Directory directory) { Iterable<String> _findTests(Directory directory) {
return directory.listSync(recursive: true, followLinks: false) return directory.listSync(recursive: true, followLinks: false)
.where((FileSystemEntity entity) => entity.path.endsWith('_test.dart') && FileSystemEntity.isFileSync(entity.path)) .where((FileSystemEntity entity) => entity.path.endsWith('_test.dart') &&
FileSystemEntity.isFileSync(entity.path))
.map((FileSystemEntity entity) => path.absolute(entity.path)); .map((FileSystemEntity entity) => path.absolute(entity.path));
} }
......
...@@ -25,6 +25,10 @@ class TraceCommand extends FlutterCommand { ...@@ -25,6 +25,10 @@ class TraceCommand extends FlutterCommand {
defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.'); defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.');
} }
bool get androidOnly => true;
bool get requiresDevice => true;
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
await downloadApplicationPackagesAndConnectToDevices(); await downloadApplicationPackagesAndConnectToDevices();
......
...@@ -39,7 +39,9 @@ class Doctor { ...@@ -39,7 +39,9 @@ class Doctor {
List<DoctorValidator> _validators = <DoctorValidator>[]; List<DoctorValidator> _validators = <DoctorValidator>[];
Iterable<Workflow> get workflows => _validators.where((DoctorValidator validator) => validator is Workflow); List<Workflow> get workflows {
return new List<Workflow>.from(_validators.where((DoctorValidator validator) => validator is Workflow));
}
/// Print a summary of the state of the tooling, as well as how to get more info. /// Print a summary of the state of the tooling, as well as how to get more info.
void summary() => printStatus(summaryText); void summary() => printStatus(summaryText);
......
...@@ -22,31 +22,68 @@ abstract class FlutterCommand extends Command { ...@@ -22,31 +22,68 @@ abstract class FlutterCommand extends Command {
/// Whether this command needs to be run from the root of a project. /// Whether this command needs to be run from the root of a project.
bool get requiresProjectRoot => true; bool get requiresProjectRoot => true;
/// Whether this command requires a (single) Flutter target device to be connected.
bool get requiresDevice => false;
/// Whether this command only applies to Android devices.
bool get androidOnly => false;
List<BuildConfiguration> get buildConfigurations => runner.buildConfigurations; List<BuildConfiguration> get buildConfigurations => runner.buildConfigurations;
Future downloadApplicationPackages() async { Future downloadToolchain() async {
if (applicationPackages == null) toolchain ??= await Toolchain.forConfigs(buildConfigurations);
applicationPackages = await ApplicationPackageStore.forConfigs(buildConfigurations);
} }
Future downloadToolchain() async { Future downloadApplicationPackagesAndConnectToDevices() async {
if (toolchain == null) await _downloadApplicationPackages();
toolchain = await Toolchain.forConfigs(buildConfigurations); _connectToDevices();
} }
void connectToDevices() { Future _downloadApplicationPackages() async {
if (devices == null) applicationPackages ??= await ApplicationPackageStore.forConfigs(buildConfigurations);
devices = new DeviceStore.forConfigs(buildConfigurations);
} }
Future downloadApplicationPackagesAndConnectToDevices() async { void _connectToDevices() {
await downloadApplicationPackages(); devices ??= new DeviceStore.forConfigs(buildConfigurations);
connectToDevices();
} }
Future<int> run() async { Future<int> run() async {
if (requiresProjectRoot && !projectRootValidator()) if (requiresProjectRoot && !projectRootValidator())
return 1; return 1;
// Ensure at least one toolchain is installed.
if (requiresDevice && !doctor.canLaunchAnything) {
printError("Unable to locate a development device; please run 'flutter doctor' "
"for information about installing additional components.");
return 1;
}
// Validate devices.
if (requiresDevice) {
List<Device> devices = await deviceManager.getDevices();
if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) {
printError("No device found with id '${deviceManager.specifiedDeviceId}'.");
return 1;
} else if (devices.isEmpty) {
printStatus('No connected devices.');
return 1;
}
devices = devices.where((Device device) => device.isSupported()).toList();
if (androidOnly)
devices = devices.where((Device device) => device.platform == TargetPlatform.android).toList();
// TODO(devoncarew): Switch this to just supporting one connected device?
if (devices.isEmpty) {
printStatus('No supported devices connected.');
return 1;
}
_devicesForCommand = await _getDevicesForCommand();
}
return await runInProject(); return await runInProject();
} }
...@@ -63,7 +100,36 @@ abstract class FlutterCommand extends Command { ...@@ -63,7 +100,36 @@ abstract class FlutterCommand extends Command {
Future<int> runInProject(); Future<int> runInProject();
List<Device> get devicesForCommand => _devicesForCommand;
Device get deviceForCommand {
// TODO(devoncarew): Switch this to just supporting one connected device?
return devicesForCommand.isNotEmpty ? devicesForCommand.first : null;
}
// This is caculated in run() if the command has [requiresDevice] specified.
List<Device> _devicesForCommand;
ApplicationPackageStore applicationPackages; ApplicationPackageStore applicationPackages;
Toolchain toolchain; Toolchain toolchain;
DeviceStore devices; DeviceStore devices;
Future<List<Device>> _getDevicesForCommand() async {
List<Device> devices = await deviceManager.getDevices();
if (devices.isEmpty)
return null;
if (deviceManager.hasSpecifiedDeviceId) {
Device device = await deviceManager.getDeviceById(deviceManager.specifiedDeviceId);
return device == null ? <Device>[] : <Device>[device];
}
devices = devices.where((Device device) => device.isSupported()).toList();
if (androidOnly)
devices = devices.where((Device device) => device.platform == TargetPlatform.android).toList();
return devices;
}
} }
...@@ -8,12 +8,14 @@ import 'dart:io'; ...@@ -8,12 +8,14 @@ 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/commands/daemon.dart'; import 'package:flutter_tools/src/commands/daemon.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/globals.dart'; import 'package:flutter_tools/src/globals.dart';
import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/mac.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'src/context.dart';
import 'src/mocks.dart'; import 'src/mocks.dart';
main() => defineTests(); main() => defineTests();
...@@ -37,6 +39,7 @@ defineTests() { ...@@ -37,6 +39,7 @@ defineTests() {
appContext[Doctor] = new Doctor(); appContext[Doctor] = new Doctor();
if (Platform.isMacOS) if (Platform.isMacOS)
appContext[XCode] = new XCode(); appContext[XCode] = new XCode();
appContext[DeviceManager] = new MockDeviceManager();
}); });
tearDown(() { tearDown(() {
......
...@@ -31,6 +31,8 @@ defineTests() { ...@@ -31,6 +31,8 @@ defineTests() {
when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false); when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false);
when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false); when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false);
testDeviceManager.addDevice(mockDevices.android);
return createTestCommandRunner(command).run(['install']).then((int code) { return createTestCommandRunner(command).run(['install']).then((int code) {
expect(code, equals(0)); expect(code, equals(0));
}); });
...@@ -53,6 +55,8 @@ defineTests() { ...@@ -53,6 +55,8 @@ defineTests() {
when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false); when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false);
when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false); when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false);
testDeviceManager.addDevice(mockDevices.iOS);
return createTestCommandRunner(command).run(['install']).then((int code) { return createTestCommandRunner(command).run(['install']).then((int code) {
expect(code, equals(0)); expect(code, equals(0));
}); });
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_tools/src/commands/listen.dart'; import 'package:flutter_tools/src/commands/listen.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'src/common.dart'; import 'src/common.dart';
...@@ -14,17 +13,11 @@ main() => defineTests(); ...@@ -14,17 +13,11 @@ main() => defineTests();
defineTests() { defineTests() {
group('listen', () { group('listen', () {
testUsingContext('returns 0 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); applyMocksToCommand(command, noDevices: true);
MockDeviceStore mockDevices = command.devices;
when(mockDevices.android.isConnected()).thenReturn(false);
when(mockDevices.iOS.isConnected()).thenReturn(false);
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
return createTestCommandRunner(command).run(['listen']).then((int code) { return createTestCommandRunner(command).run(['listen']).then((int code) {
expect(code, equals(0)); expect(code, equals(1));
}); });
}); });
}); });
......
...@@ -15,6 +15,9 @@ import 'package:test/test.dart'; ...@@ -15,6 +15,9 @@ 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.
BufferLogger get testLogger => context[Logger]; BufferLogger get testLogger => context[Logger];
MockDeviceManager get testDeviceManager => context[DeviceManager];
MockDoctor get testDoctor => context[Doctor];
void testUsingContext(String description, dynamic testMethod(), { void testUsingContext(String description, dynamic testMethod(), {
Timeout timeout, Timeout timeout,
Map<Type, dynamic> overrides: const <Type, dynamic>{} Map<Type, dynamic> overrides: const <Type, dynamic>{}
...@@ -33,7 +36,7 @@ void testUsingContext(String description, dynamic testMethod(), { ...@@ -33,7 +36,7 @@ void testUsingContext(String description, dynamic testMethod(), {
testContext[DeviceManager] = new MockDeviceManager(); testContext[DeviceManager] = new MockDeviceManager();
if (!overrides.containsKey(Doctor)) if (!overrides.containsKey(Doctor))
testContext[Doctor] = new Doctor(); testContext[Doctor] = new MockDoctor();
if (Platform.isMacOS) { if (Platform.isMacOS) {
if (!overrides.containsKey(XCode)) if (!overrides.containsKey(XCode))
...@@ -45,10 +48,31 @@ void testUsingContext(String description, dynamic testMethod(), { ...@@ -45,10 +48,31 @@ void testUsingContext(String description, dynamic testMethod(), {
} }
class MockDeviceManager implements DeviceManager { class MockDeviceManager implements DeviceManager {
List<Device> devices = <Device>[];
String specifiedDeviceId; String specifiedDeviceId;
bool get hasSpecifiedDeviceId => specifiedDeviceId != null; bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
Future<List<Device>> getAllConnectedDevices() => new Future.value(<Device>[]); Future<List<Device>> getAllConnectedDevices() => new Future.value(devices);
Future<Device> getDeviceById(String deviceId) => new Future.value(null);
Future<List<Device>> getDevices() => getAllConnectedDevices(); Future<Device> getDeviceById(String deviceId) {
Device device = devices.firstWhere((Device device) => device.id == deviceId, orElse: () => null);
return new Future.value(device);
}
Future<List<Device>> getDevices() async {
if (specifiedDeviceId == null) {
return getAllConnectedDevices();
} else {
Device device = await getDeviceById(specifiedDeviceId);
return device == null ? <Device>[] : <Device>[device];
}
}
void addDevice(Device device) => devices.add(device);
}
class MockDoctor extends Doctor {
// True for testing.
bool get canLaunchAnything => true;
} }
...@@ -12,6 +12,8 @@ import 'package:flutter_tools/src/runner/flutter_command.dart'; ...@@ -12,6 +12,8 @@ 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'),
...@@ -31,14 +33,17 @@ class MockToolchain extends Toolchain { ...@@ -31,14 +33,17 @@ class MockToolchain extends Toolchain {
class MockAndroidDevice extends Mock implements AndroidDevice { class MockAndroidDevice extends Mock implements AndroidDevice {
TargetPlatform get platform => TargetPlatform.android; TargetPlatform get platform => TargetPlatform.android;
bool isSupported() => true;
} }
class MockIOSDevice extends Mock implements IOSDevice { class MockIOSDevice extends Mock implements IOSDevice {
TargetPlatform get platform => TargetPlatform.iOS; TargetPlatform get platform => TargetPlatform.iOS;
bool isSupported() => true;
} }
class MockIOSSimulator extends Mock implements IOSSimulator { class MockIOSSimulator extends Mock implements IOSSimulator {
TargetPlatform get platform => TargetPlatform.iOSSimulator; TargetPlatform get platform => TargetPlatform.iOSSimulator;
bool isSupported() => true;
} }
class MockDeviceStore extends DeviceStore { class MockDeviceStore extends DeviceStore {
...@@ -48,10 +53,13 @@ class MockDeviceStore extends DeviceStore { ...@@ -48,10 +53,13 @@ class MockDeviceStore extends DeviceStore {
iOSSimulator: new MockIOSSimulator()); iOSSimulator: new MockIOSSimulator());
} }
void applyMocksToCommand(FlutterCommand command) { void applyMocksToCommand(FlutterCommand command, { bool noDevices: false }) {
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