feat(mdtest run/auto): unlock devices if necessary and uninstall
testing apps after test execution. support 'screen-size' property
in test spec.
Wake up and unlock the device if it is locked. This is good for
integration testing, where the devices are locked after not being
used for a long time. Uninstall tested apps after test execution.
This feature is supported to avoid too many apps running in
background if a device is used to test multiple different apps.
Also add screen-size support in test spec.
Change-Id: Id0f9a209b2da15d944fc383442fc804789974118
diff --git a/mdtest/lib/src/commands/auto.dart b/mdtest/lib/src/commands/auto.dart
index 16adf00..a176229 100644
--- a/mdtest/lib/src/commands/auto.dart
+++ b/mdtest/lib/src/commands/auto.dart
@@ -4,10 +4,11 @@
import 'dart:async';
-import 'runner.dart';
+import 'helper.dart';
import '../mobile/device.dart';
import '../mobile/device_spec.dart';
import '../mobile/key_provider.dart';
+import '../mobile/android.dart';
import '../algorithms/coverage.dart';
import '../algorithms/matching.dart';
import '../globals.dart';
@@ -67,7 +68,8 @@
if (await runner.runAllApps(deviceMapping) != 0) {
printError('Error when running applications');
- errRounds.add(roundNum);
+ await uninstallTestedApps(deviceMapping);
+ errRounds.add(roundNum++);
continue;
}
@@ -75,9 +77,12 @@
if (await runner.runTest(_specs['test-path']) != 0) {
printError('Test execution exit with error.');
- errRounds.add(roundNum);
+ await uninstallTestedApps(deviceMapping);
+ errRounds.add(roundNum++);
continue;
}
+
+ await uninstallTestedApps(deviceMapping);
}
if (errRounds.isNotEmpty) {
diff --git a/mdtest/lib/src/commands/helper.dart b/mdtest/lib/src/commands/helper.dart
new file mode 100644
index 0000000..ee5bd5d
--- /dev/null
+++ b/mdtest/lib/src/commands/helper.dart
@@ -0,0 +1,99 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import '../mobile/device.dart';
+import '../mobile/device_spec.dart';
+import '../mobile/android.dart';
+import '../globals.dart';
+
+class MDTestRunner {
+ List<Process> appProcesses;
+
+ MDTestRunner() {
+ appProcesses = <Process>[];
+ }
+
+ /// Invoke runApp function for each device spec to device mapping in parallel
+ Future<int> runAllApps(Map<DeviceSpec, Device> deviceMapping) async {
+ List<Future<int>> runAppList = <Future<int>>[];
+ for (DeviceSpec deviceSpec in deviceMapping.keys) {
+ Device device = deviceMapping[deviceSpec];
+ runAppList.add(runApp(deviceSpec, device));
+ }
+ int res = 0;
+ List<int> results = await Future.wait(runAppList);
+ for (int result in results)
+ res += result;
+ return res == 0 ? 0 : 1;
+ }
+
+ /// Create a process that runs 'flutter run ...' command which installs and
+ /// starts the app on the device. The function finds a observatory port
+ /// through the process output. If no observatory port is found, then report
+ /// error.
+ Future<int> runApp(DeviceSpec deviceSpec, Device device) async {
+ if (await unlockDevice(device) != 0) {
+ printError('Device ${device.id} fails to wake up.');
+ return 1;
+ }
+
+ Process process = await Process.start(
+ 'flutter',
+ ['run', '-d', '${device.id}', '--target=${deviceSpec.appPath}'],
+ workingDirectory: deviceSpec.appRootPath
+ );
+ appProcesses.add(process);
+ Stream lineStream = process.stdout
+ .transform(new Utf8Decoder())
+ .transform(new LineSplitter());
+ RegExp portPattern = new RegExp(r'Observatory listening on (http.*)');
+ await for (var line in lineStream) {
+ print(line.toString().trim());
+ Match portMatch = portPattern.firstMatch(line.toString());
+ if (portMatch != null) {
+ deviceSpec.observatoryUrl = portMatch.group(1);
+ break;
+ }
+ }
+
+ process.stderr.drain();
+
+ if (deviceSpec.observatoryUrl == null) {
+ printError('No observatory url is found.');
+ return 1;
+ }
+
+ return 0;
+ }
+
+ /// Create a process and invoke 'dart testPath' to run the test script. After
+ /// test result is returned (either pass or fail), kill all app processes and
+ /// return the current process exit code
+ Future<int> runTest(String testPath) async {
+ Process process = await Process.start('dart', ['$testPath']);
+ RegExp testStopPattern = new RegExp(r'All tests passed|Some tests failed');
+ Stream stdoutStream = process.stdout
+ .transform(new Utf8Decoder())
+ .transform(new LineSplitter());
+ await for (var line in stdoutStream) {
+ print(line.toString().trim());
+ if (testStopPattern.hasMatch(line.toString()))
+ break;
+ }
+ killAppProcesses();
+ process.stderr.drain();
+ return await process.exitCode;
+ }
+
+ /// Kill all app processes
+ Future<Null> killAppProcesses() async {
+ for (Process process in appProcesses) {
+ process.kill();
+ }
+ }
+}
diff --git a/mdtest/lib/src/commands/run.dart b/mdtest/lib/src/commands/run.dart
index 6f698e4..edbdf7a 100644
--- a/mdtest/lib/src/commands/run.dart
+++ b/mdtest/lib/src/commands/run.dart
@@ -4,9 +4,10 @@
import 'dart:async';
-import 'runner.dart';
+import 'helper.dart';
import '../mobile/device.dart';
import '../mobile/device_spec.dart';
+import '../mobile/android.dart';
import '../algorithms/matching.dart';
import '../globals.dart';
import '../runner/mdtest_command.dart';
@@ -51,6 +52,7 @@
if (await runner.runAllApps(deviceMapping) != 0) {
printError('Error when running applications');
+ await uninstallTestedApps(deviceMapping);
return 1;
}
@@ -58,9 +60,12 @@
if (await runner.runTest(_specs['test-path']) != 0) {
printError('Test execution exit with error.');
+ await uninstallTestedApps(deviceMapping);
return 1;
}
+ await uninstallTestedApps(deviceMapping);
+
return 0;
}
diff --git a/mdtest/lib/src/mobile/android.dart b/mdtest/lib/src/mobile/android.dart
new file mode 100644
index 0000000..a0072d4
--- /dev/null
+++ b/mdtest/lib/src/mobile/android.dart
@@ -0,0 +1,176 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:math';
+import 'dart:io';
+
+import '../mobile/device.dart';
+import '../mobile/device_spec.dart';
+import '../globals.dart';
+import '../util.dart';
+
+const String lockProp = 'mHoldingWakeLockSuspendBlocker';
+
+/// Check if the device is locked
+Future<bool> _deviceIsLocked(Device device) async {
+ Process process = await Process.start(
+ 'adb',
+ ['-s', '${device.id}', 'shell', 'dumpsys', 'power']
+ );
+ bool isLocked;
+ RegExp lockStatusPattern = new RegExp(lockProp + r'=(.*)');
+ Stream lineStream = process.stdout
+ .transform(new Utf8Decoder())
+ .transform(new LineSplitter());
+ await for (var line in lineStream) {
+ Match lockMatcher = lockStatusPattern.firstMatch(line.toString());
+ if (lockMatcher != null) {
+ isLocked = lockMatcher.group(1) == 'false';
+ break;
+ }
+ }
+
+ process.stderr.drain();
+ await process.exitCode;
+
+ return isLocked;
+}
+
+/// Unlock devices if the device is locked
+Future<int> unlockDevice(Device device) async {
+
+ bool isLocked = await _deviceIsLocked(device);
+
+ if (isLocked == null) {
+ printError('adb error: cannot find device $lockProp property');
+ return 1;
+ }
+
+ if (!isLocked) return 0;
+
+ Process wakeUpAndUnlockProcess = await Process.start(
+ 'adb',
+ ['-s', '${device.id}', 'shell', 'input', 'keyevent', 'KEYCODE_MENU']
+ );
+ wakeUpAndUnlockProcess.stdout.drain();
+ wakeUpAndUnlockProcess.stderr.drain();
+
+ return await wakeUpAndUnlockProcess.exitCode;
+}
+
+/// Uninstall tested apps
+Future<int> uninstallTestedApps(Map<DeviceSpec, Device> deviceMapping) async {
+ int result = 0;
+
+ for (DeviceSpec spec in deviceMapping.keys) {
+ Device device = deviceMapping[spec];
+
+ String androidManifestPath
+ = normalizePath(spec.appRootPath, 'android/AndroidManifest.xml');
+ String androidManifest = await new File(androidManifestPath).readAsString();
+
+ RegExp packagePattern
+ = new RegExp(r'<manifest[\s\S]*?package="(\S+)"\s+[\s\S]*?>', multiLine: true);
+ Match packageMatcher = packagePattern.firstMatch(androidManifest);
+ if (packageMatcher == null) {
+ printError('Package name not found in $androidManifestPath');
+ return 1;
+ }
+ String packageName = packageMatcher.group(1);
+
+ Process uninstallProcess = await Process.start(
+ 'adb',
+ ['-s', '${device.id}', 'uninstall', '$packageName']
+ );
+
+ Stream lineStream = uninstallProcess.stdout
+ .transform(new Utf8Decoder())
+ .transform(new LineSplitter());
+ await for (var line in lineStream) {
+ print('Uninstall $packageName on device ${device.id}: ${line.toString().trim()}');
+ }
+
+ uninstallProcess.stderr.drain();
+ result += await uninstallProcess.exitCode;
+ }
+
+ if (result != 0) {
+ printError('Cannot uninstall testing apps from devices');
+ return 1;
+ }
+ return 0;
+}
+
+/// Get device property
+Future<String> getProperty(String deviceID, String propName) async {
+ ProcessResult results = await Process.run(
+ 'adb',
+ ['-s', deviceID, 'shell', 'getprop', propName]
+ );
+ return results.stdout.toString().trim();
+}
+
+/// Get device pixels and dpi to compute screen diagonal size in inches
+Future<String> getScreenSize(String deviceID) async {
+ Process sizeProcess = await Process.start(
+ 'adb',
+ ['-s', '$deviceID', 'shell', 'wm', 'size']
+ );
+ RegExp sizePattern = new RegExp(r'Physical size:\s*(\d+)x(\d+)');
+ Stream sizeLineStream = sizeProcess.stdout
+ .transform(new Utf8Decoder())
+ .transform(new LineSplitter());
+ int xSize;
+ int ySize;
+ await for (var line in sizeLineStream) {
+ Match sizeMatcher = sizePattern.firstMatch(line.toString());
+ if (sizeMatcher != null) {
+ xSize = int.parse(sizeMatcher.group(1));
+ ySize = int.parse(sizeMatcher.group(2));
+ break;
+ }
+ }
+
+ if (xSize == null || ySize == null) {
+ printError('Screen size not found.');
+ return null;
+ }
+
+ sizeProcess.stderr.drain();
+
+ Process densityProcess = await Process.start(
+ 'adb',
+ ['-s', '$deviceID', 'shell', 'wm', 'density']
+ );
+ RegExp densityPattern = new RegExp(r'Physical density:\s*(\d+)');
+ Stream densityLineStream = densityProcess.stdout
+ .transform(new Utf8Decoder())
+ .transform(new LineSplitter());
+ int density;
+ await for (var line in densityLineStream) {
+ Match densityMatcher = densityPattern.firstMatch(line.toString());
+ if (densityMatcher != null) {
+ density = int.parse(densityMatcher.group(1));
+ break;
+ }
+ }
+
+ if (density == null) {
+ printError('Density not found.');
+ return null;
+ }
+
+ densityProcess.stderr.drain();
+
+ double xInch = xSize / density;
+ double yInch = ySize / density;
+ double diagonalSize = sqrt(xInch * xInch + yInch * yInch);
+
+ if (diagonalSize < 3.5) return 'small';
+ if (diagonalSize < 5) return 'normal';
+ if (diagonalSize < 8) return 'large';
+ return 'xlarge';
+}
diff --git a/mdtest/lib/src/mobile/device.dart b/mdtest/lib/src/mobile/device.dart
index ca8b4ad..574114f 100644
--- a/mdtest/lib/src/mobile/device.dart
+++ b/mdtest/lib/src/mobile/device.dart
@@ -6,15 +6,18 @@
import 'dart:io';
import 'key_provider.dart';
+import 'android.dart';
class Device implements ClusterKeyProvider {
Device({
- this.id,
- this.modelName
+ this.properties
});
- final String id;
- final String modelName;
+ Map<String, String> properties;
+
+ String get id => properties['device-id'];
+ String get modelName => properties['model-name'];
+ String get screenSize => properties['screen-size'];
@override
String clusterKey() {
@@ -22,7 +25,8 @@
}
@override
- String toString() => '<id: $id, model-name: $modelName>';
+ String toString()
+ => '<id: $id, model-name: $modelName, screen-size: $screenSize>';
}
Future<List<Device>> getDevices() async {
@@ -45,16 +49,14 @@
bool startReading = false;
RegExp startPattern = new RegExp(r'List of devices attached');
RegExp deviceIDPattern = new RegExp(r'\s+(\w+)\s+.*');
- RegExp stopPatternWithDevices = new RegExp(r'\d+ connected devices?');
- RegExp stopPatternWithoutDevices = new RegExp(r'No devices detected');
+ RegExp stopPattern = new RegExp(r'\d+ connected devices?|No devices detected');
await for (var line in lineStream) {
if (!startReading && startPattern.hasMatch(line.toString())) {
startReading = true;
continue;
}
- if (stopPatternWithDevices.hasMatch(line.toString())
- || stopPatternWithoutDevices.hasMatch(line.toString()))
+ if (stopPattern.hasMatch(line.toString()))
break;
if (startReading) {
@@ -73,12 +75,10 @@
Future<Device> _collectDeviceProps(String deviceID) async {
return new Device(
- id: deviceID,
- modelName: await _getProperty(deviceID, 'ro.product.model')
+ properties: <String, String> {
+ 'device-id': deviceID,
+ 'model-name': await getProperty(deviceID, 'ro.product.model'),
+ 'screen-size': await getScreenSize(deviceID)
+ }
);
}
-
-Future<String> _getProperty(String deviceID, String propName) async {
- ProcessResult results = await Process.run('adb', ['-s', deviceID, 'shell', 'getprop', propName]);
- return results.stdout.toString().trim();
-}
diff --git a/mdtest/lib/src/mobile/device_spec.dart b/mdtest/lib/src/mobile/device_spec.dart
index 44543a8..042b055 100644
--- a/mdtest/lib/src/mobile/device_spec.dart
+++ b/mdtest/lib/src/mobile/device_spec.dart
@@ -6,42 +6,46 @@
import 'dart:convert';
import 'dart:io';
-import 'package:path/path.dart' as path;
-
import 'device.dart';
import 'key_provider.dart';
import '../globals.dart';
+import '../util.dart';
class DeviceSpec implements ClusterKeyProvider {
- DeviceSpec(
- {
- this.nickName,
- this.deviceID,
- this.deviceModelName,
- this.appRootPath,
- this.appPath,
- this.observatoryUrl
- }
- );
+ DeviceSpec(String nickname, { this.specProperties }) {
+ specProperties['nickname'] = nickname;
+ }
- final String nickName;
- final String deviceID;
- final String deviceModelName;
- final String appRootPath;
- final String appPath;
- String observatoryUrl;
+ Map<String, String> specProperties;
- // TODO(kaiyuanw): rewrite matches function later if necessary
+ String get nickName => specProperties['nickname'];
+ String get deviceID => specProperties['device-id'];
+ String get deviceModelName => specProperties['model-name'];
+ String get deviceScreenSize => specProperties['screen-size'];
+ String get appRootPath => specProperties['app-root'];
+ String get appPath => specProperties['app-path'];
+ String get observatoryUrl => specProperties['observatory-url'];
+ void set observatoryUrl(String url) {
+ specProperties['observatory-url'] = url;
+ }
+
+ /// Match if property names are not specified or equal to the device property.
+ /// Checked property names includes: device-id, model-name, screen-size
bool matches(Device device) {
- if(deviceID == device.id) {
- return deviceModelName == null ?
- true : deviceModelName == device.modelName;
- } else {
- return deviceID == null ?
- (deviceModelName == null ?
- true : deviceModelName == device.modelName)
- : false;
- }
+ List<String> checkedProperties = [
+ 'device-id',
+ 'model-name',
+ 'screen-size'
+ ];
+ return checkedProperties.every(
+ (String propertyName) => isNullOrEqual(propertyName, device)
+ );
+ }
+
+ bool isNullOrEqual(String propertyName, Device device) {
+ return specProperties[propertyName] == null
+ ||
+ specProperties[propertyName] == device.properties[propertyName];
}
@override
@@ -50,8 +54,11 @@
}
@override
- String toString() => '<nickname: $nickName, id: $deviceID, '
- 'model name: $deviceModelName, port: $observatoryUrl, '
+ String toString() => '<nickname: $nickName, '
+ 'id: $deviceID, '
+ 'model name: $deviceModelName, '
+ 'screen size: $deviceScreenSize, '
+ 'port: $observatoryUrl, '
'app path: $appPath>';
}
@@ -81,22 +88,15 @@
}
}
-String normalizePath(String rootPath, String relativePath) {
- return path.normalize(path.join(rootPath, relativePath));
-}
-
/// Build a list of device specs from mappings loaded from JSON .spec file
Future<List<DeviceSpec>> constructAllDeviceSpecs(dynamic allSpecs) async {
List<DeviceSpec> deviceSpecs = <DeviceSpec>[];
for(String name in allSpecs.keys) {
- Map<String, String> specs = allSpecs[name];
+ Map<String, String> spec = allSpecs[name];
deviceSpecs.add(
new DeviceSpec(
- nickName: name,
- deviceID: specs['device-id'],
- deviceModelName: specs['model-name'],
- appRootPath: specs['app-root'],
- appPath: specs['app-path']
+ name,
+ specProperties: spec
)
);
}
diff --git a/mdtest/lib/src/util.dart b/mdtest/lib/src/util.dart
index 8c53afe..7f85405 100644
--- a/mdtest/lib/src/util.dart
+++ b/mdtest/lib/src/util.dart
@@ -5,6 +5,8 @@
import 'dart:io' show Platform;
import 'dart:math';
+import 'package:path/path.dart' as path;
+
int minLength(List<String> elements) {
if (elements == null || elements.isEmpty) return -1;
return elements.map((String e) => e.length).reduce(min);
@@ -32,3 +34,7 @@
}
return minL;
}
+
+String normalizePath(String rootPath, String relativePath) {
+ return path.normalize(path.join(rootPath, relativePath));
+}