Commit a9cbe436 authored by Ian Fischer's avatar Ian Fischer

Merge pull request #67 from iansf/ios_basics

Beginning implementation of IOSDevice. Implements list and install.
parents 524d8379 12192d00
...@@ -42,6 +42,15 @@ class AndroidApk extends ApplicationPackage { ...@@ -42,6 +42,15 @@ class AndroidApk extends ApplicationPackage {
: super(path.join(appDir, 'apks'), appPackageID, appFileName); : super(path.join(appDir, 'apks'), appPackageID, appFileName);
} }
class IOSApp extends ApplicationPackage {
static const String _appName = 'SkyShell.app';
static const String _packageID = 'com.google.SkyShell';
IOSApp(String appDir,
{String appPackageID: _packageID, String appFileName: _appName})
: super(appDir, appPackageID, appFileName);
}
enum BuildType { prebuilt, release, debug, } enum BuildType { prebuilt, release, debug, }
enum BuildPlatform { android, iOS, iOSSimulator, mac, linux, } enum BuildPlatform { android, iOS, iOSSimulator, mac, linux, }
...@@ -62,7 +71,10 @@ class ApplicationPackageFactory { ...@@ -62,7 +71,10 @@ class ApplicationPackageFactory {
static BuildType defaultBuildType = BuildType.prebuilt; static BuildType defaultBuildType = BuildType.prebuilt;
/// Default BuildPlatforms chosen if no BuildPlatforms are specified. /// Default BuildPlatforms chosen if no BuildPlatforms are specified.
static List<BuildPlatform> defaultBuildPlatforms = [BuildPlatform.android]; static List<BuildPlatform> defaultBuildPlatforms = [
BuildPlatform.android,
BuildPlatform.iOS,
];
static Map<BuildPlatform, ApplicationPackage> getAvailableApplicationPackages( static Map<BuildPlatform, ApplicationPackage> getAvailableApplicationPackages(
{BuildType requestedType, List<BuildPlatform> requestedPlatforms}) { {BuildType requestedType, List<BuildPlatform> requestedPlatforms}) {
...@@ -80,6 +92,9 @@ class ApplicationPackageFactory { ...@@ -80,6 +92,9 @@ class ApplicationPackageFactory {
case BuildPlatform.android: case BuildPlatform.android:
packages[platform] = new AndroidApk(buildPath); packages[platform] = new AndroidApk(buildPath);
break; break;
case BuildPlatform.iOS:
packages[platform] = new IOSApp(buildPath);
break;
default: default:
// TODO(iansf): Add other platforms // TODO(iansf): Add other platforms
assert(false); assert(false);
......
...@@ -24,6 +24,8 @@ abstract class _Device { ...@@ -24,6 +24,8 @@ abstract class _Device {
if (id == null) { if (id == null) {
if (className == AndroidDevice.className) { if (className == AndroidDevice.className) {
id = AndroidDevice.defaultDeviceID; id = AndroidDevice.defaultDeviceID;
} else if (className == IOSDevice.className) {
id = IOSDevice.defaultDeviceID;
} else { } else {
throw 'Attempted to create a Device of unknown type $className'; throw 'Attempted to create a Device of unknown type $className';
} }
...@@ -34,6 +36,10 @@ abstract class _Device { ...@@ -34,6 +36,10 @@ abstract class _Device {
final device = new AndroidDevice._(id); final device = new AndroidDevice._(id);
_deviceCache[id] = device; _deviceCache[id] = device;
return device; return device;
} else if (className == IOSDevice.className) {
final device = new IOSDevice._(id);
_deviceCache[id] = device;
return device;
} else { } else {
throw 'Attempted to create a Device of unknown type $className'; throw 'Attempted to create a Device of unknown type $className';
} }
...@@ -52,13 +58,120 @@ abstract class _Device { ...@@ -52,13 +58,120 @@ abstract class _Device {
bool isAppInstalled(ApplicationPackage app); bool isAppInstalled(ApplicationPackage app);
} }
class IOSDevice extends _Device {
static const String className = 'IOSDevice';
static final String defaultDeviceID = 'default_ios_id';
static const String _macInstructions =
'To work with iOS devices, please install ideviceinstaller. '
'If you use homebrew, you can install it with '
'"\$ brew install ideviceinstaller".';
static const String _linuxInstructions =
'To work with iOS devices, please install ideviceinstaller. '
'On Ubuntu or Debian, you can install it with '
'"\$ apt-get install ideviceinstaller".';
String _installerPath;
String get installerPath => _installerPath;
String _listerPath;
String get listerPath => _listerPath;
String _informerPath;
String get informerPath => _informerPath;
String _name;
String get name => _name;
factory IOSDevice({String id, String name}) {
IOSDevice device = new _Device(className, id);
device._name = name;
return device;
}
IOSDevice._(String id) : super._(id) {
_installerPath = _checkForCommand('ideviceinstaller');
_listerPath = _checkForCommand('idevice_id');
_informerPath = _checkForCommand('ideviceinfo');
}
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
List<IOSDevice> devices = [];
for (String id in _getAttachedDeviceIDs(mockIOS)) {
String name = _getDeviceName(id, mockIOS);
devices.add(new IOSDevice(id: id, name: name));
}
return devices;
}
static List<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
String listerPath =
(mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
return runSync([listerPath, '-l']).trim().split('\n');
}
static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
String informerPath = (mockIOS != null)
? mockIOS.informerPath
: _checkForCommand('ideviceinfo');
return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]);
}
static final Map<String, String> _commandMap = {};
static String _checkForCommand(String command,
[String macInstructions = _macInstructions,
String linuxInstructions = _linuxInstructions]) {
return _commandMap.putIfAbsent(command, () {
try {
command = runCheckedSync(['which', command]).trim();
} catch (e) {
if (Platform.isMacOS) {
_logging.severe(macInstructions);
} else if (Platform.isLinux) {
_logging.severe(linuxInstructions);
} else {
_logging.severe('$command is not available on your platform.');
}
exit(2);
}
return command;
});
}
@override
bool installApp(ApplicationPackage app) {
if (id == defaultDeviceID) {
runCheckedSync([installerPath, '-i', app.appPath]);
} else {
runCheckedSync([installerPath, '-u', id, '-i', app.appPath]);
}
return false;
}
@override
bool isConnected() {
List<String> ids = _getAttachedDeviceIDs();
for (String id in ids) {
if (id == this.id || this.id == defaultDeviceID) {
return true;
}
}
return false;
}
@override
bool isAppInstalled(ApplicationPackage app) {
return false;
}
}
class AndroidDevice extends _Device { class AndroidDevice extends _Device {
static const String _ADB_PATH = 'adb'; static const String _ADB_PATH = 'adb';
static const String _observatoryPort = '8181'; static const String _observatoryPort = '8181';
static const String _serverPort = '9888'; static const String _serverPort = '9888';
static const String className = 'AndroidDevice'; static const String className = 'AndroidDevice';
static final String defaultDeviceID = 'default'; static final String defaultDeviceID = 'default_android_device';
String productID; String productID;
String modelID; String modelID;
...@@ -85,10 +198,8 @@ class AndroidDevice extends _Device { ...@@ -85,10 +198,8 @@ class AndroidDevice extends _Device {
/// we don't have to rely on the test setup having adb available to it. /// we don't have to rely on the test setup having adb available to it.
static List<AndroidDevice> getAttachedDevices([AndroidDevice mockAndroid]) { static List<AndroidDevice> getAttachedDevices([AndroidDevice mockAndroid]) {
List<AndroidDevice> devices = []; List<AndroidDevice> devices = [];
String adbPath = _getAdbPath(); String adbPath =
if (mockAndroid != null) { (mockAndroid != null) ? mockAndroid.adbPath : _getAdbPath();
adbPath = mockAndroid.adbPath;
}
List<String> output = List<String> output =
runSync([adbPath, 'devices', '-l']).trim().split('\n'); runSync([adbPath, 'devices', '-l']).trim().split('\n');
RegExp deviceInfo = new RegExp( RegExp deviceInfo = new RegExp(
......
...@@ -16,15 +16,12 @@ class InstallCommand extends Command { ...@@ -16,15 +16,12 @@ class InstallCommand extends Command {
final description = 'Install your Flutter app on attached devices.'; final description = 'Install your Flutter app on attached devices.';
AndroidDevice android = null; AndroidDevice android = null;
IOSDevice ios;
InstallCommand([this.android]); InstallCommand({this.android, this.ios});
@override @override
Future<int> run() async { Future<int> run() async {
if (android == null) {
android = new AndroidDevice();
}
if (install()) { if (install()) {
return 0; return 0;
} else { } else {
...@@ -33,15 +30,28 @@ class InstallCommand extends Command { ...@@ -33,15 +30,28 @@ class InstallCommand extends Command {
} }
bool install() { bool install() {
if (android == null) {
android = new AndroidDevice();
}
if (ios == null) {
ios = new IOSDevice();
}
bool installedSomewhere = false; bool installedSomewhere = false;
Map<BuildPlatform, ApplicationPackage> packages = Map<BuildPlatform, ApplicationPackage> packages =
ApplicationPackageFactory.getAvailableApplicationPackages(); ApplicationPackageFactory.getAvailableApplicationPackages();
ApplicationPackage androidApp = packages[BuildPlatform.android]; ApplicationPackage androidApp = packages[BuildPlatform.android];
ApplicationPackage iosApp = packages[BuildPlatform.iOS];
if (androidApp != null && android.isConnected()) { if (androidApp != null && android.isConnected()) {
installedSomewhere = android.installApp(androidApp) || installedSomewhere; installedSomewhere = android.installApp(androidApp) || installedSomewhere;
} }
if (iosApp != null && ios.isConnected()) {
installedSomewhere = ios.installApp(iosApp) || installedSomewhere;
}
return installedSomewhere; return installedSomewhere;
} }
} }
...@@ -16,8 +16,9 @@ class ListCommand extends Command { ...@@ -16,8 +16,9 @@ class ListCommand extends Command {
final name = 'list'; final name = 'list';
final description = 'List all connected devices.'; final description = 'List all connected devices.';
AndroidDevice android; AndroidDevice android;
IOSDevice ios;
ListCommand([this.android]) { ListCommand({this.android, this.ios}) {
argParser.addFlag('details', argParser.addFlag('details',
abbr: 'd', abbr: 'd',
negatable: false, negatable: false,
...@@ -40,6 +41,18 @@ class ListCommand extends Command { ...@@ -40,6 +41,18 @@ class ListCommand extends Command {
print(device.id); print(device.id);
} }
} }
if (details) {
print('iOS Devices:');
}
for (IOSDevice device in IOSDevice.getAttachedDevices(ios)) {
if (details) {
print('${device.id}\t${device.name}');
} else {
print(device.id);
}
}
return 0; return 0;
} }
} }
...@@ -20,8 +20,9 @@ class StartCommand extends Command { ...@@ -20,8 +20,9 @@ class StartCommand extends Command {
final name = 'start'; final name = 'start';
final description = 'Start your Flutter app on attached devices.'; final description = 'Start your Flutter app on attached devices.';
AndroidDevice android = null; AndroidDevice android = null;
IOSDevice ios = null;
StartCommand([this.android]) { StartCommand({this.android, this.ios}) {
argParser.addFlag('poke', argParser.addFlag('poke',
negatable: false, negatable: false,
help: 'Restart the connection to the server (Android only).'); help: 'Restart the connection to the server (Android only).');
...@@ -48,7 +49,7 @@ class StartCommand extends Command { ...@@ -48,7 +49,7 @@ class StartCommand extends Command {
stopper.stop(); stopper.stop();
// Only install if the user did not specify a poke // Only install if the user did not specify a poke
InstallCommand installer = new InstallCommand(android); InstallCommand installer = new InstallCommand(android: android, ios: ios);
startedSomewhere = installer.install(); startedSomewhere = installer.install();
} }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// This test can take a while due to network requests
@Timeout(const Duration(seconds: 60))
import 'dart:io'; import 'dart:io';
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
...@@ -27,7 +29,7 @@ defineTests() { ...@@ -27,7 +29,7 @@ defineTests() {
test('init flutter-simple', () async { test('init flutter-simple', () async {
InitCommand command = new InitCommand(); InitCommand command = new InitCommand();
CommandRunner runner = new CommandRunner('test_flutter', '') CommandRunner runner = new CommandRunner('test_flutter', '')
..addCommand(command); ..addCommand(command);
await runner.run(['init', '--out', temp.path]) await runner.run(['init', '--out', temp.path])
.then((int code) => expect(code, equals(0))); .then((int code) => expect(code, equals(0)));
......
...@@ -6,7 +6,6 @@ library install_test; ...@@ -6,7 +6,6 @@ library install_test;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:sky_tools/src/application_package.dart';
import 'package:sky_tools/src/install.dart'; import 'package:sky_tools/src/install.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -17,19 +16,39 @@ main() => defineTests(); ...@@ -17,19 +16,39 @@ main() => defineTests();
defineTests() { defineTests() {
group('install', () { group('install', () {
test('returns 0 when Android is connected and ready for an install', () { test('returns 0 when Android is connected and ready for an install', () {
ApplicationPackageFactory.srcPath = './'; applicationPackageSetup();
ApplicationPackageFactory.setBuildPath(
BuildType.prebuilt, BuildPlatform.android, './');
MockAndroidDevice android = new MockAndroidDevice(); MockAndroidDevice android = new MockAndroidDevice();
when(android.isConnected()).thenReturn(true); when(android.isConnected()).thenReturn(true);
when(android.installApp(any)).thenReturn(true); when(android.installApp(any)).thenReturn(true);
InstallCommand command = new InstallCommand(android);
MockIOSDevice ios = new MockIOSDevice();
when(ios.isConnected()).thenReturn(false);
when(ios.installApp(any)).thenReturn(false);
InstallCommand command = new InstallCommand(android: android, ios: ios);
CommandRunner runner = new CommandRunner('test_flutter', '')
..addCommand(command);
runner.run(['install']).then((int code) => expect(code, equals(0)));
});
test('returns 0 when iOS is connected and ready for an install', () {
applicationPackageSetup();
MockAndroidDevice android = new MockAndroidDevice();
when(android.isConnected()).thenReturn(false);
when(android.installApp(any)).thenReturn(false);
MockIOSDevice ios = new MockIOSDevice();
when(ios.isConnected()).thenReturn(true);
when(ios.installApp(any)).thenReturn(true);
InstallCommand command = new InstallCommand(android: android, ios: ios);
CommandRunner runner = new CommandRunner('test_flutter', '') CommandRunner runner = new CommandRunner('test_flutter', '')
..addCommand(command); ..addCommand(command);
runner.run(['install']) runner.run(['install']).then((int code) => expect(code, equals(0)));
.then((int code) => expect(code, equals(0)));
}); });
}); });
} }
...@@ -16,12 +16,21 @@ main() => defineTests(); ...@@ -16,12 +16,21 @@ main() => defineTests();
defineTests() { defineTests() {
group('list', () { group('list', () {
test('returns 0 when called', () { test('returns 0 when called', () {
applicationPackageSetup();
MockAndroidDevice android = new MockAndroidDevice(); MockAndroidDevice android = new MockAndroidDevice();
// Avoid relying on adb being installed on the test system. // Avoid relying on adb being installed on the test system.
// Instead, cause the test to run the echo command. // Instead, cause the test to run the echo command.
when(android.adbPath).thenReturn('echo'); when(android.adbPath).thenReturn('echo');
ListCommand command = new ListCommand(android); MockIOSDevice ios = new MockIOSDevice();
// Avoid relying on idevice* being installed on the test system.
// Instead, cause the test to run the echo command.
when(ios.informerPath).thenReturn('echo');
when(ios.installerPath).thenReturn('echo');
when(ios.listerPath).thenReturn('echo');
ListCommand command = new ListCommand(android: android, ios: ios);
CommandRunner runner = new CommandRunner('test_flutter', '') CommandRunner runner = new CommandRunner('test_flutter', '')
..addCommand(command); ..addCommand(command);
runner.run(['list']).then((int code) => expect(code, equals(0))); runner.run(['list']).then((int code) => expect(code, equals(0)));
......
...@@ -6,7 +6,6 @@ library stop_test; ...@@ -6,7 +6,6 @@ library stop_test;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:sky_tools/src/application_package.dart';
import 'package:sky_tools/src/listen.dart'; import 'package:sky_tools/src/listen.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -17,9 +16,7 @@ main() => defineTests(); ...@@ -17,9 +16,7 @@ main() => defineTests();
defineTests() { defineTests() {
group('listen', () { group('listen', () {
test('returns 0 when no device is connected', () { test('returns 0 when no device is connected', () {
ApplicationPackageFactory.srcPath = './'; applicationPackageSetup();
ApplicationPackageFactory.setBuildPath(
BuildType.prebuilt, BuildPlatform.android, './');
MockAndroidDevice android = new MockAndroidDevice(); MockAndroidDevice android = new MockAndroidDevice();
when(android.isConnected()).thenReturn(false); when(android.isConnected()).thenReturn(false);
......
...@@ -6,7 +6,6 @@ library stop_test; ...@@ -6,7 +6,6 @@ library stop_test;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:sky_tools/src/application_package.dart';
import 'package:sky_tools/src/logs.dart'; import 'package:sky_tools/src/logs.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -17,9 +16,7 @@ main() => defineTests(); ...@@ -17,9 +16,7 @@ main() => defineTests();
defineTests() { defineTests() {
group('logs', () { group('logs', () {
test('returns 0 when no device is connected', () { test('returns 0 when no device is connected', () {
ApplicationPackageFactory.srcPath = './'; applicationPackageSetup();
ApplicationPackageFactory.setBuildPath(
BuildType.prebuilt, BuildPlatform.android, './');
MockAndroidDevice android = new MockAndroidDevice(); MockAndroidDevice android = new MockAndroidDevice();
when(android.isConnected()).thenReturn(false); when(android.isConnected()).thenReturn(false);
......
...@@ -3,9 +3,23 @@ ...@@ -3,9 +3,23 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:sky_tools/src/application_package.dart';
import 'package:sky_tools/src/device.dart'; import 'package:sky_tools/src/device.dart';
class MockAndroidDevice extends Mock implements AndroidDevice { class MockAndroidDevice extends Mock implements AndroidDevice {
@override @override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
} }
class MockIOSDevice extends Mock implements IOSDevice {
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void applicationPackageSetup() {
ApplicationPackageFactory.srcPath = './';
ApplicationPackageFactory.setBuildPath(
BuildType.prebuilt, BuildPlatform.android, './');
ApplicationPackageFactory.setBuildPath(
BuildType.prebuilt, BuildPlatform.iOS, './');
}
...@@ -6,7 +6,6 @@ library start_test; ...@@ -6,7 +6,6 @@ library start_test;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:sky_tools/src/application_package.dart';
import 'package:sky_tools/src/start.dart'; import 'package:sky_tools/src/start.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -17,15 +16,18 @@ main() => defineTests(); ...@@ -17,15 +16,18 @@ main() => defineTests();
defineTests() { defineTests() {
group('start', () { group('start', () {
test('returns 0 when Android is connected and ready to be started', () { test('returns 0 when Android is connected and ready to be started', () {
ApplicationPackageFactory.srcPath = './'; applicationPackageSetup();
ApplicationPackageFactory.setBuildPath(
BuildType.prebuilt, BuildPlatform.android, './');
MockAndroidDevice android = new MockAndroidDevice(); MockAndroidDevice android = new MockAndroidDevice();
when(android.isConnected()).thenReturn(true); when(android.isConnected()).thenReturn(true);
when(android.installApp(any)).thenReturn(true); when(android.installApp(any)).thenReturn(true);
when(android.stop(any)).thenReturn(true); when(android.stop(any)).thenReturn(true);
StartCommand command = new StartCommand(android);
MockIOSDevice ios = new MockIOSDevice();
when(ios.isConnected()).thenReturn(false);
when(ios.installApp(any)).thenReturn(false);
StartCommand command = new StartCommand(android: android, ios: ios);
CommandRunner runner = new CommandRunner('test_flutter', '') CommandRunner runner = new CommandRunner('test_flutter', '')
..addCommand(command); ..addCommand(command);
......
...@@ -6,7 +6,6 @@ library stop_test; ...@@ -6,7 +6,6 @@ library stop_test;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:sky_tools/src/application_package.dart';
import 'package:sky_tools/src/stop.dart'; import 'package:sky_tools/src/stop.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -17,9 +16,7 @@ main() => defineTests(); ...@@ -17,9 +16,7 @@ main() => defineTests();
defineTests() { defineTests() {
group('stop', () { group('stop', () {
test('returns 0 when Android is connected and ready to be stopped', () { test('returns 0 when Android is connected and ready to be stopped', () {
ApplicationPackageFactory.srcPath = './'; applicationPackageSetup();
ApplicationPackageFactory.setBuildPath(
BuildType.prebuilt, BuildPlatform.android, './');
MockAndroidDevice android = new MockAndroidDevice(); MockAndroidDevice android = new MockAndroidDevice();
when(android.isConnected()).thenReturn(true); when(android.isConnected()).thenReturn(true);
......
...@@ -6,7 +6,6 @@ library trace_test; ...@@ -6,7 +6,6 @@ library trace_test;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:sky_tools/src/application_package.dart';
import 'package:sky_tools/src/trace.dart'; import 'package:sky_tools/src/trace.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -17,9 +16,7 @@ main() => defineTests(); ...@@ -17,9 +16,7 @@ main() => defineTests();
defineTests() { defineTests() {
group('trace', () { group('trace', () {
test('returns 1 when no Android device is connected', () { test('returns 1 when no Android device is connected', () {
ApplicationPackageFactory.srcPath = './'; applicationPackageSetup();
ApplicationPackageFactory.setBuildPath(
BuildType.prebuilt, BuildPlatform.android, './');
MockAndroidDevice android = new MockAndroidDevice(); MockAndroidDevice android = new MockAndroidDevice();
when(android.isConnected()).thenReturn(false); when(android.isConnected()).thenReturn(false);
......
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