Sync test code with Cocoon (#6129)
* fix flutter watch benchmark Syncsd2d7950ecd
* Split Android/iOS impl behind a unified interface Syncsdb87e10fa5
* Switch from pub get to flutter packages get Syncsb378005cbb
* "silent" option in test runner; fix analysis errors;
This commit is contained in:
parent
8a823328ca
commit
6cdab85d28
@ -51,9 +51,11 @@ Future<Null> main(List<String> rawArgs) async {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool silent = args['silent'];
|
||||||
|
|
||||||
for (String taskName in taskNames) {
|
for (String taskName in taskNames) {
|
||||||
section('Running task "$taskName"');
|
section('Running task "$taskName"');
|
||||||
Map<String, dynamic> result = await runTask(taskName);
|
Map<String, dynamic> result = await runTask(taskName, silent: silent);
|
||||||
|
|
||||||
if (!result['success'])
|
if (!result['success'])
|
||||||
exitCode = 1;
|
exitCode = 1;
|
||||||
@ -98,4 +100,9 @@ final ArgParser _argParser = new ArgParser()
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
..addFlag(
|
||||||
|
'silent',
|
||||||
|
negatable: true,
|
||||||
|
defaultsTo: false,
|
||||||
);
|
);
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/adb.dart';
|
||||||
import 'package:flutter_devicelab/framework/framework.dart';
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
|
||||||
Future<Null> main() async {
|
Future<Null> main() async {
|
||||||
await task(createComplexLayoutStartupTest(ios: false));
|
await task(createComplexLayoutStartupTest(os: DeviceOperatingSystem.android));
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/adb.dart';
|
||||||
import 'package:flutter_devicelab/framework/framework.dart';
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
|
||||||
Future<Null> main() async {
|
Future<Null> main() async {
|
||||||
await task(createComplexLayoutStartupTest(ios: true));
|
await task(createComplexLayoutStartupTest(os: DeviceOperatingSystem.ios));
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/adb.dart';
|
||||||
import 'package:flutter_devicelab/framework/framework.dart';
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
|
||||||
Future<Null> main() async {
|
Future<Null> main() async {
|
||||||
await task(createComplexLayoutScrollPerfTest(ios: false));
|
await task(createComplexLayoutScrollPerfTest(os: DeviceOperatingSystem.android));
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/adb.dart';
|
||||||
import 'package:flutter_devicelab/framework/framework.dart';
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
|
||||||
Future<Null> main() async {
|
Future<Null> main() async {
|
||||||
await task(createComplexLayoutScrollPerfTest(ios: true));
|
await task(createComplexLayoutScrollPerfTest(os: DeviceOperatingSystem.ios));
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/adb.dart';
|
||||||
import 'package:flutter_devicelab/framework/framework.dart';
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
|
||||||
Future<Null> main() async {
|
Future<Null> main() async {
|
||||||
await task(createFlutterGalleryStartupTest(ios: false));
|
await task(createFlutterGalleryStartupTest(os: DeviceOperatingSystem.android));
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_devicelab/tasks/gallery.dart';
|
import 'package:flutter_devicelab/tasks/gallery.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/adb.dart';
|
||||||
import 'package:flutter_devicelab/framework/framework.dart';
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
|
||||||
Future<Null> main() async {
|
Future<Null> main() async {
|
||||||
await task(createGalleryTransitionTest(ios: false));
|
await task(createGalleryTransitionTest(os: DeviceOperatingSystem.android));
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/adb.dart';
|
||||||
import 'package:flutter_devicelab/framework/framework.dart';
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
|
||||||
Future<Null> main() async {
|
Future<Null> main() async {
|
||||||
await task(createFlutterGalleryStartupTest(ios: true));
|
await task(createFlutterGalleryStartupTest(os: DeviceOperatingSystem.ios));
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_devicelab/tasks/gallery.dart';
|
import 'package:flutter_devicelab/tasks/gallery.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/adb.dart';
|
||||||
import 'package:flutter_devicelab/framework/framework.dart';
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
|
||||||
Future<Null> main() async {
|
Future<Null> main() async {
|
||||||
await task(createGalleryTransitionTest(ios: true));
|
await task(createGalleryTransitionTest(os: DeviceOperatingSystem.ios));
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import 'package:flutter_devicelab/framework/utils.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
task(() async {
|
task(() async {
|
||||||
Adb device = await adb();
|
Device device = await devices.workingDevice;
|
||||||
await device.unlock();
|
await device.unlock();
|
||||||
Directory appDir =
|
Directory appDir =
|
||||||
dir(path.join(flutterDirectory.path, 'examples/flutter_gallery'));
|
dir(path.join(flutterDirectory.path, 'examples/flutter_gallery'));
|
||||||
|
@ -6,109 +6,135 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
|
|
||||||
typedef Future<Adb> AdbGetter();
|
/// The root of the API for controlling devices.
|
||||||
|
DeviceDiscovery get devices => new DeviceDiscovery();
|
||||||
|
|
||||||
/// Get an instance of [Adb].
|
/// Device operating system the test is configured to test.
|
||||||
///
|
enum DeviceOperatingSystem { android, ios }
|
||||||
/// See [realAdbGetter] for signature. This can be overwritten for testing.
|
|
||||||
AdbGetter adb = realAdbGetter;
|
|
||||||
|
|
||||||
Adb _currentDevice;
|
/// Device OS to test on.
|
||||||
|
DeviceOperatingSystem deviceOperatingSystem = DeviceOperatingSystem.android;
|
||||||
|
|
||||||
/// Picks a random Android device out of connected devices and sets it as
|
/// Discovers available devices and chooses one to work with.
|
||||||
/// [_currentDevice].
|
abstract class DeviceDiscovery {
|
||||||
Future<Null> pickNextDevice() async {
|
factory DeviceDiscovery() {
|
||||||
List<Adb> allDevices =
|
switch(deviceOperatingSystem) {
|
||||||
(await Adb.deviceIds).map((String id) => new Adb(deviceId: id)).toList();
|
case DeviceOperatingSystem.android:
|
||||||
|
return new AndroidDeviceDiscovery();
|
||||||
if (allDevices.length == 0) throw 'No Android devices detected';
|
case DeviceOperatingSystem.ios:
|
||||||
|
return new IosDeviceDiscovery();
|
||||||
// TODO(yjbanov): filter out and warn about those with low battery level
|
default:
|
||||||
_currentDevice = allDevices[new math.Random().nextInt(allDevices.length)];
|
throw new StateError('Unsupported device operating system: {config.deviceOperatingSystem}');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Adb> realAdbGetter() async {
|
|
||||||
if (_currentDevice == null) await pickNextDevice();
|
|
||||||
return _currentDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the ID of an unlocked device, unlocking it if necessary.
|
|
||||||
// TODO(yjbanov): abstract away iOS from Android.
|
|
||||||
Future<String> getUnlockedDeviceId({ bool ios: false }) async {
|
|
||||||
if (ios) {
|
|
||||||
// We currently do not have a way to lock/unlock iOS devices, or even to
|
|
||||||
// pick one out of many. So we pick the first random iPhone and assume it's
|
|
||||||
// already unlocked. For now we'll just keep them at minimum screen
|
|
||||||
// brightness so they don't drain battery too fast.
|
|
||||||
List<String> iosDeviceIds =
|
|
||||||
grep('UniqueDeviceID', from: await eval('ideviceinfo', <String>[]))
|
|
||||||
.map((String line) => line.split(' ').last)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (iosDeviceIds.isEmpty) throw 'No connected iOS devices found.';
|
|
||||||
|
|
||||||
return iosDeviceIds.first;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Adb device = await adb();
|
/// Selects a device to work with, load-balancing between devices if more than
|
||||||
await device.unlock();
|
/// one are available.
|
||||||
return device.deviceId;
|
///
|
||||||
|
/// Calling this method does not guarantee that the same device will be
|
||||||
|
/// returned. For such behavior see [workingDevice].
|
||||||
|
Future<Null> chooseWorkingDevice();
|
||||||
|
|
||||||
|
/// A device to work with.
|
||||||
|
///
|
||||||
|
/// Returns the same device when called repeatedly (unlike
|
||||||
|
/// [chooseWorkingDevice]). This is useful when you need to perform multiple
|
||||||
|
/// perations on one.
|
||||||
|
Future<Device> get workingDevice;
|
||||||
|
|
||||||
|
/// Lists all available devices' IDs.
|
||||||
|
Future<List<String>> discoverDevices();
|
||||||
|
|
||||||
|
/// Checks the health of the available devices.
|
||||||
|
Future<Map<String, HealthCheckResult>> checkDevices();
|
||||||
|
|
||||||
|
/// Prepares the system to run tasks.
|
||||||
|
Future<Null> performPreflightTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Android Debug Bridge (`adb`) client that exposes a subset of functions
|
/// A proxy for one specific device.
|
||||||
/// relevant to on-device testing.
|
abstract class Device {
|
||||||
class Adb {
|
/// A unique device identifier.
|
||||||
Adb({ this.deviceId });
|
String get deviceId;
|
||||||
|
|
||||||
final String deviceId;
|
/// Whether the device is awake.
|
||||||
|
Future<bool> isAwake();
|
||||||
|
|
||||||
|
/// Whether the device is asleep.
|
||||||
|
Future<bool> isAsleep();
|
||||||
|
|
||||||
|
/// Wake up the device if it is not awake.
|
||||||
|
Future<Null> wakeUp();
|
||||||
|
|
||||||
|
/// Send the device to sleep mode.
|
||||||
|
Future<Null> sendToSleep();
|
||||||
|
|
||||||
|
/// Emulates pressing the power button, toggling the device's on/off state.
|
||||||
|
Future<Null> togglePower();
|
||||||
|
|
||||||
|
/// Unlocks the device.
|
||||||
|
///
|
||||||
|
/// Assumes the device doesn't have a secure unlock pattern.
|
||||||
|
Future<Null> unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidDeviceDiscovery implements DeviceDiscovery {
|
||||||
// Parses information about a device. Example:
|
// Parses information about a device. Example:
|
||||||
//
|
//
|
||||||
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
|
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
|
||||||
static final RegExp _kDeviceRegex = new RegExp(r'^(\S+)\s+(\S+)(.*)');
|
static final RegExp _kDeviceRegex = new RegExp(r'^(\S+)\s+(\S+)(.*)');
|
||||||
|
|
||||||
/// Reports connection health for every device.
|
static AndroidDeviceDiscovery _instance;
|
||||||
static Future<Map<String, HealthCheckResult>> checkDevices() async {
|
|
||||||
Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
|
factory AndroidDeviceDiscovery() {
|
||||||
for (String deviceId in await deviceIds) {
|
return _instance ??= new AndroidDeviceDiscovery._();
|
||||||
try {
|
}
|
||||||
Adb device = new Adb(deviceId: deviceId);
|
|
||||||
// Just a smoke test that we can read wakefulness state
|
AndroidDeviceDiscovery._();
|
||||||
// TODO(yjbanov): also check battery level
|
|
||||||
await device._getWakefulness();
|
AndroidDevice _workingDevice;
|
||||||
results['android-device-$deviceId'] = new HealthCheckResult.success();
|
|
||||||
} catch (e, s) {
|
@override
|
||||||
results['android-device-$deviceId'] = new HealthCheckResult.error(e, s);
|
Future<AndroidDevice> get workingDevice async {
|
||||||
}
|
if (_workingDevice == null) {
|
||||||
|
await chooseWorkingDevice();
|
||||||
}
|
}
|
||||||
return results;
|
|
||||||
|
return _workingDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kills the `adb` server causing it to start a new instance upon next
|
/// Picks a random Android device out of connected devices and sets it as
|
||||||
/// command.
|
/// [workingDevice].
|
||||||
///
|
@override
|
||||||
/// Restarting `adb` helps with keeping device connections alive. When `adb`
|
Future<Null> chooseWorkingDevice() async {
|
||||||
/// runs non-stop for too long it loses connections to devices.
|
List<Device> allDevices = (await discoverDevices())
|
||||||
static Future<Null> restart() async {
|
.map((String id) => new AndroidDevice(deviceId: id))
|
||||||
await exec(adbPath, <String>['kill-server'], canFail: false);
|
.toList();
|
||||||
|
|
||||||
|
if (allDevices.isEmpty)
|
||||||
|
throw 'No Android devices detected';
|
||||||
|
|
||||||
|
// TODO(yjbanov): filter out and warn about those with low battery level
|
||||||
|
_workingDevice = allDevices[new math.Random().nextInt(allDevices.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List of device IDs visible to `adb`.
|
@override
|
||||||
static Future<List<String>> get deviceIds async {
|
Future<List<String>> discoverDevices() async {
|
||||||
List<String> output =
|
List<String> output = (await eval(adbPath, <String>['devices', '-l'], canFail: false))
|
||||||
(await eval(adbPath, <String>['devices', '-l'], canFail: false))
|
.trim().split('\n');
|
||||||
.trim()
|
|
||||||
.split('\n');
|
|
||||||
List<String> results = <String>[];
|
List<String> results = <String>[];
|
||||||
for (String line in output) {
|
for (String line in output) {
|
||||||
// Skip lines like: * daemon started successfully *
|
// Skip lines like: * daemon started successfully *
|
||||||
if (line.startsWith('* daemon ')) continue;
|
if (line.startsWith('* daemon '))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (line.startsWith('List of devices')) continue;
|
if (line.startsWith('List of devices'))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (_kDeviceRegex.hasMatch(line)) {
|
if (_kDeviceRegex.hasMatch(line)) {
|
||||||
Match match = _kDeviceRegex.firstMatch(line);
|
Match match = _kDeviceRegex.firstMatch(line);
|
||||||
@ -127,28 +153,70 @@ class Adb {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, HealthCheckResult>> checkDevices() async {
|
||||||
|
Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
|
||||||
|
for (String deviceId in await discoverDevices()) {
|
||||||
|
try {
|
||||||
|
AndroidDevice device = new AndroidDevice(deviceId: deviceId);
|
||||||
|
// Just a smoke test that we can read wakefulness state
|
||||||
|
// TODO(yjbanov): check battery level
|
||||||
|
await device._getWakefulness();
|
||||||
|
results['android-device-$deviceId'] = new HealthCheckResult.success();
|
||||||
|
} catch(e, s) {
|
||||||
|
results['android-device-$deviceId'] = new HealthCheckResult.error(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Null> performPreflightTasks() async {
|
||||||
|
// Kills the `adb` server causing it to start a new instance upon next
|
||||||
|
// command.
|
||||||
|
//
|
||||||
|
// Restarting `adb` helps with keeping device connections alive. When `adb`
|
||||||
|
// runs non-stop for too long it loses connections to devices. There may be
|
||||||
|
// a better method, but so far that's the best one I've found.
|
||||||
|
await exec(adbPath, <String>['kill-server'], canFail: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidDevice implements Device {
|
||||||
|
AndroidDevice({@required this.deviceId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String deviceId;
|
||||||
|
|
||||||
/// Whether the device is awake.
|
/// Whether the device is awake.
|
||||||
|
@override
|
||||||
Future<bool> isAwake() async {
|
Future<bool> isAwake() async {
|
||||||
return await _getWakefulness() == 'Awake';
|
return await _getWakefulness() == 'Awake';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the device is asleep.
|
/// Whether the device is asleep.
|
||||||
|
@override
|
||||||
Future<bool> isAsleep() async {
|
Future<bool> isAsleep() async {
|
||||||
return await _getWakefulness() == 'Asleep';
|
return await _getWakefulness() == 'Asleep';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wake up the device if it is not awake using [togglePower].
|
/// Wake up the device if it is not awake using [togglePower].
|
||||||
|
@override
|
||||||
Future<Null> wakeUp() async {
|
Future<Null> wakeUp() async {
|
||||||
if (!(await isAwake())) await togglePower();
|
if (!(await isAwake()))
|
||||||
|
await togglePower();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send the device to sleep mode if it is not asleep using [togglePower].
|
/// Send the device to sleep mode if it is not asleep using [togglePower].
|
||||||
|
@override
|
||||||
Future<Null> sendToSleep() async {
|
Future<Null> sendToSleep() async {
|
||||||
if (!(await isAsleep())) await togglePower();
|
if (!(await isAsleep()))
|
||||||
|
await togglePower();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends `KEYCODE_POWER` (26), which causes the device to toggle its mode
|
/// Sends `KEYCODE_POWER` (26), which causes the device to toggle its mode
|
||||||
/// between awake and asleep.
|
/// between awake and asleep.
|
||||||
|
@override
|
||||||
Future<Null> togglePower() async {
|
Future<Null> togglePower() async {
|
||||||
await shellExec('input', const <String>['keyevent', '26']);
|
await shellExec('input', const <String>['keyevent', '26']);
|
||||||
}
|
}
|
||||||
@ -156,6 +224,7 @@ class Adb {
|
|||||||
/// Unlocks the device by sending `KEYCODE_MENU` (82).
|
/// Unlocks the device by sending `KEYCODE_MENU` (82).
|
||||||
///
|
///
|
||||||
/// This only works when the device doesn't have a secure unlock pattern.
|
/// This only works when the device doesn't have a secure unlock pattern.
|
||||||
|
@override
|
||||||
Future<Null> unlock() async {
|
Future<Null> unlock() async {
|
||||||
await wakeUp();
|
await wakeUp();
|
||||||
await shellExec('input', const <String>['keyevent', '82']);
|
await shellExec('input', const <String>['keyevent', '82']);
|
||||||
@ -166,26 +235,117 @@ class Adb {
|
|||||||
/// See: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/PowerManagerInternal.java
|
/// See: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/PowerManagerInternal.java
|
||||||
Future<String> _getWakefulness() async {
|
Future<String> _getWakefulness() async {
|
||||||
String powerInfo = await shellEval('dumpsys', <String>['power']);
|
String powerInfo = await shellEval('dumpsys', <String>['power']);
|
||||||
String wakefulness =
|
String wakefulness = grep('mWakefulness=', from: powerInfo).single.split('=')[1].trim();
|
||||||
grep('mWakefulness=', from: powerInfo).single.split('=')[1].trim();
|
|
||||||
return wakefulness;
|
return wakefulness;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes [command] on `adb shell` and returns its exit code.
|
/// Executes [command] on `adb shell` and returns its exit code.
|
||||||
Future<Null> shellExec(String command, List<String> arguments,
|
Future<Null> shellExec(String command, List<String> arguments, {Map<String, String> env}) async {
|
||||||
{ Map<String, String> env }) async {
|
await exec(adbPath, <String>['shell', command]..addAll(arguments), env: env, canFail: false);
|
||||||
await exec(adbPath, <String>['shell', command]..addAll(arguments),
|
|
||||||
env: env, canFail: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes [command] on `adb shell` and returns its standard output as a [String].
|
/// Executes [command] on `adb shell` and returns its standard output as a [String].
|
||||||
Future<String> shellEval(String command, List<String> arguments,
|
Future<String> shellEval(String command, List<String> arguments, {Map<String, String> env}) {
|
||||||
{ Map<String, String> env }) {
|
return eval(adbPath, <String>['shell', command]..addAll(arguments), env: env, canFail: false);
|
||||||
return eval(adbPath, <String>['shell', command]..addAll(arguments),
|
|
||||||
env: env, canFail: false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class IosDeviceDiscovery implements DeviceDiscovery {
|
||||||
|
|
||||||
|
static IosDeviceDiscovery _instance;
|
||||||
|
|
||||||
|
factory IosDeviceDiscovery() {
|
||||||
|
return _instance ??= new IosDeviceDiscovery._();
|
||||||
|
}
|
||||||
|
|
||||||
|
IosDeviceDiscovery._();
|
||||||
|
|
||||||
|
IosDevice _workingDevice;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<IosDevice> get workingDevice async {
|
||||||
|
if (_workingDevice == null) {
|
||||||
|
await chooseWorkingDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _workingDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Picks a random iOS device out of connected devices and sets it as
|
||||||
|
/// [workingDevice].
|
||||||
|
@override
|
||||||
|
Future<Null> chooseWorkingDevice() async {
|
||||||
|
List<IosDevice> allDevices = (await discoverDevices())
|
||||||
|
.map((String id) => new IosDevice(deviceId: id))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (allDevices.length == 0)
|
||||||
|
throw 'No iOS devices detected';
|
||||||
|
|
||||||
|
// TODO(yjbanov): filter out and warn about those with low battery level
|
||||||
|
_workingDevice = allDevices[new math.Random().nextInt(allDevices.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> discoverDevices() async {
|
||||||
|
// TODO: use the -k UniqueDeviceID option, which requires much less parsing.
|
||||||
|
List<String> iosDeviceIds = grep('UniqueDeviceID', from: await eval('ideviceinfo', <String>[]))
|
||||||
|
.map((String line) => line.split(' ').last).toList();
|
||||||
|
|
||||||
|
if (iosDeviceIds.isEmpty)
|
||||||
|
throw 'No connected iOS devices found.';
|
||||||
|
|
||||||
|
return iosDeviceIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, HealthCheckResult>> checkDevices() async {
|
||||||
|
Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
|
||||||
|
for (String deviceId in await discoverDevices()) {
|
||||||
|
// TODO: do a more meaningful connectivity check than just recording the ID
|
||||||
|
results['ios-device-$deviceId'] = new HealthCheckResult.success();
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Null> performPreflightTasks() async {
|
||||||
|
// Currently we do not have preflight tasks for iOS.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iOS device.
|
||||||
|
class IosDevice implements Device {
|
||||||
|
const IosDevice({ @required this.deviceId });
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String deviceId;
|
||||||
|
|
||||||
|
// The methods below are stubs for now. They will need to be expanded.
|
||||||
|
// We currently do not have a way to lock/unlock iOS devices. So we assume the
|
||||||
|
// devices are already unlocked. For now we'll just keep them at minimum
|
||||||
|
// screen brightness so they don't drain battery too fast.
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isAwake() async => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isAsleep() async => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Null> wakeUp() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Null> sendToSleep() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Null> togglePower() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Null> unlock() async {}
|
||||||
|
}
|
||||||
|
|
||||||
/// Path to the `adb` executable.
|
/// Path to the `adb` executable.
|
||||||
String get adbPath {
|
String get adbPath {
|
||||||
String androidHome = Platform.environment['ANDROID_HOME'];
|
String androidHome = Platform.environment['ANDROID_HOME'];
|
||||||
|
@ -19,7 +19,10 @@ const Duration taskTimeoutWithGracePeriod = const Duration(minutes: 11);
|
|||||||
///
|
///
|
||||||
/// [taskName] is the name of the task. The corresponding task executable is
|
/// [taskName] is the name of the task. The corresponding task executable is
|
||||||
/// expected to be found under `bin/tasks`.
|
/// expected to be found under `bin/tasks`.
|
||||||
Future<Map<String, dynamic>> runTask(String taskName) async {
|
///
|
||||||
|
/// Running the task in [silent] mode will suppress standard output from task
|
||||||
|
/// processes and only print standard errors.
|
||||||
|
Future<Map<String, dynamic>> runTask(String taskName, { bool silent: false }) async {
|
||||||
String taskExecutable = 'bin/tasks/$taskName.dart';
|
String taskExecutable = 'bin/tasks/$taskName.dart';
|
||||||
|
|
||||||
if (!file(taskExecutable).existsSync())
|
if (!file(taskExecutable).existsSync())
|
||||||
@ -42,7 +45,9 @@ Future<Map<String, dynamic>> runTask(String taskName) async {
|
|||||||
.transform(new Utf8Decoder())
|
.transform(new Utf8Decoder())
|
||||||
.transform(new LineSplitter())
|
.transform(new LineSplitter())
|
||||||
.listen((String line) {
|
.listen((String line) {
|
||||||
stdout.writeln('[$taskName] [STDOUT] $line');
|
if (!silent) {
|
||||||
|
stdout.writeln('[$taskName] [STDOUT] $line');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
StreamSubscription<String> stderrSub = runner.stderr
|
StreamSubscription<String> stderrSub = runner.stderr
|
||||||
|
@ -242,12 +242,6 @@ String get dartBin =>
|
|||||||
|
|
||||||
Future<int> dart(List<String> args) => exec(dartBin, args);
|
Future<int> dart(List<String> args) => exec(dartBin, args);
|
||||||
|
|
||||||
Future<int> pub(String command) {
|
|
||||||
return exec(
|
|
||||||
path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'pub'),
|
|
||||||
<String>[command]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<dynamic> inDirectory(dynamic directory, Future<dynamic> action()) async {
|
Future<dynamic> inDirectory(dynamic directory, Future<dynamic> action()) async {
|
||||||
String previousCwd = cwd;
|
String previousCwd = cwd;
|
||||||
try {
|
try {
|
||||||
|
@ -105,8 +105,7 @@ class FlutterAnalyzeAppBenchmark extends Benchmark {
|
|||||||
Future<num> run() async {
|
Future<num> run() async {
|
||||||
rm(benchmarkFile);
|
rm(benchmarkFile);
|
||||||
await inDirectory(megaDir, () async {
|
await inDirectory(megaDir, () async {
|
||||||
await flutter('analyze', options: <String>[
|
await flutter('watch', options: <String>[
|
||||||
'--watch',
|
|
||||||
'--benchmark',
|
'--benchmark',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -12,23 +12,25 @@ import '../framework/adb.dart';
|
|||||||
import '../framework/framework.dart';
|
import '../framework/framework.dart';
|
||||||
import '../framework/utils.dart';
|
import '../framework/utils.dart';
|
||||||
|
|
||||||
TaskFunction createGalleryTransitionTest({ @required bool ios: false }) {
|
TaskFunction createGalleryTransitionTest({ @required DeviceOperatingSystem os }) {
|
||||||
return new GalleryTransitionTest(ios: ios);
|
return new GalleryTransitionTest(os: os);
|
||||||
}
|
}
|
||||||
|
|
||||||
class GalleryTransitionTest {
|
class GalleryTransitionTest {
|
||||||
GalleryTransitionTest({ this.ios });
|
GalleryTransitionTest({ this.os }) {
|
||||||
|
deviceOperatingSystem = os;
|
||||||
|
}
|
||||||
|
|
||||||
final bool ios;
|
final DeviceOperatingSystem os;
|
||||||
|
|
||||||
Future<TaskResult> call() async {
|
Future<TaskResult> call() async {
|
||||||
String deviceId = await getUnlockedDeviceId(ios: ios);
|
String deviceId = (await devices.workingDevice).deviceId;
|
||||||
Directory galleryDirectory =
|
Directory galleryDirectory =
|
||||||
dir('${flutterDirectory.path}/examples/flutter_gallery');
|
dir('${flutterDirectory.path}/examples/flutter_gallery');
|
||||||
await inDirectory(galleryDirectory, () async {
|
await inDirectory(galleryDirectory, () async {
|
||||||
await pub('get');
|
await flutter('packages', options: <String>['get']);
|
||||||
|
|
||||||
if (ios) {
|
if (os == DeviceOperatingSystem.ios) {
|
||||||
// This causes an Xcode project to be created.
|
// This causes an Xcode project to be created.
|
||||||
await flutter('build', options: <String>['ios', '--profile']);
|
await flutter('build', options: <String>['ios', '--profile']);
|
||||||
}
|
}
|
||||||
|
@ -11,26 +11,26 @@ import '../framework/adb.dart';
|
|||||||
import '../framework/framework.dart';
|
import '../framework/framework.dart';
|
||||||
import '../framework/utils.dart';
|
import '../framework/utils.dart';
|
||||||
|
|
||||||
TaskFunction createComplexLayoutScrollPerfTest({ @required bool ios: false }) {
|
TaskFunction createComplexLayoutScrollPerfTest({ @required DeviceOperatingSystem os }) {
|
||||||
return new PerfTest(
|
return new PerfTest(
|
||||||
'${flutterDirectory.path}/dev/benchmarks/complex_layout',
|
'${flutterDirectory.path}/dev/benchmarks/complex_layout',
|
||||||
'test_driver/scroll_perf.dart',
|
'test_driver/scroll_perf.dart',
|
||||||
'complex_layout_scroll_perf',
|
'complex_layout_scroll_perf',
|
||||||
ios: ios
|
os: os,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskFunction createFlutterGalleryStartupTest({ bool ios: false }) {
|
TaskFunction createFlutterGalleryStartupTest({ @required DeviceOperatingSystem os }) {
|
||||||
return new StartupTest(
|
return new StartupTest(
|
||||||
'${flutterDirectory.path}/examples/flutter_gallery',
|
'${flutterDirectory.path}/examples/flutter_gallery',
|
||||||
ios: ios
|
os: os,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskFunction createComplexLayoutStartupTest({ bool ios: false }) {
|
TaskFunction createComplexLayoutStartupTest({ @required DeviceOperatingSystem os }) {
|
||||||
return new StartupTest(
|
return new StartupTest(
|
||||||
'${flutterDirectory.path}/dev/benchmarks/complex_layout',
|
'${flutterDirectory.path}/dev/benchmarks/complex_layout',
|
||||||
ios: ios
|
os: os,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,17 +46,19 @@ TaskFunction createComplexLayoutBuildTest() {
|
|||||||
class StartupTest {
|
class StartupTest {
|
||||||
static const Duration _startupTimeout = const Duration(minutes: 2);
|
static const Duration _startupTimeout = const Duration(minutes: 2);
|
||||||
|
|
||||||
StartupTest(this.testDirectory, { this.ios });
|
StartupTest(this.testDirectory, { this.os }) {
|
||||||
|
deviceOperatingSystem = os;
|
||||||
|
}
|
||||||
|
|
||||||
final String testDirectory;
|
final String testDirectory;
|
||||||
final bool ios;
|
final DeviceOperatingSystem os;
|
||||||
|
|
||||||
Future<TaskResult> call() async {
|
Future<TaskResult> call() async {
|
||||||
return await inDirectory(testDirectory, () async {
|
return await inDirectory(testDirectory, () async {
|
||||||
String deviceId = await getUnlockedDeviceId(ios: ios);
|
String deviceId = (await devices.workingDevice).deviceId;
|
||||||
await pub('get');
|
await flutter('packages', options: <String>['get']);
|
||||||
|
|
||||||
if (ios) {
|
if (os == DeviceOperatingSystem.ios) {
|
||||||
// This causes an Xcode project to be created.
|
// This causes an Xcode project to be created.
|
||||||
await flutter('build', options: <String>['ios', '--profile']);
|
await flutter('build', options: <String>['ios', '--profile']);
|
||||||
}
|
}
|
||||||
@ -80,19 +82,19 @@ class StartupTest {
|
|||||||
/// performance.
|
/// performance.
|
||||||
class PerfTest {
|
class PerfTest {
|
||||||
|
|
||||||
PerfTest(this.testDirectory, this.testTarget, this.timelineFileName, { this.ios });
|
PerfTest(this.testDirectory, this.testTarget, this.timelineFileName, { this.os });
|
||||||
|
|
||||||
final String testDirectory;
|
final String testDirectory;
|
||||||
final String testTarget;
|
final String testTarget;
|
||||||
final String timelineFileName;
|
final String timelineFileName;
|
||||||
final bool ios;
|
final DeviceOperatingSystem os;
|
||||||
|
|
||||||
Future<TaskResult> call() {
|
Future<TaskResult> call() {
|
||||||
return inDirectory(testDirectory, () async {
|
return inDirectory(testDirectory, () async {
|
||||||
String deviceId = await getUnlockedDeviceId(ios: ios);
|
String deviceId = (await devices.workingDevice).deviceId;
|
||||||
await pub('get');
|
await flutter('packages', options: <String>['get']);
|
||||||
|
|
||||||
if (ios) {
|
if (os == DeviceOperatingSystem.ios) {
|
||||||
// This causes an Xcode project to be created.
|
// This causes an Xcode project to be created.
|
||||||
await flutter('build', options: <String>['ios', '--profile']);
|
await flutter('build', options: <String>['ios', '--profile']);
|
||||||
}
|
}
|
||||||
@ -124,9 +126,9 @@ class BuildTest {
|
|||||||
|
|
||||||
Future<TaskResult> call() async {
|
Future<TaskResult> call() async {
|
||||||
return await inDirectory(testDirectory, () async {
|
return await inDirectory(testDirectory, () async {
|
||||||
Adb device = await adb();
|
Device device = await devices.workingDevice;
|
||||||
await device.unlock();
|
await device.unlock();
|
||||||
await pub('get');
|
await flutter('packages', options: <String>['get']);
|
||||||
|
|
||||||
Stopwatch watch = new Stopwatch()..start();
|
Stopwatch watch = new Stopwatch()..start();
|
||||||
await flutter('build', options: <String>[
|
await flutter('build', options: <String>[
|
||||||
|
@ -25,7 +25,7 @@ class EditRefreshTask {
|
|||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
|
|
||||||
Future<TaskResult> call() async {
|
Future<TaskResult> call() async {
|
||||||
Adb device = await adb();
|
Device device = await devices.workingDevice;
|
||||||
await device.unlock();
|
await device.unlock();
|
||||||
Benchmark benchmark = new EditRefreshBenchmark(commit, timestamp);
|
Benchmark benchmark = new EditRefreshBenchmark(commit, timestamp);
|
||||||
section(benchmark.name);
|
section(benchmark.name);
|
||||||
@ -57,7 +57,7 @@ class EditRefreshBenchmark extends Benchmark {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<num> run() async {
|
Future<num> run() async {
|
||||||
Adb device = await adb();
|
Device device = await devices.workingDevice;
|
||||||
rm(benchmarkFile);
|
rm(benchmarkFile);
|
||||||
int exitCode = await inDirectory(megaDir, () async {
|
int exitCode = await inDirectory(megaDir, () async {
|
||||||
return await flutter('run',
|
return await flutter('run',
|
||||||
|
@ -24,7 +24,7 @@ TaskFunction createBasicMaterialAppSizeTest() {
|
|||||||
throw 'Failed to create sample Flutter app in ${sampleDir.path}';
|
throw 'Failed to create sample Flutter app in ${sampleDir.path}';
|
||||||
|
|
||||||
await inDirectory(sampleDir, () async {
|
await inDirectory(sampleDir, () async {
|
||||||
await pub('get');
|
await flutter('packages', options: <String>['get']);
|
||||||
await flutter('build', options: <String>['clean']);
|
await flutter('build', options: <String>['clean']);
|
||||||
await flutter('build', options: <String>['apk', '--release']);
|
await flutter('build', options: <String>['apk', '--release']);
|
||||||
apkSizeInBytes = await file('${sampleDir.path}/build/app.apk').length();
|
apkSizeInBytes = await file('${sampleDir.path}/build/app.apk').length();
|
||||||
|
@ -10,28 +10,27 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:flutter_devicelab/framework/adb.dart';
|
import 'package:flutter_devicelab/framework/adb.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('adb', () {
|
group('device', () {
|
||||||
Adb device;
|
Device device;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
FakeAdb.resetLog();
|
FakeDevice.resetLog();
|
||||||
adb = null;
|
device = null;
|
||||||
device = new FakeAdb();
|
device = new FakeDevice();
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
adb = realAdbGetter;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('isAwake/isAsleep', () {
|
group('isAwake/isAsleep', () {
|
||||||
test('reads Awake', () async {
|
test('reads Awake', () async {
|
||||||
FakeAdb.pretendAwake();
|
FakeDevice.pretendAwake();
|
||||||
expect(await device.isAwake(), isTrue);
|
expect(await device.isAwake(), isTrue);
|
||||||
expect(await device.isAsleep(), isFalse);
|
expect(await device.isAsleep(), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reads Asleep', () async {
|
test('reads Asleep', () async {
|
||||||
FakeAdb.pretendAsleep();
|
FakeDevice.pretendAsleep();
|
||||||
expect(await device.isAwake(), isFalse);
|
expect(await device.isAwake(), isFalse);
|
||||||
expect(await device.isAsleep(), isTrue);
|
expect(await device.isAsleep(), isTrue);
|
||||||
});
|
});
|
||||||
@ -48,7 +47,7 @@ void main() {
|
|||||||
|
|
||||||
group('wakeUp', () {
|
group('wakeUp', () {
|
||||||
test('when awake', () async {
|
test('when awake', () async {
|
||||||
FakeAdb.pretendAwake();
|
FakeDevice.pretendAwake();
|
||||||
await device.wakeUp();
|
await device.wakeUp();
|
||||||
expectLog(<CommandArgs>[
|
expectLog(<CommandArgs>[
|
||||||
cmd(command: 'dumpsys', arguments: <String>['power']),
|
cmd(command: 'dumpsys', arguments: <String>['power']),
|
||||||
@ -56,7 +55,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('when asleep', () async {
|
test('when asleep', () async {
|
||||||
FakeAdb.pretendAsleep();
|
FakeDevice.pretendAsleep();
|
||||||
await device.wakeUp();
|
await device.wakeUp();
|
||||||
expectLog(<CommandArgs>[
|
expectLog(<CommandArgs>[
|
||||||
cmd(command: 'dumpsys', arguments: <String>['power']),
|
cmd(command: 'dumpsys', arguments: <String>['power']),
|
||||||
@ -67,7 +66,7 @@ void main() {
|
|||||||
|
|
||||||
group('sendToSleep', () {
|
group('sendToSleep', () {
|
||||||
test('when asleep', () async {
|
test('when asleep', () async {
|
||||||
FakeAdb.pretendAsleep();
|
FakeDevice.pretendAsleep();
|
||||||
await device.sendToSleep();
|
await device.sendToSleep();
|
||||||
expectLog(<CommandArgs>[
|
expectLog(<CommandArgs>[
|
||||||
cmd(command: 'dumpsys', arguments: <String>['power']),
|
cmd(command: 'dumpsys', arguments: <String>['power']),
|
||||||
@ -75,7 +74,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('when awake', () async {
|
test('when awake', () async {
|
||||||
FakeAdb.pretendAwake();
|
FakeDevice.pretendAwake();
|
||||||
await device.sendToSleep();
|
await device.sendToSleep();
|
||||||
expectLog(<CommandArgs>[
|
expectLog(<CommandArgs>[
|
||||||
cmd(command: 'dumpsys', arguments: <String>['power']),
|
cmd(command: 'dumpsys', arguments: <String>['power']),
|
||||||
@ -86,7 +85,7 @@ void main() {
|
|||||||
|
|
||||||
group('unlock', () {
|
group('unlock', () {
|
||||||
test('sends unlock event', () async {
|
test('sends unlock event', () async {
|
||||||
FakeAdb.pretendAwake();
|
FakeDevice.pretendAwake();
|
||||||
await device.unlock();
|
await device.unlock();
|
||||||
expectLog(<CommandArgs>[
|
expectLog(<CommandArgs>[
|
||||||
cmd(command: 'dumpsys', arguments: <String>['power']),
|
cmd(command: 'dumpsys', arguments: <String>['power']),
|
||||||
@ -98,10 +97,10 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void expectLog(List<CommandArgs> log) {
|
void expectLog(List<CommandArgs> log) {
|
||||||
expect(FakeAdb.commandLog, log);
|
expect(FakeDevice.commandLog, log);
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandArgs cmd({ String command, List<String> arguments, Map<String, String> env }) => new CommandArgs(
|
CommandArgs cmd({String command, List<String> arguments, Map<String, String> env}) => new CommandArgs(
|
||||||
command: command,
|
command: command,
|
||||||
arguments: arguments,
|
arguments: arguments,
|
||||||
env: env
|
env: env
|
||||||
@ -110,7 +109,7 @@ CommandArgs cmd({ String command, List<String> arguments, Map<String, String> en
|
|||||||
typedef dynamic ExitErrorFactory();
|
typedef dynamic ExitErrorFactory();
|
||||||
|
|
||||||
class CommandArgs {
|
class CommandArgs {
|
||||||
CommandArgs({ this.command, this.arguments, this.env });
|
CommandArgs({this.command, this.arguments, this.env});
|
||||||
|
|
||||||
final String command;
|
final String command;
|
||||||
final List<String> arguments;
|
final List<String> arguments;
|
||||||
@ -142,8 +141,8 @@ class CommandArgs {
|
|||||||
: null.hashCode;
|
: null.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeAdb extends Adb {
|
class FakeDevice extends AndroidDevice {
|
||||||
FakeAdb({ String deviceId: null }) : super(deviceId: deviceId);
|
FakeDevice({String deviceId: null}) : super(deviceId: deviceId);
|
||||||
|
|
||||||
static String output = '';
|
static String output = '';
|
||||||
static ExitErrorFactory exitErrorFactory = () => null;
|
static ExitErrorFactory exitErrorFactory = () => null;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user