Compile against the devtools annotations to fix docs build. [DO NOT MERGE] am: 83533c5f87 am: 5e5b10e86e
am: 508d76f134 -s ours
* commit '508d76f134853cb82e4eb2dc7e86d657ed4a9b7e':
Compile against the devtools annotations to fix docs build. [DO NOT MERGE]
diff --git a/.classpath b/.classpath
index cea4eac..3a2c618 100644
--- a/.classpath
+++ b/.classpath
@@ -4,10 +4,8 @@
<classpathentry kind="src" path="res"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/kxml2/kxml2-2.3.0.jar"/>
- <classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/guavalib_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/guava/guava/src"/>
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/jline-1.0_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/jline/src"/>
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/junit_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/junit/src"/>
- <classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
<classpathentry combineaccessrules="false" exported="true" kind="src" path="/tf-remote-client"/>
<classpathentry kind="output" path="bin"/>
diff --git a/Android.mk b/Android.mk
index 87626a6..085eae3 100644
--- a/Android.mk
+++ b/Android.mk
@@ -29,7 +29,7 @@
LOCAL_STATIC_JAVA_LIBRARIES := junit kxml2-2.3.0 jline-1.0 tf-remote-client
# emmalib is only a runtime dependency if generating code coverage reporters,
# not a compile time dependency
-LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt emmalib tools-common-prebuilt devtools-annotations-prebuilt
+LOCAL_JAVA_LIBRARIES := emmalib tools-common-prebuilt
include $(BUILD_HOST_JAVA_LIBRARY)
@@ -58,6 +58,8 @@
-hdf android.whichdoc online \
-hdf sac true \
-hdf devices true \
+ -showAnnotation com.android.tradefed.config.OptionClass \
+ -showAnnotation com.android.tradefed.config.Option \
include $(BUILD_DROIDDOC)
@@ -68,7 +70,7 @@
# Note that this is incompatible with `make dist`. If you want to make
# the distribution, you must run `tapas` with the individual target names.
.PHONY: tradefed-all
-tradefed-all: tradefed tradefed-tests tf-prod-tests tf-prod-metatests
+tradefed-all: tradefed tradefed-tests tf-prod-tests tf-prod-metatests tradefed_win script_help verify
# ====================================
include $(CLEAR_VARS)
@@ -76,20 +78,20 @@
LOCAL_MODULE_TAGS := optional
-LOCAL_PREBUILT_EXECUTABLES := tradefed.sh
+LOCAL_PREBUILT_EXECUTABLES := tradefed.sh tradefed_win.bat script_help.sh verify.sh
include $(BUILD_HOST_PREBUILT)
# Build all sub-directories
include $(call all-makefiles-under,$(LOCAL_PATH))
########################################################
-# Zip up the built files and dist it as google-tradefed.zip
-ifneq (,$(filter tradefed, $(TARGET_BUILD_APPS)))
+# Zip up the built files and dist it as tradefed.zip
+ifneq (,$(filter tradefed tradefed-all, $(TARGET_BUILD_APPS)))
-tradefed_dist_host_jars := tradefed tradefed-tests ddmlib-prebuilt tf-prod-tests emmalib loganalysis loganalysis-tests tf-remote-client devtools-annotations-prebuilt
+tradefed_dist_host_jars := tradefed tradefed-tests tf-prod-tests emmalib loganalysis loganalysis-tests tf-remote-client
tradefed_dist_host_jar_files := $(foreach m, $(tradefed_dist_host_jars), $(HOST_OUT_JAVA_LIBRARIES)/$(m).jar)
-tradefed_dist_host_exes := tradefed.sh
+tradefed_dist_host_exes := tradefed.sh tradefed_win.bat script_help.sh verify.sh
tradefed_dist_host_exe_files := $(foreach m, $(tradefed_dist_host_exes), $(BUILD_OUT_EXECUTABLES)/$(m))
tradefed_dist_test_apks := TradeFedUiTestApp TradeFedTestApp DeviceSetupUtil
diff --git a/prod-tests/Android.mk b/prod-tests/Android.mk
index 01b4f5a..5432a31 100644
--- a/prod-tests/Android.mk
+++ b/prod-tests/Android.mk
@@ -26,7 +26,7 @@
LOCAL_MODULE := tf-prod-tests
LOCAL_MODULE_TAGS := optional
-LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt tradefed loganalysis
+LOCAL_JAVA_LIBRARIES := tradefed loganalysis
include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/prod-tests/res/config/template/local.xml b/prod-tests/res/config/template/local.xml
new file mode 100644
index 0000000..3ec5873
--- /dev/null
+++ b/prod-tests/res/config/template/local.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Common base configuration for local runs. -->
+<configuration description="Common base configuration for local runs">
+ <option name="bugreport-on-invocation-ended" value="true" />
+ <build_provider class="com.android.tradefed.build.BootstrapBuildProvider" />
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="screen-always-on" value="on" />
+ <option name="screen-adaptive-brightness" value="off" />
+ <option name="screen-brightness" value="30" />
+ </target_preparer>
+
+ <template-include name="test" />
+
+ <logger class="com.android.tradefed.log.FileLogger">
+ <option name="log-level" value="VERBOSE" />
+ <option name="log-level-display" value="VERBOSE" />
+ </logger>
+ <log_saver class="com.android.tradefed.result.FileSystemLogSaver" />
+ <result_reporter class="com.android.tradefed.result.ConsoleResultReporter" />
+ <result_reporter class="com.android.tradefed.result.InvocationFailureEmailResultReporter" />
+ <result_reporter class="com.android.tradefed.result.DeviceUnavailEmailResultReporter" />
+ <template-include name="reporters" default="google/template/reporters/empty" />
+</configuration>
diff --git a/prod-tests/src/com/android/app/tests/AppLaunchTest.java b/prod-tests/src/com/android/app/tests/AppLaunchTest.java
index b6abd38..acf3816 100644
--- a/prod-tests/src/com/android/app/tests/AppLaunchTest.java
+++ b/prod-tests/src/com/android/app/tests/AppLaunchTest.java
@@ -15,7 +15,6 @@
*/
package com.android.app.tests;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.build.IAppBuildInfo;
import com.android.tradefed.build.IBuildInfo;
@@ -120,7 +119,7 @@
listener.testStarted(installTest);
String result = getDevice().installPackage(apkFile, true);
if (result != null) {
- listener.testFailed(TestFailure.FAILURE, installTest, result);
+ listener.testFailed(installTest, result);
}
listener.testEnded(installTest, Collections.<String, String> emptyMap());
}
diff --git a/prod-tests/src/com/android/bluetooth/tests/BluetoothStressTest.java b/prod-tests/src/com/android/bluetooth/tests/BluetoothStressTest.java
index f39b3a0..3cd1d81 100644
--- a/prod-tests/src/com/android/bluetooth/tests/BluetoothStressTest.java
+++ b/prod-tests/src/com/android/bluetooth/tests/BluetoothStressTest.java
@@ -597,7 +597,7 @@
newConf.append(line).append("\n");
}
}
- mTestDevice.executeAdbCommand("remount");
+ mTestDevice.remountSystemWritable();
return mTestDevice.pushString(newConf.toString(), BTSNOOP_CONF_FILE);
} catch (IOException e) {
return false;
diff --git a/prod-tests/src/com/android/continuous/SmokeTestFailureReporter.java b/prod-tests/src/com/android/continuous/SmokeTestFailureReporter.java
index fad434c..e0e7785 100644
--- a/prod-tests/src/com/android/continuous/SmokeTestFailureReporter.java
+++ b/prod-tests/src/com/android/continuous/SmokeTestFailureReporter.java
@@ -17,12 +17,12 @@
package com.android.continuous;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.result.TestFailureEmailResultReporter;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestResult.TestStatus;
-import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.util.Email;
import com.android.tradefed.util.IEmail;
@@ -98,14 +98,16 @@
private String describeStatus(TestStatus status) {
switch (status) {
- case ERROR:
- return "had an error";
case FAILURE:
return "failed";
case PASSED:
return "passed";
case INCOMPLETE:
return "did not complete";
+ case ASSUMPTION_FAILURE:
+ return "assumption failed";
+ case IGNORED:
+ return "ignored";
}
return "had an unknown result";
}
diff --git a/prod-tests/src/com/android/encryption/tests/EncryptionFunctionalityTest.java b/prod-tests/src/com/android/encryption/tests/EncryptionFunctionalityTest.java
index 2016982..9e38110 100644
--- a/prod-tests/src/com/android/encryption/tests/EncryptionFunctionalityTest.java
+++ b/prod-tests/src/com/android/encryption/tests/EncryptionFunctionalityTest.java
@@ -42,7 +42,7 @@
*/
public class EncryptionFunctionalityTest implements IDeviceTest, IRemoteTest {
- private static final int BOOT_TIMEOUT = 120 * 1000;
+ private static final int BOOT_TIMEOUT = 2 * 60 * 1000;
ITestDevice mTestDevice = null;
@@ -93,7 +93,8 @@
mTestDevice.waitForDeviceAvailable();
stageEnd(false); // stage 6
mTestDevice.executeShellCommand("vdc cryptfs changepw default");
- mTestDevice.reboot();
+ mTestDevice.nonBlockingReboot();
+ mTestDevice.waitForBootComplete(BOOT_TIMEOUT * 3);
stageEnd(false); // stage 7
}
} catch (DeviceNotAvailableException e) {
diff --git a/prod-tests/src/com/android/framework/tests/BandwidthMicroBenchMarkTest.java b/prod-tests/src/com/android/framework/tests/BandwidthMicroBenchMarkTest.java
index cad496e..458e4d8 100644
--- a/prod-tests/src/com/android/framework/tests/BandwidthMicroBenchMarkTest.java
+++ b/prod-tests/src/com/android/framework/tests/BandwidthMicroBenchMarkTest.java
@@ -18,6 +18,7 @@
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
import com.android.framework.tests.BandwidthStats.CompareResult;
import com.android.framework.tests.BandwidthStats.ComparisonRecord;
import com.android.tradefed.config.Option;
@@ -29,7 +30,6 @@
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.IRunUtil.IRunnableResult;
diff --git a/prod-tests/src/com/android/framework/tests/DataIdleTest.java b/prod-tests/src/com/android/framework/tests/DataIdleTest.java
index 12fed10..56d1c03 100644
--- a/prod-tests/src/com/android/framework/tests/DataIdleTest.java
+++ b/prod-tests/src/com/android/framework/tests/DataIdleTest.java
@@ -18,6 +18,7 @@
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
@@ -27,7 +28,6 @@
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.RunUtil;
diff --git a/prod-tests/src/com/android/framework/tests/DownloadManagerHostTests.java b/prod-tests/src/com/android/framework/tests/DownloadManagerHostTests.java
index dff0370..fbea647 100644
--- a/prod-tests/src/com/android/framework/tests/DownloadManagerHostTests.java
+++ b/prod-tests/src/com/android/framework/tests/DownloadManagerHostTests.java
@@ -223,7 +223,7 @@
* Take dumpsys wifi when test fails.
*/
@Override
- public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ public void testFailed(TestIdentifier test, String trace) {
try {
String output = mDevice.executeShellCommand("dumpsys wifi");
if (output == null) {
@@ -239,7 +239,7 @@
CLog.e("Error getting dumpsys wifi");
CLog.e(e);
} finally {
- super.testFailed(status, test, trace);
+ super.testFailed(test, trace);
}
}
}
diff --git a/prod-tests/src/com/android/framework/tests/FrameworkPerfTest.java b/prod-tests/src/com/android/framework/tests/FrameworkPerfTest.java
index d0cb486..f80aea2 100644
--- a/prod-tests/src/com/android/framework/tests/FrameworkPerfTest.java
+++ b/prod-tests/src/com/android/framework/tests/FrameworkPerfTest.java
@@ -18,12 +18,12 @@
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.TestResult;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.IRunUtil;
diff --git a/prod-tests/src/com/android/framework/tests/FrameworkStressTest.java b/prod-tests/src/com/android/framework/tests/FrameworkStressTest.java
index d39b255..7df7657 100644
--- a/prod-tests/src/com/android/framework/tests/FrameworkStressTest.java
+++ b/prod-tests/src/com/android/framework/tests/FrameworkStressTest.java
@@ -18,6 +18,7 @@
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
import com.android.loganalysis.item.BugreportItem;
import com.android.loganalysis.item.LogcatItem;
import com.android.loganalysis.parser.BugreportParser;
@@ -29,7 +30,6 @@
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
diff --git a/prod-tests/src/com/android/framework/tests/PackageManagerHostTestUtils.java b/prod-tests/src/com/android/framework/tests/PackageManagerHostTestUtils.java
index 37dbfeb..7bd29bb 100644
--- a/prod-tests/src/com/android/framework/tests/PackageManagerHostTestUtils.java
+++ b/prod-tests/src/com/android/framework/tests/PackageManagerHostTestUtils.java
@@ -473,24 +473,7 @@
*/
public void determinePrivateAppPath(File apkFile, String pkgName)
throws DeviceNotAvailableException {
- String result = installFileForwardLocked(apkFile, true);
- assertEquals(null, result);
- waitForPackageManager();
-
- // grep for package to make sure it is installed
- assertTrue(doesPackageExist(pkgName));
-
- // Determine path of secret path.
- result = mDevice.executeShellCommand("pm path " + pkgName);
- if (result.indexOf(PRE_JB_APP_PRIVATE_PATH) != -1) {
- setAppPrivatePath(PRE_JB_APP_PRIVATE_PATH);
- } else if (result.indexOf(JB_APP_PRIVATE_PATH) != -1) {
- setAppPrivatePath(JB_APP_PRIVATE_PATH);
- } else {
- Assert.fail("Failed to locate private app path on device.");
- }
- CLog.d("Device private app path is: %s", getAppPrivatePath());
- uninstallApp(pkgName);
+ setAppPrivatePath(JB_APP_PRIVATE_PATH);
}
/**
diff --git a/prod-tests/src/com/android/framework/tests/PackageManagerHostTests.java b/prod-tests/src/com/android/framework/tests/PackageManagerHostTests.java
index e5aa08f..552df68 100644
--- a/prod-tests/src/com/android/framework/tests/PackageManagerHostTests.java
+++ b/prod-tests/src/com/android/framework/tests/PackageManagerHostTests.java
@@ -938,7 +938,7 @@
return;
}
mPMHostUtils.installFile(getTestAppFilePath(SHARED_UID_APK_64), true);
- assertEquals(ARMEABI_V7A, mPMHostUtils.getAbi(SHARED_UID_PKG_64));
+ assertEquals(ARM64_V8A, mPMHostUtils.getAbi(SHARED_UID_PKG_64));
} finally {
mPMHostUtils.uninstallApp(SHARED_UID_PKG_64);
}
diff --git a/prod-tests/src/com/android/framework/tests/PackageManagerOTATestUtils.java b/prod-tests/src/com/android/framework/tests/PackageManagerOTATestUtils.java
index 07a0e3f..6cae21c 100644
--- a/prod-tests/src/com/android/framework/tests/PackageManagerOTATestUtils.java
+++ b/prod-tests/src/com/android/framework/tests/PackageManagerOTATestUtils.java
@@ -80,8 +80,7 @@
*/
public void removeSystemApp(String systemApp, boolean reboot)
throws DeviceNotAvailableException {
- remountSystemRW();
- mDevice.waitForDeviceAvailable();
+ mDevice.remountSystemWritable();
String cmd = String.format("rm %s", systemApp);
mDevice.executeShellCommand(cmd);
if (reboot) {
@@ -163,8 +162,12 @@
CLog.d("Failed to find node for xpath %s", xPathString);
return false;
}
- CLog.d("Value of node %s: %s", xPathString, n.getNodeValue());
- return n.getNodeValue().equalsIgnoreCase(value);
+ boolean result = n.getNodeValue().equalsIgnoreCase(value);
+ if (!result) {
+ CLog.v("Value of node %s: \"%s\", expected: \"%s\"",
+ xPathString, n.getNodeValue(), value);
+ }
+ return result;
}
/**
@@ -245,15 +248,6 @@
}
/**
- * Helper method to remount system partition.
- *
- * @throws DeviceNotAvailableException
- */
- public void remountSystemRW() throws DeviceNotAvailableException {
- mDevice.executeAdbCommand("remount");
- }
-
- /**
* Helper method to stop system shell.
*
* @throws DeviceNotAvailableException
@@ -294,7 +288,7 @@
*/
public void pushSystemApp(final File localFile, final String deviceFilePath)
throws DeviceNotAvailableException {
- remountSystemRW();
+ mDevice.remountSystemWritable();
stopSystem();
mDevice.pushFile(localFile, deviceFilePath);
startSystem();
diff --git a/prod-tests/src/com/android/framework/tests/PackageManagerOTATests.java b/prod-tests/src/com/android/framework/tests/PackageManagerOTATests.java
index 9204444..cb45803 100644
--- a/prod-tests/src/com/android/framework/tests/PackageManagerOTATests.java
+++ b/prod-tests/src/com/android/framework/tests/PackageManagerOTATests.java
@@ -363,13 +363,15 @@
}
/**
- * Test when update has the same version.
+ * Test when updated system app has the same version. Package manager is expected to use
+ * the newly installed upgrade.
* <p/>
* Assumes adb is running as root in device under test.
*
* @throws DeviceNotAvailableException
*/
- public void testSystemAppUpdatedSameVersion() throws DeviceNotAvailableException {
+ public void testSystemAppUpdatedSameVersion_PreferUpdatedApk()
+ throws DeviceNotAvailableException {
mUtils.pushSystemApp(getTestAppFilePath(VERSION_2_APK), mSystemAppPath);
mPackageXml = mUtils.pullPackagesXML();
assertTrue("The package should be installed",
@@ -386,7 +388,7 @@
mUtils.installFile(getTestAppFilePath(VERSION_2_APK), true);
mPackageXml = mUtils.pullPackagesXML();
- assertFalse("After system app upgrade, the path should be the upgraded app on /data",
+ assertFalse("After system app upgrade, the path should be the upgraded app in /data",
mUtils.expectEquals(mPackageXml, CODE_PATH_XPATH, mSystemAppPath));
assertTrue("Package version should be 2",
mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
@@ -400,63 +402,10 @@
mUtils.restartSystem();
mPackageXml = mUtils.pullPackagesXML();
- assertTrue("After reboot, the path should be the be installed",
+ assertFalse("After reboot, the path should be the upgraded app in /data",
mUtils.expectEquals(mPackageXml, CODE_PATH_XPATH, mSystemAppPath));
assertTrue("Package version should be 2",
mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
- assertFalse("Updated-package should NOT be present",
- mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
- assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
- assertTrue("VIBRATE permission should be granted",
- mUtils.packageHasPermission(PACKAGE_NAME, VIBRATE_PERMISSION));
- assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
- mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
-
- mUtils.restartSystem();
- mPackageXml = mUtils.pullPackagesXML();
- assertTrue("After reboot, the path should be the be installed",
- mUtils.expectEquals(mPackageXml, CODE_PATH_XPATH, mSystemAppPath));
- assertTrue("Package version should be 2",
- mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
- assertFalse("Updated-package should NOT be present",
- mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
- assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
- assertTrue("VIBRATE permission should be granted",
- mUtils.packageHasPermission(PACKAGE_NAME, VIBRATE_PERMISSION));
- assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
- mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
- }
-
- /**
- * Test when update has a different filename.
- * <p/>
- * Assumes adb is running as root in device under test.
- *
- * @throws DeviceNotAvailableException
- */
- public void testUpdatedSystemAppChangeFileName() throws DeviceNotAvailableException {
- mUtils.pushSystemApp(getTestAppFilePath(VERSION_1_APK), mSystemAppPath);
- mPackageXml = mUtils.pullPackagesXML();
- assertNotNull("Failed to pull packages xml file from device", mPackageXml);
- assertTrue("After system app push, the package should be installed",
- mUtils.expectExists(mPackageXml, PACKAGE_XPATH));
- assertTrue("Package version should be 1",
- mUtils.expectEquals(mPackageXml, VERSION_XPATH, "1"));
- assertFalse("Updated-package should not be present",
- mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
- assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
- assertTrue("VIBRATE permission should be granted",
- mUtils.packageHasPermission(PACKAGE_NAME, VIBRATE_PERMISSION));
- assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
- mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
-
- mUtils.installFile(getTestAppFilePath(VERSION_2_APK), true);
- mPackageXml = mUtils.pullPackagesXML();
- assertTrue("After system app upgrade, the path should be the upgraded app on /data",
- mUtils.expectStartsWith(mPackageXml, CODE_PATH_XPATH,
- DATA_APP_DIRECTORY + PACKAGE_NAME));
- assertTrue("Package version should be 2",
- mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
assertTrue("Updated-package should be present",
mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
@@ -465,21 +414,6 @@
assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
- mUtils.removeSystemApp(mSystemAppPath, false);
- mUtils.pushSystemApp(getTestAppFilePath(VERSION_2_APK), mDiffSystemAppPath);
-
- mPackageXml = mUtils.pullPackagesXML();
- assertTrue("After reboot, the system path should be correct",
- mUtils.expectEquals(mPackageXml, CODE_PATH_XPATH, mDiffSystemAppPath));
- assertTrue("Package version should be 2",
- mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
- assertFalse("Updated-package should not be present",
- mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
- assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
- assertTrue("VIBRATE permission should be granted",
- mUtils.packageHasPermission(PACKAGE_NAME, VIBRATE_PERMISSION));
- assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
- mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
}
/**
diff --git a/prod-tests/src/com/android/graphics/tests/ImageProcessingTest.java b/prod-tests/src/com/android/graphics/tests/ImageProcessingTest.java
index cc3dc9f..9166c5d 100644
--- a/prod-tests/src/com/android/graphics/tests/ImageProcessingTest.java
+++ b/prod-tests/src/com/android/graphics/tests/ImageProcessingTest.java
@@ -18,6 +18,7 @@
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
@@ -26,7 +27,6 @@
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.RunUtil;
diff --git a/prod-tests/src/com/android/graphics/tests/SkiaTest.java b/prod-tests/src/com/android/graphics/tests/SkiaTest.java
new file mode 100644
index 0000000..7d0dc18
--- /dev/null
+++ b/prod-tests/src/com/android/graphics/tests/SkiaTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.graphics.tests;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IFileEntry;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.util.RunUtil;
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import java.io.File;
+import java.util.Collections;
+
+/**
+ * Test for running Skia native tests.
+ *
+ * The test is not necessarily Skia specific, but it provides
+ * functionality that allows native Skia tests to be run.
+ *
+ * Includes options to specify the Skia test app to run (inside
+ * nativetest directory), flags to pass to the test app, and a file
+ * to retrieve off the device after the test completes. (Skia test
+ * apps record their results to a json file, so retrieving this file
+ * allows us to view the results so long as the app completed.)
+ */
+@OptionClass(alias = "skia_native_tests")
+public class SkiaTest implements IRemoteTest, IDeviceTest {
+ private ITestDevice mDevice;
+
+ static final String DEFAULT_NATIVETEST_PATH = "/data/nativetest";
+
+ @Option(name = "native-test-device-path",
+ description = "The path on the device where native tests are located.")
+ private String mNativeTestDevicePath = DEFAULT_NATIVETEST_PATH;
+
+ @Option(name = "skia-flags",
+ description = "Flags to pass to the skia program.")
+ private String mFlags = "";
+
+ @Option(name = "skia-app",
+ description = "Skia program to run.",
+ mandatory = true)
+ private String mSkiaApp = "";
+
+ @Option(name = "skia-json",
+ description = "Full path on device for json output file.")
+ private File mOutputFile = null;
+
+ @Option(name = "skia-pngs",
+ description = "Directory on device for holding png results for retrieval.")
+ private File mPngDir = null;
+
+ @Override
+ public void setDevice(ITestDevice device) {
+ mDevice = device;
+ }
+
+ @Override
+ public ITestDevice getDevice() {
+ return mDevice;
+ }
+
+ @Override
+ public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+ if (mDevice == null) {
+ throw new IllegalArgumentException("Device has not been set");
+ }
+
+ listener.testRunStarted(mSkiaApp, 1);
+ long start = System.currentTimeMillis();
+
+ // Native Skia tests are in nativeTestDirectory/mSkiaApp/mSkiaApp.
+ String fullPath = mNativeTestDevicePath + "/"
+ + mSkiaApp + "/" + mSkiaApp;
+ IFileEntry app = mDevice.getFileEntry(fullPath);
+ TestIdentifier testId = new TestIdentifier(mSkiaApp, "testFileExists");
+ listener.testStarted(testId);
+ if (app == null) {
+ CLog.w("Could not find test %s in %s!", fullPath, mDevice.getSerialNumber());
+ listener.testFailed(testId, "Device does not have " + fullPath);
+ listener.testEnded(testId, null);
+ } else {
+ // The test for detecting the file has ended.
+ listener.testEnded(testId, null);
+ prepareDevice();
+ runTest(app);
+ retrieveFiles(mSkiaApp, listener);
+ }
+
+ listener.testRunEnded(System.currentTimeMillis() - start,
+ Collections.<String, String>emptyMap());
+ }
+
+ /**
+ * Emulates running mkdirs on an ITestDevice.
+ *
+ * Creates the directory named by dir *on device*, recursively creating missing parent
+ * directories if necessary.
+ *
+ * @param dir Directory to create.
+ */
+ private void mkdirs(File dir) throws DeviceNotAvailableException {
+ if (dir == null || mDevice.doesFileExist(dir.getPath())) {
+ return;
+ }
+
+ String dirName = dir.getPath();
+ CLog.v("creating folder '%s'", dirName);
+ mDevice.executeShellCommand("mkdir -p " + dirName);
+ }
+
+ /**
+ * Do pre-test setup on the device.
+ *
+ * Setup involves ensuring necessary directories exist and removing old
+ * test result files.
+ */
+ private void prepareDevice() throws DeviceNotAvailableException {
+ if (mOutputFile != null) {
+ String path = mOutputFile.getPath();
+ if (mDevice.doesFileExist(path)) {
+ // Delete the file. We don't want to think this file from an
+ // earlier run represents this one.
+ CLog.v("Removing old file " + path);
+ mDevice.executeShellCommand("rm " + path);
+ } else {
+ // Ensure its containing folder exists.
+ mkdirs(mOutputFile.getParentFile());
+ }
+ }
+
+ if (mPngDir != null) {
+ String pngPath = mPngDir.getPath();
+ if (mDevice.doesFileExist(pngPath)) {
+ // Empty the old directory
+ mDevice.executeShellCommand("rm -rf " + pngPath + "/*");
+ } else {
+ mkdirs(mPngDir);
+ }
+ }
+ }
+
+ /**
+ * Retrieve a file from the device and upload it to the listener.
+ *
+ * Each file for uploading is considered its own test, so we can track
+ * whether or not uploading succeeded.
+ *
+ * @param remoteFile File on the device.
+ * @param testIdClass String to be passed to TestIdentifier's constructor
+ * as className.
+ * @param testIdMethod String passed to TestIdentifier's constructor as
+ * testName.
+ * @param listener Listener for reporting test failure/success and
+ * uploading files.
+ * @param type LogDataType of the file being uploaded.
+ */
+ private void retrieveAndUploadFile(File remoteFile, String testIdClass, String testIdMethod,
+ ITestInvocationListener listener, LogDataType type) throws DeviceNotAvailableException {
+ String remotePath = remoteFile.getPath();
+ CLog.v("adb pull %s (using pullFile)", remotePath);
+ File localFile = mDevice.pullFile(remotePath);
+
+ TestIdentifier testId = new TestIdentifier(testIdClass, testIdMethod);
+ listener.testStarted(testId);
+ if (localFile == null) {
+ listener.testFailed(testId, "Failed to pull " + remotePath);
+ } else {
+ CLog.v("pulled result file to " + localFile.getPath());
+ FileInputStreamSource source = new FileInputStreamSource(localFile);
+ // Use the original name, for clarity.
+ listener.testLog(remoteFile.getName(), type, source);
+ source.cancel();
+ if (!localFile.delete()) {
+ CLog.w("Failed to delete temporary file %s", localFile.getPath());
+ }
+ }
+ listener.testEnded(testId, null);
+ }
+
+ /**
+ * Retrieve files from the device.
+ *
+ * Report to the listener whether retrieving the files succeeded.
+ *
+ * @param appName Name of the app.
+ * @param listener Listener for reporting results of file retrieval.
+ */
+ private void retrieveFiles(String appName,
+ ITestInvocationListener listener) throws DeviceNotAvailableException {
+ // FIXME: This could be achieved with DeviceFileReporter. Blocked on b/18408206.
+ if (mOutputFile != null) {
+ retrieveAndUploadFile(mOutputFile, appName, "outputJson", listener, LogDataType.TEXT);
+ }
+
+ if (mPngDir != null) {
+ String pngDir = mPngDir.getPath();
+ IFileEntry remotePngDir = mDevice.getFileEntry(pngDir);
+ for (IFileEntry pngFile : remotePngDir.getChildren(false)) {
+ if (pngFile.getName().endsWith("png")) {
+ retrieveAndUploadFile(new File(pngFile.getFullPath()),
+ "PngRetrieval", pngFile.getName(), listener, LogDataType.PNG);
+ }
+ }
+ }
+ }
+
+ /**
+ * Run a test on a device.
+ *
+ * @param app Test app to run.
+ */
+ private void runTest(IFileEntry app) throws DeviceNotAvailableException {
+ String fullPath = app.getFullEscapedPath();
+ // force file to be executable
+ mDevice.executeShellCommand(String.format("chmod 755 %s", fullPath));
+
+ // The device will not immediately capture logs in response to
+ // startLogcat. Instead, it delays 5 * 1000ms. See TestDevice.java
+ // mLogStartDelay. To ensure we see all the logs, sleep by the same
+ // amount.
+ mDevice.startLogcat();
+ RunUtil.getDefault().sleep(5 * 1000);
+
+ String cmd = fullPath + " " + mFlags;
+ CLog.v("Running '%s' on %s", cmd, mDevice.getSerialNumber());
+
+ mDevice.executeShellCommand("stop");
+ mDevice.executeShellCommand(cmd);
+ mDevice.executeShellCommand("start");
+ }
+}
diff --git a/prod-tests/src/com/android/graphics/tests/UiPerformanceTest.java b/prod-tests/src/com/android/graphics/tests/UiPerformanceTest.java
index 7dacb1d..b1016f2 100644
--- a/prod-tests/src/com/android/graphics/tests/UiPerformanceTest.java
+++ b/prod-tests/src/com/android/graphics/tests/UiPerformanceTest.java
@@ -131,7 +131,7 @@
String rawFileList =
mTestDevice.executeShellCommand(String.format("ls \"%s\"", rawFileDir));
- String[] rawFileString = rawFileList.split("\r\n");
+ String[] rawFileString = rawFileList.split("\r?\n");
File resFile = null;
InputStreamSource outputSource = null;
for (int i = 0; i < rawFileString.length; i++) {
diff --git a/prod-tests/src/com/android/media/tests/AudioJitterTest.java b/prod-tests/src/com/android/media/tests/AudioJitterTest.java
index 5c12a86..7058cc7 100644
--- a/prod-tests/src/com/android/media/tests/AudioJitterTest.java
+++ b/prod-tests/src/com/android/media/tests/AudioJitterTest.java
@@ -17,7 +17,6 @@
package com.android.media.tests;
import com.android.ddmlib.CollectingOutputReceiver;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
@@ -43,7 +42,6 @@
private ITestDevice mDevice;
- /* /home/android-test/testdata/media/sljitter */
private static final String DEVICE_TEMPORARY_DIR_PATH = "/data/local/tmp/";
private static final String JITTER_BINARY_FILENAME = "sljitter";
private static final String JITTER_BINARY_DEVICE_PATH =
@@ -121,7 +119,7 @@
if (errMsg != null) {
CLog.e(errMsg);
- listener.testFailed(TestFailure.FAILURE, testId, errMsg);
+ listener.testFailed(testId, errMsg);
listener.testEnded(testId, metrics);
listener.testRunFailed(errMsg);
} else {
diff --git a/prod-tests/src/com/android/media/tests/CameraLatencyTest.java b/prod-tests/src/com/android/media/tests/CameraLatencyTest.java
index 64d055d..82375d4 100644
--- a/prod-tests/src/com/android/media/tests/CameraLatencyTest.java
+++ b/prod-tests/src/com/android/media/tests/CameraLatencyTest.java
@@ -167,9 +167,8 @@
// Grab a bugreport if warranted
if (auxListener.hasFailedTests()) {
- CLog.i("Grabbing bugreport after test '%s' finished with %d failures and %d errors.",
- test.mTestName, auxListener.getNumFailedTests(),
- auxListener.getNumErrorTests());
+ CLog.i("Grabbing bugreport after test '%s' finished with %d failures.",
+ test.mTestName, auxListener.getNumAllFailedTests());
InputStreamSource bugreport = mTestDevice.getBugreport();
listener.testLog(String.format("bugreport-%s.txt", test.mTestName),
LogDataType.BUGREPORT, bugreport);
diff --git a/prod-tests/src/com/android/media/tests/CameraPerformanceTest.java b/prod-tests/src/com/android/media/tests/CameraPerformanceTest.java
new file mode 100644
index 0000000..aee0316
--- /dev/null
+++ b/prod-tests/src/com/android/media/tests/CameraPerformanceTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.media.tests;
+
+import com.google.common.collect.ImmutableMap;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.StubTestInvocationListener;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.InstrumentationTest;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This test invocation runs android.hardware.camera2.cts.PerformanceTest -
+ * Camera2 API use case performance KPIs, such as camera open time, session creation time,
+ * shutter lag etc. The KPI data will be parsed and reported to dashboard.
+ */
+public class CameraPerformanceTest implements IDeviceTest, IRemoteTest {
+
+ private static final String LOG_TAG = CameraPerformanceTest.class.getSimpleName();
+ private static final String TEST_CLASS_NAME =
+ "android.hardware.camera2.cts.PerformanceTest";
+ private static final String TEST_PACKAGE_NAME = "com.android.cts.hardware";
+ private static final String TEST_RUNNER_NAME =
+ "android.support.test.runner.AndroidJUnitRunner";
+ private static final String RU_KEY = "camera_framework_performance";
+
+ private final int MAX_TEST_TIMEOUT = 10 * 60 * 1000; // 10 mins
+
+ @Option(name="method", shortName = 'm',
+ description="Used to specify a specific test method to run")
+ private String mMethodName = null;
+
+ private ITestDevice mDevice = null;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setDevice(ITestDevice device) {
+ mDevice = device;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ITestDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+ CollectingListener collectingListener = new CollectingListener();
+ runTest(collectingListener);
+ Map<String, String> parsedMetrics = parseResult(collectingListener.mStdout);
+ postMetrics(listener, parsedMetrics);
+ }
+
+ private void runTest(ITestInvocationListener listener) throws DeviceNotAvailableException {
+ InstrumentationTest instr = new InstrumentationTest();
+ instr.setDevice(getDevice());
+ instr.setPackageName(TEST_PACKAGE_NAME);
+ instr.setRunnerName(TEST_RUNNER_NAME);
+ instr.setClassName(TEST_CLASS_NAME);
+ if (mMethodName != null) {
+ instr.setMethodName(mMethodName);
+ }
+ instr.setShellTimeout(MAX_TEST_TIMEOUT);
+ instr.run(listener);
+ }
+
+ /**
+ * Parse Camera Performance KPIs result from the stdout generated by each test run.
+ * Then put them all together to post the final report
+ *
+ * @return a {@link HashMap} that contains pairs of kpiName and kpiValue
+ */
+ private Map<String, String> parseResult(Map<String, String> metrics) {
+ Map<String, String> resultsAll = new HashMap<String, String>();
+ Camera2KpiParser parser = new Camera2KpiParser();
+ for (Map.Entry<String, String> metric : metrics.entrySet()) {
+ String testMethod = metric.getKey();
+ String stdout = metric.getValue();
+ CLog.d("test name %s", testMethod);
+ CLog.d("stdout %s", stdout);
+
+ // Get pairs of { KPI name, KPI value } from stdout that each test outputs.
+ // Assuming that a device has both the front and back cameras, parser will return
+ // 2 KPIs in HashMap. For an example of testCameraLaunch,
+ // {
+ // ("Camera 0 Camera launch time", "379.20"),
+ // ("Camera 1 Camera launch time", "272.80"),
+ // }
+ Map<String, String> testKpis = parser.parse(stdout, testMethod);
+ for (String k : testKpis.keySet()) {
+ if (resultsAll.containsKey(k)) {
+ throw new RuntimeException(String.format("KPI name (%s) conflicts with " +
+ "the existing names. ", k));
+ }
+ }
+
+ // Put each result together to post the final result
+ resultsAll.putAll(testKpis);
+ }
+ return resultsAll;
+ }
+
+ /**
+ * A listener to collect the stdout from each test run.
+ */
+ private class CollectingListener extends StubTestInvocationListener {
+ public Map<String, String> mStdout = new HashMap<String, String>();
+
+ @Override
+ public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
+ for (Map.Entry<String, String> metric : testMetrics.entrySet()) {
+ // capture only test name and stdout generated by test
+ mStdout.put(test.getTestName(), metric.getValue());
+ }
+ }
+ }
+
+ /**
+ * Data class of Camera Performance KPIs separated into summary and KPI items
+ */
+ private class Camera2KpiData {
+ public class KpiItem {
+ private String mTestId; // "android.hardware.camera2.cts.PerformanceTest#testSingleCapture"
+ private String mCameraId; // "0" or "1"
+ private String mKpiName; // "Camera capture latency"
+ private String mType; // "lower_better"
+ private String mUnit; // "ms"
+ private String mKpiValue; // "736.0 688.0 679.0 667.0 686.0"
+ private String mKey; // primary key = cameraId + kpiName
+ private KpiItem(String testId, String cameraId, String kpiName, String type,
+ String unit, String kpiValue) {
+ mTestId = testId;
+ mCameraId = cameraId;
+ mKpiName = kpiName;
+ mType = type;
+ mUnit = unit;
+ mKpiValue = kpiValue;
+ // Note that the key shouldn't contain ":" for side by side report.
+ mKey = String.format("Camera %s %s", cameraId, kpiName);
+ }
+ public String getTestId() { return mTestId; }
+ public String getCameraId() { return mCameraId; }
+ public String getKpiName() { return mKpiName; }
+ public String getType() { return mType; }
+ public String getUnit() { return mUnit; }
+ public String getKpiValue() { return mKpiValue; }
+ public String getKey() { return mKey; }
+ }
+
+ private KpiItem mSummary;
+ private Map<String, KpiItem> mKpis = new HashMap<String, KpiItem>();
+
+ public KpiItem createItem(String testId, String cameraId, String kpiName, String type,
+ String unit, String kpiValue) {
+ return new KpiItem(testId, cameraId, kpiName, type, unit, kpiValue);
+ }
+ public KpiItem getSummary() { return mSummary; }
+ public void setSummary(KpiItem summary) { mSummary = summary; }
+ public List<KpiItem> getKpisByKpiName(String kpiName) {
+ List<KpiItem> kpiItems = new ArrayList<KpiItem>();
+ for (KpiItem log : mKpis.values()) {
+ if (log.getKpiName().equals(kpiName)) {
+ kpiItems.add(log);
+ }
+ }
+ return kpiItems;
+ }
+ public void addKpi(KpiItem kpiItem) {
+ mKpis.put(kpiItem.getKey(), kpiItem);
+ }
+ }
+
+ /**
+ * Parses the stdout generated by the underlying instrumentation test
+ * and returns it to test runner for later reporting.
+ *
+ * Format:
+ * (summary message)| |(type)|(unit)|(value) ++++
+ * (test id)|(message)|(type)|(unit)|(value)... +++
+ * ...
+ *
+ * Example:
+ * Camera launch average time for Camera 1| |lower_better|ms|586.6++++
+ * android.hardware.camera2.cts.PerformanceTest#testCameraLaunch:171|Camera 0: Camera open time|lower_better|ms|74.0 100.0 70.0 67.0 82.0 +++
+ * android.hardware.camera2.cts.PerformanceTest#testCameraLaunch:171|Camera 0: Camera configure stream time|lower_better|ms|9.0 5.0 5.0 8.0 5.0
+ * ...
+ *
+ * See also com.android.cts.util.ReportLog for the format detail.
+ *
+ */
+ private class Camera2KpiParser {
+ private static final String LOG_SEPARATOR = "\\+\\+\\+";
+ private static final String SUMMARY_SEPARATOR = "\\+\\+\\+\\+";
+ private static final String LOG_ELEM_SEPARATOR = "|";
+ private final Pattern SUMMARY_REGEX = Pattern.compile(
+ "^(?<message>[^|]+)\\| \\|(?<type>[^|]+)\\|(?<unit>[^|]+)\\|(?<value>[0-9 .]+)");
+ private final Pattern KPI_REGEX = Pattern.compile(
+ "^(?<testId>[^|]+)\\|(?<message>[^|]+)\\|(?<type>[^|]+)\\|(?<unit>[^|]+)\\|(?<values>[0-9 .]+)");
+ // eg. "Camera 0: Camera capture latency"
+ private final Pattern KPI_KEY_REGEX = Pattern.compile(
+ "^Camera\\s+(?<cameraId>\\d+):\\s+(?<kpiName>.*)");
+
+ // HashMap that contains pairs of (testMethod), (the name of KPI to be reported)
+ // TODO(hyungtaekim) : Use MultiMap instead if more than one KPI need to be reported
+ private final ImmutableMap<String, String> REPORTING_KPIS =
+ new ImmutableMap.Builder<String, String>()
+ .put("testCameraLaunch", "Camera launch time")
+ .put("testSingleCapture", "Camera capture result latency")
+ .build();
+
+ /**
+ * Parse Camera Performance KPIs result first, then leave the only KPIs that matter.
+ *
+ * @param input String to be parsed
+ * @param testMethod test method name used to leave the only metric that matters
+ * @return a {@link HashMap} that contains kpiName and kpiValue
+ */
+ public Map<String, String> parse(String input, String testMethod) {
+ return filter(parseToData(input), testMethod);
+ }
+
+ private Map<String, String> filter(Camera2KpiData data, String testMethod) {
+ Map<String, String> filtered = new HashMap<String, String>();
+ String kpiToReport = REPORTING_KPIS.get(testMethod);
+ // report the only selected items
+ List<Camera2KpiData.KpiItem> items = data.getKpisByKpiName(kpiToReport);
+ for (Camera2KpiData.KpiItem item : items) {
+ filtered.put(item.getKey(), item.getKpiValue());
+ }
+ return filtered;
+ }
+
+ private Camera2KpiData parseToData(String input) {
+ Camera2KpiData data = new Camera2KpiData();
+
+ // Split summary and KPIs from stdout passes as parameter.
+ String[] output = input.split(SUMMARY_SEPARATOR);
+ if (output.length != 2) {
+ throw new RuntimeException("Value not in correct format");
+ }
+ Matcher summaryMatcher = SUMMARY_REGEX.matcher(output[0].trim());
+
+ // Parse summary.
+ // Example: "Camera launch average time for Camera 1| |lower_better|ms|586.6++++"
+ if (summaryMatcher.matches()) {
+ data.setSummary(data.createItem(null,
+ "-1",
+ summaryMatcher.group("message"),
+ summaryMatcher.group("type"),
+ summaryMatcher.group("unit"),
+ summaryMatcher.group("value")));
+ } else {
+ // Currently malformed summary won't block a test as it's not used for report.
+ CLog.w("Summary not in correct format");
+ }
+
+ // Parse KPIs.
+ // Example: "android.hardware.camera2.cts.PerformanceTest#testCameraLaunch:171|Camera 0: Camera open time|lower_better|ms|74.0 100.0 70.0 67.0 82.0 +++"
+ String[] kpis = output[1].split(LOG_SEPARATOR);
+ for (String kpi : kpis) {
+ Matcher kpiMatcher = KPI_REGEX.matcher(kpi.trim());
+ if (kpiMatcher.matches()) {
+ String message = kpiMatcher.group("message");
+ Matcher m = KPI_KEY_REGEX.matcher(message.trim());
+ if (!m.matches()) {
+ throw new RuntimeException("Value not in correct format");
+ }
+ String cameraId = m.group("cameraId");
+ String kpiName = m.group("kpiName");
+ // get average of kpi values
+ String[] values = kpiMatcher.group("values").split("\\s+");
+ double sum = 0;
+ for (String value : values) {
+ sum += Double.parseDouble(value);
+ }
+ String kpiValue = String.format("%.1f", sum / values.length);
+ data.addKpi(data.createItem(kpiMatcher.group("testId"),
+ cameraId,
+ kpiName,
+ kpiMatcher.group("type"),
+ kpiMatcher.group("unit"),
+ kpiValue));
+ } else {
+ throw new RuntimeException("KPI not in correct format");
+ }
+ }
+ return data;
+ }
+ }
+
+ /**
+ * Report run metrics by creating an empty test run to stick them in.
+ *
+ * @param listener The {@link ITestInvocationListener} of test results
+ * @param metrics The {@link Map} that contains metrics for the given test
+ */
+ private void postMetrics(ITestInvocationListener listener, Map<String, String> metrics) {
+ listener.testRunStarted(RU_KEY, 1);
+ TestIdentifier testId = new TestIdentifier(RU_KEY, LOG_TAG);
+ listener.testStarted(testId);
+ listener.testEnded(testId, Collections.<String, String> emptyMap());
+ listener.testRunEnded(0, metrics);
+ }
+}
diff --git a/prod-tests/src/com/android/media/tests/CameraStressTest.java b/prod-tests/src/com/android/media/tests/CameraStressTest.java
index a7bf58a..c3504f5 100644
--- a/prod-tests/src/com/android/media/tests/CameraStressTest.java
+++ b/prod-tests/src/com/android/media/tests/CameraStressTest.java
@@ -203,8 +203,7 @@
// Grab a bugreport if warranted
if (auxListener.hasFailedTests()) {
Log.e(LOG_TAG, String.format("Grabbing bugreport after test '%s' finished with " +
- "%d failures and %d errors.", test.mTestName, auxListener.getNumFailedTests(),
- auxListener.getNumErrorTests()));
+ "%d failures.", test.mTestName, auxListener.getNumAllFailedTests()));
InputStreamSource bugreport = mTestDevice.getBugreport();
listener.testLog(String.format("bugreport-%s.txt", test.mTestName),
LogDataType.BUGREPORT, bugreport);
diff --git a/prod-tests/src/com/android/media/tests/MediaPlayerStressTest.java b/prod-tests/src/com/android/media/tests/MediaPlayerStressTest.java
index 07bef61..b1d911d 100644
--- a/prod-tests/src/com/android/media/tests/MediaPlayerStressTest.java
+++ b/prod-tests/src/com/android/media/tests/MediaPlayerStressTest.java
@@ -85,6 +85,8 @@
mPatternMap.put("PlaybackCrash", "^Total Error: (\\d+)");
mPatternMap.put("TrackLagging", "^Total Track Lagging: (\\d+)");
mPatternMap.put("BadInterleave", "^Total Bad Interleaving: (\\d+)");
+ mPatternMap.put("FailedToCompleteWithNoError",
+ "^Total Failed To Complete With No Error: (\\d+)");
}
@Override
diff --git a/prod-tests/src/com/android/media/tests/VideoMultimeterRunner.java b/prod-tests/src/com/android/media/tests/VideoMultimeterRunner.java
new file mode 100644
index 0000000..af5a318
--- /dev/null
+++ b/prod-tests/src/com/android/media/tests/VideoMultimeterRunner.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.media.tests;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.util.CommandResult;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+
+/**
+ * A harness that test video playback with multiple devices and reports result.
+ */
+public class VideoMultimeterRunner extends VideoMultimeterTest
+ implements IDeviceTest, IRemoteTest {
+
+ @Option(name = "robot-util-path", description = "path for robot control util",
+ importance = Importance.ALWAYS, mandatory = true)
+ String mRobotUtilPath = "/tmp/robot_util.sh";
+
+ @Option(name = "device-map", description =
+ "Device serials map to location and audio input. May be repeated",
+ importance = Importance.ALWAYS)
+ Map<String, String> mDeviceMap = new HashMap<String, String>();
+
+ static final long ROBOT_TIMEOUT_MS = 60 * 1000;
+
+ static final Semaphore runToken = new Semaphore(1);
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run(ITestInvocationListener listener)
+ throws DeviceNotAvailableException {
+ long durationMs = 0;
+ TestIdentifier testId = new TestIdentifier(getClass()
+ .getCanonicalName(), RUN_KEY);
+
+ listener.testRunStarted(RUN_KEY, 0);
+ listener.testStarted(testId);
+
+ long testStartTime = System.currentTimeMillis();
+ Map<String, String> metrics = new HashMap<String, String>();
+
+ try {
+ CLog.v("Waiting to acquire run token");
+ runToken.acquire();
+
+ String deviceSerial = getDevice().getSerialNumber();
+
+ if (moveArm(deviceSerial) && setupTestEnv()) {
+ runMultimeterTest(listener, metrics);
+ } else {
+ listener.testFailed(testId, "Failed to set up environment");
+ }
+ } catch (InterruptedException e) {
+ CLog.d("Acquire run token interrupted");
+ listener.testFailed(testId, "Failed to acquire run token");
+ } finally {
+ runToken.release();
+ listener.testEnded(testId, metrics);
+ durationMs = System.currentTimeMillis() - testStartTime;
+ listener.testRunEnded(durationMs, metrics);
+ }
+ }
+
+ protected boolean moveArm(String deviceSerial) {
+ if (mDeviceMap.containsKey(deviceSerial)) {
+ CLog.v("Moving robot arm to device " + deviceSerial);
+ CommandResult cr = getRunUtil().runTimedCmd(
+ ROBOT_TIMEOUT_MS, mRobotUtilPath, mDeviceMap.get(deviceSerial));
+ CLog.v(cr.getStdout());
+ return true;
+ } else {
+ CLog.e("Cannot find device in map, test failed");
+ return false;
+ }
+ }
+}
diff --git a/prod-tests/src/com/android/media/tests/VideoMultimeterTest.java b/prod-tests/src/com/android/media/tests/VideoMultimeterTest.java
index 2d01490..d8e5e80 100644
--- a/prod-tests/src/com/android/media/tests/VideoMultimeterTest.java
+++ b/prod-tests/src/com/android/media/tests/VideoMultimeterTest.java
@@ -23,12 +23,15 @@
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.SnapshotInputStreamSource;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
+import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -42,45 +45,53 @@
*/
public class VideoMultimeterTest implements IDeviceTest, IRemoteTest {
- private static final String RUN_KEY = "video_multimeter";
+ static final String RUN_KEY = "video_multimeter";
@Option(name = "multimeter-util-path", description = "path for multimeter control util",
importance = Importance.ALWAYS)
- private String mUtilPath = "/tmp/util.sh";
+ String mMeterUtilPath = "/tmp/util.sh";
- private static final String START_VIDEO_PLAYER = "am start"
+ static final String START_VIDEO_PLAYER = "am start"
+ " -a android.intent.action.VIEW -t video/mp4 -d \"file://%s\""
+ " -n \"com.google.android.apps.plus/.phone.VideoViewActivity\"";
- private static final String KILL_VIDEO_PLAYER = "am force-stop com.google.android.apps.plus";
- private static final String ROTATE_LANDSCAPE = "content insert --uri content://settings/system"
+ static final String KILL_VIDEO_PLAYER = "am force-stop com.google.android.apps.plus";
+ static final String ROTATE_LANDSCAPE = "content insert --uri content://settings/system"
+ " --bind name:s:user_rotation --bind value:i:1";
- private static final String VIDEO_DIR = "/sdcard/DCIM/Camera/";
+ static final String VIDEO_DIR = "/sdcard/DCIM/Camera/";
- private static final String CALI_VIDEO_DEVICE_PATH = VIDEO_DIR + "video_cali.mp4";
+ static final String CALI_VIDEO_DEVICE_PATH = VIDEO_DIR + "video_cali.mp4";
- private static final String TEST_VIDEO_1_DEVICE_PATH = VIDEO_DIR + "video.mp4";
- private static final String TEST_VIDEO_1_PREFIX = "bbb_";
- private static final long TEST_VIDEO_1_DURATION = 11 * 60; // in second
+ // FIXIT: move video path and info to options for flexibility
+ static final String TEST_VIDEO_1_DEVICE_PATH = VIDEO_DIR + "video.mp4";
+ static final String TEST_VIDEO_1_PREFIX = "24fps_";
+ static final float TEST_VIDEO_1_FPS = 24;
+ static final long TEST_VIDEO_1_DURATION = 11 * 60; // in second
- private static final String TEST_VIDEO_2_DEVICE_PATH = VIDEO_DIR + "video2.mp4";
- private static final String TEST_VIDEO_2_PREFIX = "60fps_";
- private static final long TEST_VIDEO_2_DURATION = 5 * 60; // in second
+ static final String TEST_VIDEO_2_DEVICE_PATH = VIDEO_DIR + "video2.mp4";
+ static final String TEST_VIDEO_2_PREFIX = "60fps_";
+ static final float TEST_VIDEO_2_FPS = 60;
+ static final long TEST_VIDEO_2_DURATION = 5 * 60; // in second
- private static final String CMD_GET_FRAMERATE_STATE = "GETF";
- private static final String CMD_START_CALIBRATION = "STAC";
- private static final String CMD_STOP_CALIBRATION = "STOC";
- private static final String CMD_START_MEASUREMENT = "STAM";
- private static final String CMD_STOP_MEASUREMENT = "STOM";
- private static final String CMD_GET_NUM_FRAMES = "GETN";
- private static final String CMD_GET_ALL_DATA = "GETD";
+ // Max number of trailing frames to trim
+ static final int TRAILING_FRAMES_MAX = 3;
+ // Min threshold for duration of trailing frames
+ static final long FRAME_DURATION_THRESHOLD_US = 500 * 1000; // 0.5s
- private static final long DEVICE_SYNC_TIME_MS = 30 * 1000;
- private static final long CALIBRATION_TIMEOUT_MS = 30 * 1000;
- private static final long COMMAND_TIMEOUT_MS = 5 * 1000;
- private static final long GETDATA_TIMEOUT_MS = 10 * 60 * 1000;
+ static final String CMD_GET_FRAMERATE_STATE = "GETF";
+ static final String CMD_START_CALIBRATION = "STAC";
+ static final String CMD_STOP_CALIBRATION = "STOC";
+ static final String CMD_START_MEASUREMENT = "STAM";
+ static final String CMD_STOP_MEASUREMENT = "STOM";
+ static final String CMD_GET_NUM_FRAMES = "GETN";
+ static final String CMD_GET_ALL_DATA = "GETD";
- private ITestDevice mDevice;
+ static final long DEVICE_SYNC_TIME_MS = 30 * 1000;
+ static final long CALIBRATION_TIMEOUT_MS = 30 * 1000;
+ static final long COMMAND_TIMEOUT_MS = 5 * 1000;
+ static final long GETDATA_TIMEOUT_MS = 10 * 60 * 1000;
+
+ ITestDevice mDevice;
/**
* {@inheritDoc}
@@ -105,19 +116,19 @@
}
}
- private boolean setupTestEnv() throws DeviceNotAvailableException {
+ protected boolean setupTestEnv() throws DeviceNotAvailableException {
getRunUtil().sleep(DEVICE_SYNC_TIME_MS);
CommandResult cr = getRunUtil().runTimedCmd(
- COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_MEASUREMENT);
+ COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_MEASUREMENT);
getDevice().setDate(new Date());
CLog.i("syncing device time to host time");
getRunUtil().sleep(3 * 1000);
// start and stop to clear old data
- cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_START_MEASUREMENT);
+ cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_START_MEASUREMENT);
getRunUtil().sleep(3 * 1000);
- cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_MEASUREMENT);
+ cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_MEASUREMENT);
getRunUtil().sleep(3 * 1000);
CLog.i("Stopping measurement: " + cr.getStdout());
getDevice().unlockDevice();
@@ -128,7 +139,7 @@
getRunUtil().sleep(3 * 1000);
rotateScreen();
getRunUtil().sleep(1 * 1000);
- cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_START_CALIBRATION);
+ cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_START_CALIBRATION);
CLog.i("Starting calibration: " + cr.getStdout());
// check whether multimeter is calibrated
@@ -137,14 +148,15 @@
while (!isCalibrated
&& System.currentTimeMillis() - calibrationStartTime <= CALIBRATION_TIMEOUT_MS) {
getRunUtil().sleep(1 * 1000);
- cr = getRunUtil().runTimedCmd(2 * 1000, mUtilPath, CMD_GET_FRAMERATE_STATE);
+ cr = getRunUtil().runTimedCmd(2 * 1000, mMeterUtilPath, CMD_GET_FRAMERATE_STATE);
if (cr.getStdout().contains("calib0")) {
isCalibrated = true;
}
}
getDevice().executeShellCommand(KILL_VIDEO_PLAYER);
if (!isCalibrated) {
- cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_CALIBRATION);
+ cr = getRunUtil().runTimedCmd(
+ COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_CALIBRATION);
CLog.e("Calibration timed out.");
return false;
} else {
@@ -165,16 +177,17 @@
rotateScreen();
getRunUtil().sleep(1 * 1000);
- cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_START_MEASUREMENT);
+ cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_START_MEASUREMENT);
CLog.i("Starting measurement: " + cr.getStdout());
// end measurement
getRunUtil().sleep(durationSecond * 1000);
- cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_MEASUREMENT);
+ cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_MEASUREMENT);
CLog.i("Stopping measurement: " + cr.getStdout());
if (cr == null || !cr.getStdout().contains("OK")) {
- cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_MEASUREMENT);
+ cr = getRunUtil().runTimedCmd(
+ COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_MEASUREMENT);
CLog.i("Retry - Stopping measurement: " + cr.getStdout());
}
@@ -182,23 +195,33 @@
getDevice().clearErrorDialogs();
}
- private Map<String, String> getResult(Map<String, String> metrics,
- String keyprefix, boolean lipsync) {
+ private Map<String, String> getResult(ITestInvocationListener listener,
+ Map<String, String> metrics, String keyprefix, float fps, boolean lipsync) {
CommandResult cr;
// get number of results
getRunUtil().sleep(5 * 1000);
- cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_GET_NUM_FRAMES);
+ cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_GET_NUM_FRAMES);
String frameNum = cr.getStdout();
CLog.i("Number of results: " + frameNum);
- // get all results
- cr = getRunUtil().runTimedCmd(GETDATA_TIMEOUT_MS, mUtilPath, CMD_GET_ALL_DATA);
+ // get all results and write to output file
+ cr = getRunUtil().runTimedCmd(GETDATA_TIMEOUT_MS, mMeterUtilPath, CMD_GET_ALL_DATA);
String allData = cr.getStdout();
- CLog.i("Data: " + allData);
+ listener.testLog(keyprefix, LogDataType.TEXT, new SnapshotInputStreamSource(
+ new ByteArrayInputStream(allData.getBytes())));
// parse results
- return parseResult(metrics, frameNum, allData, keyprefix, lipsync);
+ return parseResult(metrics, frameNum, allData, keyprefix, fps, lipsync);
+ }
+
+ protected void runMultimeterTest(ITestInvocationListener listener,
+ Map<String,String> metrics) throws DeviceNotAvailableException {
+ doMeasurement(TEST_VIDEO_1_DEVICE_PATH, TEST_VIDEO_1_DURATION);
+ metrics = getResult(listener, metrics, TEST_VIDEO_1_PREFIX, TEST_VIDEO_1_FPS, true);
+
+ doMeasurement(TEST_VIDEO_2_DEVICE_PATH, TEST_VIDEO_2_DURATION);
+ metrics = getResult(listener, metrics, TEST_VIDEO_2_PREFIX, TEST_VIDEO_2_FPS, true);
}
/**
@@ -217,11 +240,7 @@
Map<String, String> metrics = new HashMap<String, String>();
if (setupTestEnv()) {
- doMeasurement(TEST_VIDEO_1_DEVICE_PATH, TEST_VIDEO_1_DURATION);
- metrics = getResult(metrics, TEST_VIDEO_1_PREFIX, true);
-
- doMeasurement(TEST_VIDEO_2_DEVICE_PATH, TEST_VIDEO_2_DURATION);
- metrics = getResult(metrics, TEST_VIDEO_2_PREFIX, true);
+ runMultimeterTest(listener, metrics);
}
long durationMs = System.currentTimeMillis() - testStartTime;
@@ -236,16 +255,22 @@
* @return a {@link HashMap} that contains metrics keys and results
*/
private Map<String, String> parseResult(Map<String, String> metrics,
- String numFrames, String result, String keyprefix, boolean lipsync) {
- CLog.i("== Video Multimeter Result '%s' ==", keyprefix);
+ String numFrames, String result, String keyprefix, float fps,
+ boolean lipsync) {
+ final int MISSING_FRAME_CEILING = 5; //5+ frames missing count the same
+ final double[] MISSING_FRAME_WEIGHT = {0.0, 1.0, 2.5, 5.0, 6.25, 8.0};
+ CLog.i("== Video Multimeter Result '%s' ==", keyprefix);
Pattern p = Pattern.compile("OK\\s+(\\d+)$");
Matcher m = p.matcher(numFrames.trim());
+ String frameCapturedStr = "0";
+ long frameCaptured = 0;
if (m.matches()) {
- String numFrame = m.group(1);
- metrics.put(keyprefix + "frame_captured", numFrame);
- CLog.i("Captured frames: " + numFrame);
- if (Integer.parseInt(numFrame) == 0) {
+ frameCapturedStr = m.group(1);
+ metrics.put(keyprefix + "frame_captured", frameCapturedStr);
+ CLog.i("Captured frames: " + frameCapturedStr);
+ frameCaptured = Long.parseLong(frameCapturedStr);
+ if (frameCaptured == 0) {
// no frame captured
CLog.w("No frame captured for " + keyprefix);
return metrics;
@@ -255,49 +280,96 @@
return metrics;
}
- // Get total captured frames from the last line of result
+ // Get total captured frames and calculate smoothness and freezing score
// format: "OK (time); (frame duration); (marker color); (total dropped frames)"
+ p = Pattern.compile("OK\\s+\\d+;\\s*(-?\\d+);\\s*[a-z]+;\\s*(\\d+)");
String[] lines = result.split(System.getProperty("line.separator"));
- for (int i = lines.length - 1; i >= 0; i--) {
- p = Pattern.compile("OK\\s+\\d+;\\s*\\d+;\\s*[a-z]+;\\s*(\\d+)");
+ String totalDropFrame = "-1";
+ String lastDropFrame = "0";
+ long frameCount = 0;
+ long consecutiveDropFrame = 0;
+ double freezingPenalty = 0.0;
+ long frameDuration = 0;
+ double offByOne = 0;
+ double offByMultiple = 0;
+ double expectedFrameDurationInUs = 1000000.0 / fps;
+ for (int i = 0; i < lines.length; i++) {
m = p.matcher(lines[i].trim());
if (m.matches()) {
- String dropFrame = m.group(1);
- metrics.put(keyprefix + "frame_drop", dropFrame);
- CLog.i("Dropped frames: " + dropFrame);
- break;
+ frameCount++;
+ frameDuration = Long.parseLong(m.group(1));
+ totalDropFrame = m.group(2);
+ // trim the last few data points if needed
+ if (frameCount >= frameCaptured - TRAILING_FRAMES_MAX - 1 &&
+ frameDuration > FRAME_DURATION_THRESHOLD_US) {
+ metrics.put(keyprefix + "frame_captured", String.valueOf(frameCount));
+ break;
+ }
+ if (lastDropFrame.equals(totalDropFrame)) {
+ if (consecutiveDropFrame > 0) {
+ freezingPenalty += MISSING_FRAME_WEIGHT[(int) (Math.min(consecutiveDropFrame,
+ MISSING_FRAME_CEILING))] * consecutiveDropFrame;
+ consecutiveDropFrame = 0;
+ }
+ } else {
+ consecutiveDropFrame++;
+ }
+ lastDropFrame = totalDropFrame;
+
+ if (frameDuration < expectedFrameDurationInUs * 0.5) {
+ offByOne++;
+ } else if (frameDuration > expectedFrameDurationInUs * 1.5) {
+ if (frameDuration < expectedFrameDurationInUs * 2.5) {
+ offByOne++;
+ } else {
+ offByMultiple++;
+ }
+ }
}
}
- if (!metrics.containsKey(keyprefix + "frame_drop")) {
+ if (totalDropFrame.equals("-1")) {
// no matching result found
CLog.w("No result found for " + keyprefix);
return metrics;
+ } else {
+ metrics.put(keyprefix + "frame_drop", totalDropFrame);
+ CLog.i("Dropped frames: " + totalDropFrame);
}
+ double smoothnessScore = 100.0 - (offByOne / frameCaptured) * 100.0 -
+ (offByMultiple / frameCaptured) * 300.0;
+ metrics.put(keyprefix + "smoothness", String.valueOf(smoothnessScore));
+ CLog.i("Off by one frame: " + offByOne);
+ CLog.i("Off by multiple frames: " + offByMultiple);
+ CLog.i("Smoothness score: " + smoothnessScore);
+
+ double freezingScore = 100.0 - 100.0 * freezingPenalty / frameCaptured;
+ metrics.put(keyprefix + "freezing", String.valueOf(freezingScore));
+ CLog.i("Freezing score: " + freezingScore);
// parse lipsync results (the audio and video synchronization offset)
// format: "OK (time); (frame duration); (marker color); (total dropped frames); (lipsync)"
+ p = Pattern.compile("OK\\s+\\d+;\\s*\\d+;\\s*[a-z]+;\\s*\\d+;\\s*(-?\\d+)");
if (lipsync) {
ArrayList<Integer> lipsyncVals = new ArrayList<Integer>();
StringBuilder lipsyncValsStr = new StringBuilder("[");
long lipsyncSum = 0;
for (int i = 0; i < lines.length; i++) {
- p = Pattern.compile("OK\\s+\\d+;\\s*\\d+;\\s*[a-z]+;\\s*\\d+;\\s*(-?\\d+)");
m = p.matcher(lines[i].trim());
if (m.matches()) {
int lipSyncVal = Integer.parseInt(m.group(1));
lipsyncVals.add(lipSyncVal);
lipsyncValsStr.append(lipSyncVal);
- lipsyncValsStr.append(" ,");
+ lipsyncValsStr.append(", ");
lipsyncSum += lipSyncVal;
}
}
if (lipsyncVals.size() > 0) {
lipsyncValsStr.append("]");
+ CLog.i("Lipsync values: " + lipsyncValsStr);
Collections.sort(lipsyncVals);
int lipsyncCount = lipsyncVals.size();
int minLipsync = lipsyncVals.get(0);
int maxLipsync = lipsyncVals.get(lipsyncCount - 1);
- CLog.i("Lipsync values: " + lipsyncVals.toString());
metrics.put(keyprefix + "lipsync_count", String.valueOf(lipsyncCount));
CLog.i("Lipsync Count: " + lipsyncCount);
metrics.put(keyprefix + "lipsync_min", String.valueOf(lipsyncVals.get(0)));
@@ -315,7 +387,7 @@
return metrics;
}
- private IRunUtil getRunUtil() {
+ protected IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
}
diff --git a/prod-tests/src/com/android/monkey/MonkeyBase.java b/prod-tests/src/com/android/monkey/MonkeyBase.java
index a9fef9b..6a0b59a 100644
--- a/prod-tests/src/com/android/monkey/MonkeyBase.java
+++ b/prod-tests/src/com/android/monkey/MonkeyBase.java
@@ -80,6 +80,8 @@
private static final String LAUNCH_APP_CMD = "am start -W -n '%s' " +
"-a android.intent.action.MAIN -c android.intent.category.LAUNCHER -f 0x10200000";
+ private static final String NULL_UPTIME = "0.00";
+
/**
* Helper to run a monkey command with an absolute timeout.
* <p>
@@ -204,6 +206,10 @@
@Option(name = "screenshot", description = "Take a device screenshot on monkey completion")
private boolean mScreenshot = false;
+ @Option(name = "ignore-security-exceptions",
+ description = "Ignore SecurityExceptions while injecting events")
+ private boolean mIgnoreSecurityExceptions = true;
+
private ITestDevice mTestDevice = null;
private MonkeyLogItem mMonkeyLog = null;
private BugreportItem mBugreport = null;
@@ -281,7 +287,11 @@
StringBuilder outputBuilder = new StringBuilder();
CommandHelper commandHelper = new CommandHelper();
+
+ long start = System.currentTimeMillis();
long duration = 0;
+ Date dateAfter = null;
+ String uptimeAfter = NULL_UPTIME;
// Generate the monkey log prefix, which includes the device uptime
outputBuilder.append(String.format("# %s - device uptime = %s: Monkey command used " +
@@ -289,31 +299,33 @@
try {
onMonkeyStart();
- long start = System.currentTimeMillis();
commandHelper.runCommand(mTestDevice, command, getMonkeyTimeoutMs());
- duration = System.currentTimeMillis() - start;
} finally {
- onMonkeyFinish();
- outputBuilder.append(commandHelper.getOutput());
-
- // Generate the monkey log suffix, which includes the device uptime.
- outputBuilder.append(String.format("\n# %s - device uptime = %s: Monkey command ran " +
- "for: %d:%02d (mm:ss)\n", new Date().toString(), getUptime(),
- duration / 1000 / 60, duration / 1000 % 60));
-
// Wait for device to recover if it's not online. If it hasn't recovered, ignore.
try {
mTestDevice.waitForDeviceOnline(2 * 60 * 1000);
- } catch (DeviceNotAvailableException e) {
- CLog.w("Device %s not available after 2 minutes.", mTestDevice.getSerialNumber());
+ duration = System.currentTimeMillis() - start;
+ dateAfter = new Date();
+ uptimeAfter = getUptime();
+ onMonkeyFinish();
+ takeScreenshot(listener, "screenshot");
+
+ mBugreport = takeBugreport(listener, BUGREPORT_NAME);
+ // FIXME: Remove this once traces.txt is no longer needed.
+ takeTraces(listener);
+ } finally {
+ // @@@ DO NOT add anything that requires device interaction into this block @@@
+ // @@@ logging that no longer requires device interaction MUST be in this block @@@
+ outputBuilder.append(commandHelper.getOutput());
+ if (dateAfter == null) {
+ dateAfter = new Date();
+ }
+ // Generate the monkey log suffix, which includes the device uptime.
+ outputBuilder.append(String.format("\n# %s - device uptime = %s: Monkey command "
+ + "ran for: %d:%02d (mm:ss)\n", dateAfter.toString(), uptimeAfter,
+ duration / 1000 / 60, duration / 1000 % 60));
+ mMonkeyLog = createMonkeyLog(listener, MONKEY_LOG_NAME, outputBuilder.toString());
}
-
- takeScreenshot(listener, "screenshot");
-
- mBugreport = takeBugreport(listener, BUGREPORT_NAME);
- // FIXME: Remove this once traces.txt is no longer needed.
- takeTraces(listener);
- mMonkeyLog = createMonkeyLog(listener, MONKEY_LOG_NAME, outputBuilder.toString());
}
checkResults();
@@ -423,7 +435,9 @@
cmdList.add(cat);
}
- cmdList.add("--ignore-security-exceptions");
+ if (mIgnoreSecurityExceptions) {
+ cmdList.add("--ignore-security-exceptions");
+ }
if (mThrottle >= 1) {
cmdList.add("--throttle");
@@ -472,8 +486,8 @@
/**
* Get a {@link String} containing the number seconds since the device was booted.
* <p>
- * {@code "0.00"} is returned if the device becomes unresponsive. Used in the monkey log prefix
- * and suffix.
+ * {@code NULL_UPTIME} is returned if the device becomes unresponsive. Used in the monkey log
+ * prefix and suffix.
* </p>
*/
protected String getUptime() {
@@ -496,7 +510,7 @@
CLog.e("Device %s became unresponsive while getting the uptime.",
mTestDevice.getSerialNumber());
}
- return "0.00";
+ return NULL_UPTIME;
}
/**
@@ -560,10 +574,6 @@
* Check the results and return if valid or throw an assertion error if not valid.
*/
private void checkResults() {
- if (!isRetriable()) {
- return;
- }
-
Assert.assertNotNull("Monkey log is null", mMonkeyLog);
Assert.assertNotNull("Bugreport is null", mBugreport);
Assert.assertNotNull("Bugreport is empty", mBugreport.getTime());
@@ -603,4 +613,4 @@
protected long getMonkeyTimeoutMs() {
return mMonkeyTimeout * 60 * 1000;
}
-}
+}
\ No newline at end of file
diff --git a/prod-tests/src/com/android/monkey/MonkeyBrillopadForwarder.java b/prod-tests/src/com/android/monkey/MonkeyBrillopadForwarder.java
index a36f3e9..a2f1516 100644
--- a/prod-tests/src/com/android/monkey/MonkeyBrillopadForwarder.java
+++ b/prod-tests/src/com/android/monkey/MonkeyBrillopadForwarder.java
@@ -135,12 +135,12 @@
if (!status.equals(MonkeyStatus.FINISHED)) {
String failure = String.format("%s.\n%s", status.getDescription(),
crashTrace.toString());
- super.testFailed(TestFailure.FAILURE, monkeyTest, failure);
+ super.testFailed(monkeyTest, failure);
}
} catch (AssertionError e) {
- super.testFailed(TestFailure.FAILURE, monkeyTest, Throwables.getStackTraceAsString(e));
+ super.testFailed(monkeyTest, Throwables.getStackTraceAsString(e));
} catch (RuntimeException e) {
- super.testFailed(TestFailure.ERROR, monkeyTest, Throwables.getStackTraceAsString(e));
+ super.testFailed(monkeyTest, Throwables.getStackTraceAsString(e));
} finally {
super.testEnded(monkeyTest, monkeyMetrics);
}
diff --git a/prod-tests/src/com/android/monkey/MonkeyPackageDiff.java b/prod-tests/src/com/android/monkey/MonkeyPackageDiff.java
deleted file mode 100644
index 49ff4bf..0000000
--- a/prod-tests/src/com/android/monkey/MonkeyPackageDiff.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.monkey;
-
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
-import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.Option.Importance;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.ByteArrayInputStreamSource;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.testtype.IDeviceTest;
-import com.android.tradefed.testtype.IRemoteTest;
-import com.google.common.base.Joiner;
-
-import junit.framework.Assert;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Compares the list of packages the monkey can run against a golden file and fails on diffs.
- */
-public class MonkeyPackageDiff implements IDeviceTest, IRemoteTest {
- private static final String TEST_KEY = "MonkeyPackageDiff";
- private static final String MONKEY_CMD = "monkey -v -v -v %s0";
- private static final Pattern PACKAGE_PATTERN = Pattern.compile(
- "^//\\s+\\+ [^\\(]+\\(from package ([^\\)]+)\\)$");
-
- @Option(name = "category", description = "Monkey app category. May be repeated.")
- private Collection<String> mCategories = new LinkedList<String>();
-
- @Option(name = "golden-file", description = "The golden file containing the list of packages",
- importance = Importance.ALWAYS, mandatory = true)
- private File mGoldenFile = null;
-
- @Option(name = "ignore-package", description = "Package name to ignore. May be repeated.")
- private Collection<String> mIgnoreList = new HashSet<String>();
-
- ITestDevice mDevice = null;
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
- Assert.assertNotNull(getDevice());
-
- Set<String> expectedPackages = null;
- try {
- expectedPackages = readGoldenFile();
- } catch (IOException e) {
- Assert.fail("Could not read golden file");
- }
-
- StringBuilder categories = new StringBuilder();
- for (String category : mCategories) {
- categories.append("-c ").append(category).append(" ");
- }
- String cmd = String.format(MONKEY_CMD, categories.toString());
- String output = getDevice().executeShellCommand(cmd);
-
- SortedSet<String> actualPackages = new TreeSet<String>();
- for (String line : output.split("\n")) {
- line = line.trim();
- Matcher m = PACKAGE_PATTERN.matcher(line);
- if (m.matches()) {
- actualPackages.add(m.group(1));
- }
- }
-
- StringBuilder packageList = new StringBuilder();
- for (String pack : actualPackages) {
- packageList.append(pack).append("\n");
- }
- listener.testLog("packages", LogDataType.TEXT,
- new ByteArrayInputStreamSource(packageList.toString().getBytes()));
-
- SortedSet<String> addedPackages = new TreeSet<String>();
- for (String pack : actualPackages) {
- if (!expectedPackages.contains(pack) && !mIgnoreList.contains(pack)) {
- addedPackages.add(pack);
- }
- }
- SortedSet<String> removedPackages = new TreeSet<String>();
- for (String pack : expectedPackages) {
- if (!actualPackages.contains(pack) && !mIgnoreList.contains(pack)) {
- removedPackages.add(pack);
- }
- }
-
- reportMetrics(listener, addedPackages, removedPackages);
- }
-
- /**
- * Report the metrics and fail the tests if any packages are added or removed.
- */
- private void reportMetrics(ITestInvocationListener listener, Set<String> addedPackages,
- Set<String> removedPackages) {
- listener.testRunStarted(TEST_KEY, 0);
- Map<String, String> metrics = new HashMap<String, String>();
- Map<String, String> emptyMap = Collections.emptyMap();
-
- TestIdentifier testId = new TestIdentifier(getClass().getCanonicalName(), "added");
- listener.testStarted(testId);
- metrics.put("added", Integer.toString(addedPackages.size()));
- if (!addedPackages.isEmpty()) {
- String message = String.format("Added packages: %s",
- Joiner.on(", ").join(addedPackages));
- listener.testFailed(TestFailure.FAILURE, testId, message);
- }
- listener.testEnded(testId, emptyMap);
-
- testId = new TestIdentifier(getClass().getCanonicalName(), "removed");
- listener.testStarted(testId);
- metrics.put("removed", Integer.toString(removedPackages.size()));
- if (!removedPackages.isEmpty()) {
- String message = String.format("Removed packages: %s",
- Joiner.on(", ").join(removedPackages));
- listener.testFailed(TestFailure.FAILURE, testId, message);
- }
- listener.testEnded(testId, emptyMap);
-
- CLog.d("About to report monkey package diff metrics: %s", metrics);
- listener.testRunEnded(0, metrics);
- }
-
- /**
- * Read the golden file and return a set of strings.
- */
- private Set<String> readGoldenFile() throws IOException {
- Set<String> packages = new HashSet<String>();
- BufferedReader reader = new BufferedReader(new FileReader(mGoldenFile));
- String line;
- try {
- while ((line = reader.readLine()) != null) {
- line = line.trim();
- if (!"".equals(line)) {
- packages.add(line);
- }
- }
- } finally {
- reader.close();
- }
- return packages;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void setDevice(ITestDevice device) {
- mDevice = device;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public ITestDevice getDevice() {
- return mDevice;
- }
-
-}
diff --git a/prod-tests/src/com/android/performance/tests/AppLaunchMetricsTest.java b/prod-tests/src/com/android/performance/tests/AppLaunchMetricsTest.java
deleted file mode 100644
index fc2bbba..0000000
--- a/prod-tests/src/com/android/performance/tests/AppLaunchMetricsTest.java
+++ /dev/null
@@ -1,395 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.performance.tests;
-
-import com.android.ddmlib.IDevice;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
-import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.SnapshotInputStreamSource;
-import com.android.tradefed.testtype.IDeviceTest;
-import com.android.tradefed.testtype.IRemoteTest;
-import com.android.tradefed.util.RunUtil;
-import com.android.tradefed.util.StreamUtil;
-
-import junit.framework.Assert;
-import junit.framework.TestCase;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Runs the app launch test.
- * <p>
- * Launches each app and records the amount of time it took to launch the app.
- * </p>
- */
-public class AppLaunchMetricsTest implements IDeviceTest, IRemoteTest {
- private static final String APP_LAUNCH = "/data/framework/app_launch";
- private static final String APP_LIST_FILE = "launch_list.txt";
- private static final String APP_OUTPUT_FILE = "launch_perf_output.txt";
-
- private static final String TEST_KEY = "ApplicationStartupTime";
-
- private static final String LAUNCH_TIME_NAME = "app_launch_times";
- private static final String BUGREPORT_NAME = "app_launch_bugreport";
-
- /** The pattern to match the app-name argument */
- private static final Pattern APP_NAME_PATTERN = Pattern.compile("(.+),(.+)");
- /** The pattern of the output */
- private static final Pattern APP_TIME_PATTERN = Pattern.compile("(.+)\\|(\\d+)");
-
- private ITestDevice mTestDevice;
- private String mAppListPath = null;
- private String mAppOutputPath = null;
-
- @Option(name = "app-name", description = "The name of the app in the launcher or an app, key "
- + "pair. E.G. \"Browser\" or \"Browser,android_browser\". May be repeated.")
- private Collection<String> mAppNames = new ArrayList<String>();
-
- /**
- * Class that stores useful info about the app.
- */
- static class AppInfo {
- private String mName = null;
- private String mOutputKey = null;
- private String mPostKey = null;
- private Integer mTime = null;
-
- public AppInfo(String name) {
- mName = name;
- mPostKey = mOutputKey = makeOutputKey(name);
- }
-
- public AppInfo(String name, String key) {
- mName = name;
- mOutputKey = makeOutputKey(name);
- mPostKey = key;
- }
-
- public String getAppListEntry() {
- return String.format("%s,%s\n", mName, mPostKey);
- }
-
- public String getName() {
- return mName;
- }
-
- public String getPostKey() {
- return mPostKey;
- }
-
- public String getOutputKey() {
- return mOutputKey;
- }
-
- public Integer getTime() {
- return mTime;
- }
-
- public void setTime(Integer time) {
- mTime = time;
- }
-
- private String makeOutputKey(String name) {
- return name.toLowerCase().replaceAll(" ", "");
- }
- }
-
- private Map<String, AppInfo> mAppInfos = new HashMap<String, AppInfo>();
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
- Assert.assertNotNull(mTestDevice);
-
- mAppListPath = new File(mTestDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE),
- APP_LIST_FILE).getAbsolutePath();
- mAppOutputPath = new File(mTestDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE),
- APP_OUTPUT_FILE).getAbsolutePath();
-
- setupAppInfos();
-
- // Setup the device
- mTestDevice.executeShellCommand(String.format("rm %s %s", mAppListPath, mAppOutputPath));
- mTestDevice.pushString(generateAppList(), mAppListPath);
- mTestDevice.executeShellCommand(String.format("chmod 750 %s", APP_LAUNCH));
-
- // Sleep 30 seconds to let device settle.
- RunUtil.getDefault().sleep(30 * 1000);
-
- // Run the test
- String output = mTestDevice.executeShellCommand(APP_LAUNCH);
-
- CLog.d("App launch output: %s", output);
- logOutputFile(listener);
- }
-
- /**
- * Sets up the {@link AppInfo} map based on the app-name args.
- * <p>
- * Generates from {@link appNames}, a collection of Strings formated as either an app name or as
- * an app name, key pair with a comma separator. The key for the map will be the lowercase name
- * with spaces removed.
- * </p>
- */
- private void setupAppInfos() {
- for (String app : mAppNames) {
- Matcher m = APP_NAME_PATTERN.matcher(app);
- AppInfo info;
- if (m.matches()) {
- info = new AppInfo(m.group(1), m.group(2));
- } else {
- info = new AppInfo(app);
- }
- mAppInfos.put(info.getOutputKey(), info);
- }
- }
-
- /**
- * Generate the app list as a String.
- *
- * @return the app list to push to the device.
- */
- private String generateAppList() {
- StringBuilder sb = new StringBuilder();
- for (AppInfo info : mAppInfos.values()) {
- sb.append(info.getAppListEntry());
- }
- return sb.toString();
- }
-
- /**
- * Parses and logs the output file.
- *
- * @param listener the {@link ITestInvocationListener}
- * @throws DeviceNotAvailableException If the device is not available.
- */
- private void logOutputFile(ITestInvocationListener listener)
- throws DeviceNotAvailableException {
- File outputFile = null;
- InputStreamSource outputSource = null;
-
- try {
- outputFile = mTestDevice.pullFile(mAppOutputPath);
- if (outputFile != null) {
- outputSource = new SnapshotInputStreamSource(new FileInputStream(outputFile));
- listener.testLog(LAUNCH_TIME_NAME, LogDataType.TEXT, outputSource);
- parseOutputFile(StreamUtil.getStringFromStream(new BufferedInputStream(
- new FileInputStream(outputFile))));
- }
- } catch(IOException e) {
- CLog.e("Got IOException: %s", e);
- } finally {
- if (outputFile != null) {
- outputFile.delete();
- }
- if (outputSource != null) {
- outputSource.cancel();
- }
- }
-
- if (shouldTakeBugreport()) {
- InputStreamSource bugreport = mTestDevice.getBugreport();
- try {
- listener.testLog(BUGREPORT_NAME, LogDataType.TEXT, bugreport);
- } finally {
- bugreport.cancel();
- }
- }
-
- reportMetrics(listener);
- }
-
- /**
- * Parses the output file and populate the {@link AppInfo} objects with the launch times.
- *
- * @param contents The file contents.
- * @throws IOException If an IOException is caused.
- */
- private void parseOutputFile(String contents) throws IOException {
- for (String line : contents.split("\n")) {
- Matcher m = APP_TIME_PATTERN.matcher(line);
- if (m.matches()) {
- AppInfo appInfo = mAppInfos.get(m.group(1).toLowerCase());
- if (appInfo != null) {
- appInfo.setTime(Integer.parseInt(m.group(2)));
- }
- }
- }
- }
-
- /**
- * Report the metrics and attach it to the listener.
- * <p>
- * If any of the app times are {@code null}, that app is assumed to not have launched and will
- * be marked as failed.
- * </p>
- * @param listener the {@link ITestInvocationListener}
- */
- private void reportMetrics(ITestInvocationListener listener) {
- listener.testRunStarted(TEST_KEY, 0);
- Map<String, String> metrics = new HashMap<String, String>();
-
- for (AppInfo appInfo : mAppInfos.values()) {
- TestIdentifier testId = new TestIdentifier(getClass().getCanonicalName(),
- appInfo.getPostKey());
- listener.testStarted(testId);
- if (appInfo.getTime() != null) {
- metrics.put(appInfo.getPostKey(), Integer.toString(appInfo.getTime()));
- } else {
- listener.testFailed(TestFailure.FAILURE, testId, "No app launch time");
- }
- Map<String, String> empty = Collections.emptyMap();
- listener.testEnded(testId, empty);
- }
- CLog.d("About to report app launch metrics: %s", metrics);
- listener.testRunEnded(0, metrics);
- }
-
- /**
- * If a bugreport should be taken after the run.
- *
- * @return true if any of the apps have a {@code null} launch time.
- */
- private boolean shouldTakeBugreport() {
- for (AppInfo appInfo : mAppInfos.values()) {
- if (appInfo.getTime() == null) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void setDevice(ITestDevice device) {
- mTestDevice = device;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public ITestDevice getDevice() {
- return mTestDevice;
- }
-
- public static class MetaTest extends TestCase {
- AppLaunchMetricsTest mTestInstance = null;
-
- @Override
- public void setUp() throws Exception {
- mTestInstance = new AppLaunchMetricsTest();
-
- mTestInstance.mAppNames.add("App 1");
- mTestInstance.mAppNames.add("App 2,key2");
- }
-
- public void testAppInfo() throws Exception {
- AppInfo info = new AppInfo("app_name");
- assertEquals("app_name", info.getName());
- assertEquals("app_name", info.getOutputKey());
- assertEquals("app_name", info.getPostKey());
-
- info = new AppInfo("AppName");
- assertEquals("AppName", info.getName());
- assertEquals("appname", info.getOutputKey());
- assertEquals("appname", info.getPostKey());
-
- info = new AppInfo("App Name");
- assertEquals("App Name", info.getName());
- assertEquals("appname", info.getOutputKey());
- assertEquals("appname", info.getPostKey());
-
- info = new AppInfo("App & Name");
- assertEquals("App & Name", info.getName());
- assertEquals("app&name", info.getOutputKey());
- assertEquals("app&name", info.getPostKey());
- assertEquals("App & Name,app&name\n", info.getAppListEntry());
-
- info = new AppInfo("App Name", "key");
- assertEquals("App Name", info.getName());
- assertEquals("appname", info.getOutputKey());
- assertEquals("key", info.getPostKey());
-
- assertNull(info.getTime());
- info.setTime(0);
- assertEquals(new Integer(0), info.getTime());
- assertEquals("App Name,key\n", info.getAppListEntry());
- }
-
- public void testSetupAppInfos() throws Exception {
- mTestInstance.setupAppInfos();
- assertEquals(2, mTestInstance.mAppInfos.size());
- assertNotNull(mTestInstance.mAppInfos.get("app1"));
- assertEquals("App 1", mTestInstance.mAppInfos.get("app1").getName());
- assertEquals("app1", mTestInstance.mAppInfos.get("app1").getOutputKey());
- assertEquals("app1", mTestInstance.mAppInfos.get("app1").getPostKey());
- assertNotNull(mTestInstance.mAppInfos.get("app2"));
- assertEquals("App 2", mTestInstance.mAppInfos.get("app2").getName());
- assertEquals("app2", mTestInstance.mAppInfos.get("app2").getOutputKey());
- assertEquals("key2", mTestInstance.mAppInfos.get("app2").getPostKey());
- }
-
- public void testGenerateAppList() throws Exception {
- mTestInstance.setupAppInfos();
- assertEquals(2, mTestInstance.mAppInfos.size());
-
- assertTrue(mTestInstance.generateAppList().contains("App 1,app1\n"));
- assertTrue(mTestInstance.generateAppList().contains("App 2,key2\n"));
- }
-
- public void testParseOutputFile_success() throws Exception {
- mTestInstance.setupAppInfos();
- assertEquals(2, mTestInstance.mAppInfos.size());
-
- mTestInstance.parseOutputFile("app1|1234\napp2|5678\n");
- assertFalse(mTestInstance.shouldTakeBugreport());
- assertEquals(new Integer(1234), mTestInstance.mAppInfos.get("app1").getTime());
- assertEquals(new Integer(5678), mTestInstance.mAppInfos.get("app2").getTime());
- }
-
- public void testParseOutputFile_fail() throws Exception {
- mTestInstance.setupAppInfos();
- assertEquals(2, mTestInstance.mAppInfos.size());
-
- mTestInstance.parseOutputFile("app1|1234\n");
- assertTrue(mTestInstance.shouldTakeBugreport());
- assertEquals(new Integer(1234), mTestInstance.mAppInfos.get("app1").getTime());
- assertNull(mTestInstance.mAppInfos.get("app2").getTime());
- }
- }
-}
diff --git a/prod-tests/src/com/android/performance/tests/FioBenchmarkTest.java b/prod-tests/src/com/android/performance/tests/FioBenchmarkTest.java
index 99d0f1b..994c4f1 100644
--- a/prod-tests/src/com/android/performance/tests/FioBenchmarkTest.java
+++ b/prod-tests/src/com/android/performance/tests/FioBenchmarkTest.java
@@ -377,6 +377,10 @@
description="The number of worker jobs for the media server benchmark.")
private int mMediaScannerWorkerJobCount = 4;
+ @Option(name="key-suffix",
+ description="The suffix to add to the reporting key in order to override the default")
+ private String mKeySuffix = null;
+
/**
* Sets up all the benchmarks.
*/
@@ -811,6 +815,7 @@
mTestDevice.executeShellCommand(String.format("rm -r %s", mTmpDir));
mTestDevice.executeShellCommand(String.format("rm -r %s", mFioDir));
mTestDevice.executeShellCommand("start");
+ mTestDevice.waitForDeviceAvailable();
}
/**
@@ -859,7 +864,9 @@
// Report metrics
Map<String, String> metrics = new HashMap<String, String>();
- listener.testRunStarted(test.mKey, 0);
+ String key = mKeySuffix == null ? test.mKey : test.mKey + mKeySuffix;
+
+ listener.testRunStarted(key, 0);
for (PerfMetricInfo m : test.mPerfMetrics) {
if (!output.mResults.containsKey(m.mJobName)) {
CLog.w("Job name %s was not found in the results", m.mJobName);
@@ -873,7 +880,8 @@
CLog.w("%s was not in results for the job %s", m.mFieldName, m.mJobName);
}
}
- CLog.d("About to report metrics to %s: %s", test.mKey, metrics);
+
+ CLog.d("About to report metrics to %s: %s", key, metrics);
listener.testRunEnded(0, metrics);
}
diff --git a/prod-tests/src/com/android/performance/tests/GLBenchmarkTest.java b/prod-tests/src/com/android/performance/tests/GLBenchmarkTest.java
index 5d44f78..a2647e1 100644
--- a/prod-tests/src/com/android/performance/tests/GLBenchmarkTest.java
+++ b/prod-tests/src/com/android/performance/tests/GLBenchmarkTest.java
@@ -16,7 +16,6 @@
package com.android.performance.tests;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
@@ -195,7 +194,7 @@
}
if (errMsg != null) {
CLog.e(errMsg);
- listener.testFailed(TestFailure.FAILURE, testId, errMsg);
+ listener.testFailed(testId, errMsg);
listener.testEnded(testId, metrics);
listener.testRunFailed(errMsg);
} else {
diff --git a/prod-tests/src/com/android/performance/tests/GeekbenchTest.java b/prod-tests/src/com/android/performance/tests/GeekbenchTest.java
index c3d2a8e..a23da0e 100644
--- a/prod-tests/src/com/android/performance/tests/GeekbenchTest.java
+++ b/prod-tests/src/com/android/performance/tests/GeekbenchTest.java
@@ -17,7 +17,6 @@
package com.android.performance.tests;
import com.android.ddmlib.CollectingOutputReceiver;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
@@ -142,7 +141,7 @@
if (errMsg != null) {
CLog.e(errMsg);
- listener.testFailed(TestFailure.FAILURE, testId, errMsg);
+ listener.testFailed(testId, errMsg);
listener.testEnded(testId, metrics);
listener.testRunFailed(errMsg);
} else {
diff --git a/prod-tests/src/com/android/performance/tests/StartupMetricsTest.java b/prod-tests/src/com/android/performance/tests/StartupMetricsTest.java
index 8f89c7e..86cc149 100644
--- a/prod-tests/src/com/android/performance/tests/StartupMetricsTest.java
+++ b/prod-tests/src/com/android/performance/tests/StartupMetricsTest.java
@@ -47,7 +47,7 @@
public static final String BUGREPORT_LOG_NAME = "bugreport_startup.txt";
@Option(name="boot-time-ms", description="Timeout in ms to wait for device to boot.")
- private long mBootTimeMs = 5 * 60 * 1000;
+ private long mBootTimeMs = 20 * 60 * 1000;
@Option(name="boot-poll-time-ms", description="Delay in ms between polls for device to boot.")
private long mBootPoolTimeMs = 500;
@@ -78,6 +78,10 @@
*/
void executeRebootTest(ITestInvocationListener listener) throws DeviceNotAvailableException {
Map<String, String> runMetrics = new HashMap<String, String>();
+ String upTimeString = mTestDevice.executeShellCommand("cat /proc/uptime");
+ upTimeString = upTimeString.split(" ")[0];
+ assert(Double.parseDouble(upTimeString) > 0);
+ runMetrics.put("init-boot", upTimeString);
mTestDevice.setRecoveryMode(RecoveryMode.NONE);
CLog.d("Reboot test start.");
mTestDevice.nonBlockingReboot();
diff --git a/prod-tests/src/com/android/performance/tests/VellamoBenchmark.java b/prod-tests/src/com/android/performance/tests/VellamoBenchmark.java
index 419925d..0da7637 100644
--- a/prod-tests/src/com/android/performance/tests/VellamoBenchmark.java
+++ b/prod-tests/src/com/android/performance/tests/VellamoBenchmark.java
@@ -14,7 +14,6 @@
package com.android.performance.tests;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
@@ -140,7 +139,7 @@
}
if (errMsg != null) {
CLog.e(errMsg);
- listener.testFailed(TestFailure.FAILURE, testId, errMsg);
+ listener.testFailed(testId, errMsg);
}
long durationMs = System.currentTimeMillis() - testStartTime;
metrics.put("total", Double.toString(sumScore));
diff --git a/prod-tests/src/com/android/sdk/EmulatorBootTest.java b/prod-tests/src/com/android/sdk/EmulatorBootTest.java
index 677c7dc..86db8f0 100644
--- a/prod-tests/src/com/android/sdk/EmulatorBootTest.java
+++ b/prod-tests/src/com/android/sdk/EmulatorBootTest.java
@@ -16,7 +16,7 @@
package com.android.sdk;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
+import com.android.ddmlib.Log;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.sdk.tests.EmulatorGpsPreparer;
import com.android.sdk.tests.EmulatorSmsPreparer;
@@ -26,6 +26,7 @@
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.targetprep.BuildError;
import com.android.tradefed.targetprep.SdkAvdPreparer;
@@ -102,17 +103,19 @@
mAvdPreparer.setUp(mDevice, mBuildInfo);
mSmsPreparer.setUp(mDevice, mBuildInfo);
mGpsPreparer.setUp(mDevice, mBuildInfo);
+
+ checkLauncherRunningOnEmulator(mDevice);
}
catch(BuildError b) {
- listener.testFailed(TestFailure.ERROR, bootTest, StreamUtil.getStackTrace(b));
+ listener.testFailed(bootTest, StreamUtil.getStackTrace(b));
// throw exception to prevent other tests from executing needlessly
throw new DeviceUnresponsiveException("The emulator failed to boot", b);
}
catch(RuntimeException e) {
- listener.testFailed(TestFailure.ERROR, bootTest, StreamUtil.getStackTrace(e));
+ listener.testFailed(bootTest, StreamUtil.getStackTrace(e));
throw e;
} catch (TargetSetupError e) {
- listener.testFailed(TestFailure.ERROR, bootTest, StreamUtil.getStackTrace(e));
+ listener.testFailed(bootTest, StreamUtil.getStackTrace(e));
throw new RuntimeException(e);
}
finally {
@@ -120,4 +123,27 @@
listener.testRunEnded(0, new HashMap<String,String>());
}
}
+
+ private void checkLauncherRunningOnEmulator(ITestDevice device) throws BuildError, DeviceNotAvailableException {
+ Integer apiLevel = device.getApiLevel();
+ String cmd = "ps";
+ if (apiLevel >= 21) {
+ cmd = "am stack list";
+ } else if (apiLevel == 19) {
+ cmd = "am stack boxes";
+ }
+ String cmdResult = device.executeShellCommand(cmd);
+ CLog.i("%s on device %s is %s", cmd, mDevice.getSerialNumber(), cmdResult);
+
+ String[] cmdResultLines = cmdResult.split("\n");
+ int i;
+ for(i = 0; i < cmdResultLines.length; ++i) {
+ if (cmdResultLines[i].contains("com.android.launcher")) {
+ break;
+ }
+ }
+ if(i == cmdResultLines.length) {
+ throw new BuildError("The emulator do not have launcher run");
+ }
+ }
}
diff --git a/prod-tests/src/com/android/sdk/tests/SdkTestAppTest.java b/prod-tests/src/com/android/sdk/tests/SdkTestAppTest.java
index 6978174..6996bdd 100644
--- a/prod-tests/src/com/android/sdk/tests/SdkTestAppTest.java
+++ b/prod-tests/src/com/android/sdk/tests/SdkTestAppTest.java
@@ -15,7 +15,6 @@
*/
package com.android.sdk.tests;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.ISdkBuildInfo;
@@ -182,10 +181,10 @@
runTestAppTest(target, testAppDir, isLibrary);
} catch (AssertionError e) {
CLog.w("%s failed. %s", testId, e);
- listener.testFailed(TestFailure.FAILURE, testId, getThrowableTraceAsString(e));
+ listener.testFailed(testId, getThrowableTraceAsString(e));
} catch (Throwable t) {
CLog.w("%s failed. %s", testId, t);
- listener.testFailed(TestFailure.ERROR, testId, getThrowableTraceAsString(t));
+ listener.testFailed(testId, getThrowableTraceAsString(t));
}
listener.testEnded(testId, Collections.EMPTY_MAP);
}
diff --git a/prod-tests/src/com/android/security/tests/SELinuxDenialsTests.java b/prod-tests/src/com/android/security/tests/SELinuxDenialsTests.java
index 117157e..b14ad71 100644
--- a/prod-tests/src/com/android/security/tests/SELinuxDenialsTests.java
+++ b/prod-tests/src/com/android/security/tests/SELinuxDenialsTests.java
@@ -52,7 +52,7 @@
public class SELinuxDenialsTests implements IRemoteTest, IDeviceTest {
private ITestDevice mDevice;
- private static final String ADB_SHELL_KERNEL_LOGS_CMD = "su -c dmesg";
+ private static final String ADB_SHELL_KERNEL_LOGS_CMD = "dmesg";
private static final String DMESG_OUTPUT_FILE_NAME = "dmesg_output";
@Option(name="selinux-domains-file",
diff --git a/prod-tests/src/com/android/sensor/tests/SingleSensorTests.java b/prod-tests/src/com/android/sensor/tests/SingleSensorTests.java
index c6389dc..aedf1bc 100644
--- a/prod-tests/src/com/android/sensor/tests/SingleSensorTests.java
+++ b/prod-tests/src/com/android/sensor/tests/SingleSensorTests.java
@@ -77,7 +77,7 @@
String rawFileList = getDevice().executeShellCommand(
String.format("ls ${EXTERNAL_STORAGE}/%s*", mOutputPrefix));
if (rawFileList != null && !rawFileList.contains("No such file or directory")) {
- String[] filePaths = rawFileList.split("\r\n");
+ String[] filePaths = rawFileList.split("\r?\n");
for (String filePath : filePaths) {
pullFile(listener, filePath);
}
diff --git a/prod-tests/src/com/android/wireless/tests/ConnectivityManagerTest.java b/prod-tests/src/com/android/wireless/tests/ConnectivityManagerTest.java
index c2f4b0f..36ba4ed 100644
--- a/prod-tests/src/com/android/wireless/tests/ConnectivityManagerTest.java
+++ b/prod-tests/src/com/android/wireless/tests/ConnectivityManagerTest.java
@@ -21,6 +21,7 @@
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.BugreportCollector;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.testtype.IDeviceTest;
@@ -39,7 +40,6 @@
public class ConnectivityManagerTest implements IRemoteTest, IDeviceTest {
private ITestDevice mTestDevice = null;
- private static long START_TIMER = 5 * 60 * 1000; //5 minutes
// Define instrumentation test package and runner.
private static final String TEST_PACKAGE_NAME =
"com.android.connectivitymanagertest";
@@ -49,8 +49,6 @@
String.format("%s.functional.ConnectivityManagerMobileTest", TEST_PACKAGE_NAME);
private static final int TEST_TIMER = 60 * 60 * 1000; // 1 hour
- private RadioHelper mRadioHelper;
-
@Option(name="ssid", description="The ssid used for wifi connection.")
private String mSsid = null;
@@ -63,6 +61,9 @@
@Option(name="wifi-only")
private boolean mWifiOnly = false;
+ @Option(name="start-sleep", description="The amount of time to sleep before starting in secs.")
+ private int mStartSleepSecs = 5 * 60;
+
@Override
public void setDevice(ITestDevice testDevice) {
mTestDevice = testDevice;
@@ -74,22 +75,22 @@
}
@Override
- public void run(ITestInvocationListener standardListener)
+ public void run(ITestInvocationListener listener)
throws DeviceNotAvailableException {
Assert.assertNotNull(mTestDevice);
Assert.assertNotNull(mSsid);
- mRadioHelper = new RadioHelper(mTestDevice);
- RunUtil.getDefault().sleep(START_TIMER);
+
+ CLog.d("Sleeping for %d secs", mStartSleepSecs);
+ RunUtil.getDefault().sleep(mStartSleepSecs * 1000);
+
if (!mWifiOnly) {
- // capture a bugreport if activation or data setup failed
- if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
- mRadioHelper.getBugreport(standardListener);
- return;
- }
+ final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+ Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+ Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
}
+
// Add bugreport listener for bugreport after each test case fails
- BugreportCollector bugListener = new
- BugreportCollector(standardListener, mTestDevice);
+ BugreportCollector bugListener = new BugreportCollector(listener, mTestDevice);
bugListener.addPredicate(BugreportCollector.AFTER_FAILED_TESTCASES);
bugListener.setDescriptiveName("connectivity_manager_test");
// Device may reboot during the test, to capture a bugreport after that,
diff --git a/prod-tests/src/com/android/wireless/tests/RadioHelper.java b/prod-tests/src/com/android/wireless/tests/RadioHelper.java
index 1928f5f..68f88c6 100644
--- a/prod-tests/src/com/android/wireless/tests/RadioHelper.java
+++ b/prod-tests/src/com/android/wireless/tests/RadioHelper.java
@@ -18,9 +18,6 @@
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
@@ -52,14 +49,14 @@
* Get phone type 0 - None, 1 - GSM, 2 - CDMA
*/
private String getPhoneType() throws DeviceNotAvailableException {
- return mDevice.getPropertySync("gsm.current.phone-type");
+ return mDevice.getProperty("gsm.current.phone-type");
}
/**
* Get sim state
*/
private String getSimState() throws DeviceNotAvailableException {
- return mDevice.getPropertySync("gsm.sim.state");
+ return mDevice.getProperty("gsm.sim.state");
}
/**
@@ -155,8 +152,8 @@
/**
* Wait for device data setup
- * @return true if data setup succeeded
- * @return false if data setup failed
+ *
+ * @return true if data setup succeeded, false otherwise
*/
public boolean waitForDataSetup() throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
@@ -168,13 +165,4 @@
}
return false;
}
-
- // capture a bugreport
- public void getBugreport(ITestInvocationListener listener)
- throws DeviceNotAvailableException {
- CLog.d("Capture a bugreport");
- InputStreamSource bugreport = mDevice.getBugreport();
- listener.testLog("bugreport", LogDataType.BUGREPORT, bugreport);
- bugreport.cancel();
- }
}
diff --git a/prod-tests/src/com/android/wireless/tests/SmsStressTest.java b/prod-tests/src/com/android/wireless/tests/SmsStressTest.java
index ba1da84..2171f64 100644
--- a/prod-tests/src/com/android/wireless/tests/SmsStressTest.java
+++ b/prod-tests/src/com/android/wireless/tests/SmsStressTest.java
@@ -15,10 +15,10 @@
*/
package com.android.wireless.tests;
-import com.android.ddmlib.IDevice;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
@@ -47,56 +47,51 @@
* Run the Sms stress test. This test stresses sms message sending and receiving
*/
public class SmsStressTest implements IRemoteTest, IDeviceTest {
- private ITestDevice mTestDevice = null;
+ private static final String TEST_PACKAGE_NAME = "com.android.messagingtests";
+ private static final String TEST_RUNNER_NAME = "android.test.InstrumentationTestRunner";
- // Define instrumentation test package and runner.
- private static final String TEST_PACKAGE_NAME = "com.android.mms.tests";
- private static final String TEST_RUNNER_NAME = "com.android.mms.SmsTestRunner";
- private static final String TEST_CLASS_NAME = "com.android.mms.ui.SmsStressTest";
+ private static final String OUTPUT_PATH = "result.txt";
private static final String ITEM_KEY = "single_thread";
private static final String METRICS_NAME = "sms_stress";
- private static final Pattern MESSAGE_PATTERN =
- Pattern.compile("^send message (\\d+) out of (\\d+)");
+
+ private static final String OUTPUT_PASSED = "passed";
+ private static final String OUTPUT_FAILED = "failed";
+ private static final Pattern OUTPUT_PATTERN = Pattern.compile(
+ String.format("^(\\d+),(%s|%s)$", OUTPUT_PASSED, OUTPUT_FAILED));
+
private static final String INSERT_COMMAND =
"sqlite3 /data/data/com.android.providers.settings/databases/settings.db "
+ "\"INSERT INTO global (name, value) values (\'%s\',\'%s\');\"";
- private String mOutputFile = "result.txt";
- @Option(name="recipient",
- description="The recipient of sms messages")
- private String mRecipient = null;
+ private enum MessagingApp {
+ HANGOUTS ("com.android.messagingtests.stress.HangoutsStressTest"),
+ MESSAGING ("com.android.messagingtests.stress.MessagingStressTest"),
+ MESSENGER ("com.android.messagingtests.stress.MessengerStressTest");
- @Option(name="messages",
- description="The total number of messages to send")
- private int mNumMessages = 100;
+ private final String mTestClass;
- @Option(name="messagefile",
- description="The file to load sending message")
- private String mMessageFile = null;
+ MessagingApp(String testClass) {
+ mTestClass = testClass;
+ }
- @Option(name="recipientfile",
- description="The file to load recipients")
- private String mRecipientFile = null;
-
- @Option(name="receivetimer",
- description="The timer before verifying messages receiption when sending sms"
- + "to the test device itself (s)")
- private int mReceiveTimer = 300;
-
- @Option(name="sendinterval",
- description="The time interval between two consecutive sms.")
- private int mSendInterval = 10;
-
- @Override
- public void setDevice(ITestDevice testDevice) {
- mTestDevice = testDevice;
+ public String getTestClass() {
+ return mTestClass;
+ }
}
- @Override
- public ITestDevice getDevice() {
- return mTestDevice;
- }
+ private ITestDevice mTestDevice = null;
+
+ @Option(name="iterations", description="The total number of iterations to run",
+ importance=Importance.ALWAYS)
+ private int mIterations = 100;
+
+ @Option(name="phone-number", description="The phone number to use")
+ private String mPhoneNumber = null;
+
+ @Option(name="app", description="The default messaging app on the device",
+ importance=Importance.IF_UNSET, mandatory=true)
+ private MessagingApp mApp = null;
/**
* Configure device with special settings
@@ -113,91 +108,84 @@
}
/**
- * Run sms stress test and parse test results
+ * Run messaging stress test and parse test results
*/
@Override
- public void run(ITestInvocationListener standardListener)
+ public void run(ITestInvocationListener listener)
throws DeviceNotAvailableException {
Assert.assertNotNull(mTestDevice);
setupDevice();
- RadioHelper mRadioHelper = new RadioHelper(mTestDevice);
- // Capture a bugreport if activation or data setup failed
- if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
- mRadioHelper.getBugreport(standardListener);
- return;
- }
+
+ final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+ Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+ Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(
TEST_PACKAGE_NAME, TEST_RUNNER_NAME, mTestDevice.getIDevice());
- runner.setClassName(TEST_CLASS_NAME);
- if (mRecipient != null) {
- runner.addInstrumentationArg("recipient", mRecipient);
+ runner.setClassName(mApp.getTestClass());
+ runner.addInstrumentationArg("iterations", Integer.toString(mIterations));
+ if (mPhoneNumber != null) {
+ runner.addInstrumentationArg("phone_number", mPhoneNumber);
}
- if (mMessageFile != null) {
- runner.addInstrumentationArg("messagefile", mMessageFile);
- }
- if (mRecipientFile != null) {
- runner.addInstrumentationArg("messagefile", mMessageFile);
- }
- runner.addInstrumentationArg("messages", Integer.toString(mNumMessages));
- runner.addInstrumentationArg(
- "receivetimer", Integer.toString(mReceiveTimer));
- runner.addInstrumentationArg(
- "sendinterval", Integer.toString(mSendInterval));
+ mTestDevice.runInstrumentationTests(runner, listener);
- mTestDevice.runInstrumentationTests(runner, standardListener);
- logOutputFile(standardListener);
- cleanOutputFiles();
+ InputStreamSource screenshot = mTestDevice.getScreenshot();
+ try {
+ listener.testLog(String.format("screenshot"),
+ LogDataType.PNG, screenshot);
+ } finally {
+ StreamUtil.cancel(screenshot);
+ }
+
+ parseOutput(listener);
}
/**
* Collect test results and report test results.
- *
- * @param listener
*/
- private void logOutputFile(ITestInvocationListener listener)
- throws DeviceNotAvailableException {
- // Capture a bugreport right after the test
- InputStreamSource bugreport = mTestDevice.getBugreport();
- listener.testLog("bugreport", LogDataType.BUGREPORT, bugreport);
- bugreport.cancel();
-
+ private void parseOutput(ITestInvocationListener listener)
+ throws DeviceNotAvailableException {
+ File outputFile = null;
InputStreamSource outputSource = null;
- Map<String, String> runMetrics = new HashMap<String, String>();
- File resFile = null;
- BufferedReader br = null;
- try {
- resFile = mTestDevice.pullFileFromExternal(mOutputFile);
- if (resFile == null) {
- return;
- }
- // Save a copy of the output file
- CLog.d("Sending %d byte file %s into the logosphere!",
- resFile.length(), resFile);
- outputSource = new SnapshotInputStreamSource(new FileInputStream(resFile));
- listener.testLog(mOutputFile, LogDataType.TEXT, outputSource);
+ BufferedReader outputReader = null;
+ Integer iterations = null;
- // Parse the results file and post results to test listener
- br = new BufferedReader(new FileReader(resFile));
- String line = null;
- while ((line = br.readLine()) != null) {
- Matcher match = MESSAGE_PATTERN.matcher(line);
- if (match.matches()) {
- String value = match.group(1);
- CLog.d("iteration: %s", value);
- runMetrics.put(ITEM_KEY, value);
+ try {
+ outputFile = mTestDevice.pullFileFromExternal(OUTPUT_PATH);
+ if (outputFile != null) {
+ CLog.d("Sending %d byte file %s into the logosphere!",
+ outputFile.length(), outputFile);
+ outputSource = new SnapshotInputStreamSource(new FileInputStream(outputFile));
+ listener.testLog("sms_stress_output", LogDataType.TEXT, outputSource);
+
+ outputReader = new BufferedReader(new FileReader(outputFile));
+ String line = null;
+ while ((line = outputReader.readLine()) != null) {
+ Matcher m = OUTPUT_PATTERN.matcher(line);
+ if (m.matches()) {
+ if (OUTPUT_PASSED.equals(m.group(2))) {
+ iterations = Integer.parseInt(m.group(1));
+ }
+ } else {
+ CLog.w("Line '%s' did not match output pattern", line);
+ }
}
}
+
+ Map<String, String> metrics = new HashMap<String, String>();
+ metrics.put(ITEM_KEY, Integer.toString(iterations == null ? 0 : iterations + 1));
+ reportMetrics(METRICS_NAME, listener, metrics);
} catch (IOException e) {
- CLog.e("IOException while reading from data stream: %s", e);
+ CLog.e("IOException parsing output file: %s", e);
+ Assert.fail("IOException parsing output file");
} finally {
- FileUtil.deleteFile(resFile);
+ FileUtil.deleteFile(outputFile);
StreamUtil.cancel(outputSource);
- StreamUtil.close(br);
+ StreamUtil.close(outputReader);
}
- reportMetrics(METRICS_NAME, listener, runMetrics);
}
+
/**
* Report run metrics by creating an empty test run to stick them in
*/
@@ -210,11 +198,18 @@
}
/**
- * Clean up output files from the last test run
+ * {@inheritDoc}
*/
- private void cleanOutputFiles() throws DeviceNotAvailableException {
- CLog.d("Remove output file: %s", mOutputFile);
- String extStore = mTestDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
- mTestDevice.executeShellCommand(String.format("rm %s/%s", extStore, mOutputFile));
+ @Override
+ public void setDevice(ITestDevice testDevice) {
+ mTestDevice = testDevice;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ITestDevice getDevice() {
+ return mTestDevice;
}
}
diff --git a/prod-tests/src/com/android/wireless/tests/TelephonyStabilityTest.java b/prod-tests/src/com/android/wireless/tests/TelephonyStabilityTest.java
index e1704eb..ee36b0a 100644
--- a/prod-tests/src/com/android/wireless/tests/TelephonyStabilityTest.java
+++ b/prod-tests/src/com/android/wireless/tests/TelephonyStabilityTest.java
@@ -106,7 +106,6 @@
private long mScreenTimeoutMin = 30;
private ITestDevice mTestDevice = null;
- private RadioHelper mRadioHelper;
/**
* Run the telephony stability test and collect results
@@ -117,11 +116,10 @@
Assert.assertNotNull(mPhoneNumber);
setScreenTimeout();
- mRadioHelper = new RadioHelper(mTestDevice);
- if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
- mRadioHelper.getBugreport(listener);
- return;
- }
+
+ final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+ Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+ Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(TEST_PACKAGE_NAME,
TEST_RUNNER_NAME, mTestDevice.getIDevice());
@@ -165,6 +163,14 @@
bugreport.cancel();
}
}
+
+ InputStreamSource screenshot = mTestDevice.getScreenshot();
+ try {
+ listener.testLog(String.format("screenshot_%04d", lastBugreportIteration),
+ LogDataType.PNG, screenshot);
+ } finally {
+ StreamUtil.cancel(screenshot);
+ }
}
reportMetrics(listener, metrics);
}
diff --git a/prod-tests/src/com/android/wireless/tests/TelephonyTest.java b/prod-tests/src/com/android/wireless/tests/TelephonyTest.java
index 8fae8e3..2ad9022 100644
--- a/prod-tests/src/com/android/wireless/tests/TelephonyTest.java
+++ b/prod-tests/src/com/android/wireless/tests/TelephonyTest.java
@@ -83,7 +83,6 @@
private long mStartPauseDurationSec = 2;
private ITestDevice mTestDevice = null;
- private RadioHelper mRadioHelper;
/**
* Run the telephony outgoing call stress test
@@ -95,12 +94,9 @@
Assert.assertNotNull(mTestDevice);
Assert.assertNotNull(mPhoneNumber);
- mRadioHelper = new RadioHelper(mTestDevice);
- // wait for data connection
- if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
- mRadioHelper.getBugreport(listener);
- return;
- }
+ final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+ Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+ Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(TEST_PACKAGE_NAME,
TEST_RUNNER_NAME, mTestDevice.getIDevice());
@@ -126,11 +122,15 @@
if (successfulIterations < mIterations) {
mTestDevice.waitForDeviceOnline(30 * 1000);
InputStreamSource bugreport = mTestDevice.getBugreport();
+ InputStreamSource screenshot = mTestDevice.getScreenshot();
try {
listener.testLog(String.format("bugreport_%s", successfulIterations),
LogDataType.BUGREPORT, bugreport);
+ listener.testLog(String.format("screenshot_%s", successfulIterations),
+ LogDataType.PNG, screenshot);
} finally {
- bugreport.cancel();
+ StreamUtil.cancel(bugreport);
+ StreamUtil.cancel(screenshot);
}
}
diff --git a/prod-tests/src/com/android/wireless/tests/VpnTest.java b/prod-tests/src/com/android/wireless/tests/VpnTest.java
index 0598118..66cfcc1 100644
--- a/prod-tests/src/com/android/wireless/tests/VpnTest.java
+++ b/prod-tests/src/com/android/wireless/tests/VpnTest.java
@@ -20,7 +20,6 @@
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.BugreportCollector;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.ITestInvocationListener;
diff --git a/prod-tests/src/com/android/wireless/tests/WifiStressTest.java b/prod-tests/src/com/android/wireless/tests/WifiStressTest.java
index 000cdb6..01d1c5e 100644
--- a/prod-tests/src/com/android/wireless/tests/WifiStressTest.java
+++ b/prod-tests/src/com/android/wireless/tests/WifiStressTest.java
@@ -67,7 +67,6 @@
private static final int RECONNECT_TEST_TIMER = 12 * 60 * 60 * 1000; // 12 hours
private String mOutputFile = "WifiStressTestOutput.txt";
- private RadioHelper mRadioHelper;
/**
* Stores the test cases that we should consider running.
@@ -96,45 +95,46 @@
description="The number of iterations to run soft ap stress test")
private String mApIteration = "100";
- @Option(name="scan-iteration",
- description="The number of iterations to run WiFi scanning test")
- private String mScanIteration = "100";
+ @Option(name="idle-time",
+ description="The device idle time after screen off")
+ private String mIdleTime = "30"; // 30 seconds
@Option(name="reconnect-iteration",
description="The number of iterations to run WiFi reconnection stress test")
private String mReconnectionIteration = "100";
- @Option(name="reconnect-ssid",
- description="The ssid for WiFi recoonection stress test")
- private String mReconnectionSsid = "securenetdhcp";
-
@Option(name="reconnect-password",
description="The password for the above ssid in WiFi reconnection stress test")
private String mReconnectionPassword = "androidwifi";
- @Option(name="idle-time",
- description="The device idle time after screen off")
- private String mIdleTime = "30"; // 30 seconds
+ @Option(name="reconnect-ssid",
+ description="The ssid for WiFi recoonection stress test")
+ private String mReconnectionSsid = "securenetdhcp";
+
+ @Option(name="reconnection-test",
+ description="Option to run the wifi reconnection stress test")
+ private boolean mReconnectionTestFlag = true;
+
+ @Option(name="scan-iteration",
+ description="The number of iterations to run WiFi scanning test")
+ private String mScanIteration = "100";
@Option(name="scan-test",
description="Option to run the scan stress test")
private boolean mScanTestFlag = true;
+ @Option(name="skip-set-device-screen-timeout",
+ description="Option to skip screen timeout configuration")
+ private boolean mSkipSetDeviceScreenTimeout = false;
+
@Option(name="tether-test",
description="Option to run the tethering stress test")
private boolean mTetherTestFlag = true;
- @Option(name="reconnection-test",
- description="Option to run the wifi reconnection stress test")
- private boolean mReconnectionTestFlag = true;
-
@Option(name="wifi-only")
private boolean mWifiOnly = false;
- private void setupTests() throws DeviceNotAvailableException {
- // get RadioHelper
- mRadioHelper = new RadioHelper(mTestDevice);
-
+ private void setupTests() {
if (mTestList != null) {
return;
}
@@ -185,7 +185,7 @@
* Configure screen timeout property
* @throws DeviceNotAvailableException
*/
- private void configDevice() throws DeviceNotAvailableException {
+ private void setDeviceScreenTimeout() throws DeviceNotAvailableException {
// Set device screen_off_timeout as svc power can be set to false in the Wi-Fi test
String command = ("sqlite3 /data/data/com.android.providers.settings/databases/settings.db "
+ "\"UPDATE system SET value=\'600000\' WHERE name=\'screen_off_timeout\';\"");
@@ -225,13 +225,15 @@
throws DeviceNotAvailableException {
Assert.assertNotNull(mTestDevice);
setupTests();
- configDevice();
+ if (!mSkipSetDeviceScreenTimeout) {
+ setDeviceScreenTimeout();
+ }
RunUtil.getDefault().sleep(START_TIMER);
+
if (!mWifiOnly) {
- if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
- mRadioHelper.getBugreport(standardListener);
- return;
- }
+ final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+ Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+ Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
}
IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(
diff --git a/prod-tests/tests/Android.mk b/prod-tests/tests/Android.mk
index 67d958f..f7f1e18 100644
--- a/prod-tests/tests/Android.mk
+++ b/prod-tests/tests/Android.mk
@@ -26,7 +26,7 @@
LOCAL_MODULE := tf-prod-metatests
LOCAL_MODULE_TAGS := optional
LOCAL_STATIC_JAVA_LIBRARIES := easymock
-LOCAL_JAVA_LIBRARIES := tradefed tf-prod-tests ddmlib-prebuilt
+LOCAL_JAVA_LIBRARIES := tradefed tf-prod-tests
include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/prod-tests/tests/src/com/android/continuous/SmokeTestFailureReporterTest.java b/prod-tests/tests/src/com/android/continuous/SmokeTestFailureReporterTest.java
index cbfa689..2d143f0 100644
--- a/prod-tests/tests/src/com/android/continuous/SmokeTestFailureReporterTest.java
+++ b/prod-tests/tests/src/com/android/continuous/SmokeTestFailureReporterTest.java
@@ -16,7 +16,6 @@
package com.android.continuous;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.build.BuildInfo;
import com.android.tradefed.build.IBuildInfo;
@@ -68,7 +67,7 @@
mReporter.invocationStarted(build);
mReporter.testRunStarted("testrun", 1);
mReporter.testStarted(testId);
- mReporter.testFailed(TestFailure.FAILURE, testId, trace);
+ mReporter.testFailed(testId, trace);
mReporter.testEnded(testId, emptyMap);
mReporter.testRunEnded(2, emptyMap);
mReporter.invocationEnded(1);
@@ -113,7 +112,7 @@
mReporter.testEnded(testPass1, emptyMap);
mReporter.testStarted(testFail);
- mReporter.testFailed(TestFailure.FAILURE, testFail, trace);
+ mReporter.testFailed(testFail, trace);
mReporter.testEnded(testFail, emptyMap);
mReporter.testStarted(testPass2);
diff --git a/remote/.classpath b/remote/.classpath
index fc4759a..ac373ae 100644
--- a/remote/.classpath
+++ b/remote/.classpath
@@ -5,5 +5,7 @@
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/guavalib_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/guava/guava/src"/>
<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
+ <classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/guava/guava-15.0.jar"/>
+ <classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/jsr305lib_intermediates/javalib.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/remote/Android.mk b/remote/Android.mk
index 0793f33..0c1919c 100644
--- a/remote/Android.mk
+++ b/remote/Android.mk
@@ -25,8 +25,7 @@
LOCAL_MODULE_TAGS := optional
# only depend on ddmlib for the Log class
-LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt
-LOCAL_STATIC_JAVA_LIBRARIES := json-prebuilt guavalib
+LOCAL_STATIC_JAVA_LIBRARIES := json-prebuilt jsr305lib guava-15.0-prebuilt ddmlib-prebuilt devtools-annotations-prebuilt
include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/remote/src/com/android/tradefed/command/remote/DeviceDescriptor.java b/remote/src/com/android/tradefed/command/remote/DeviceDescriptor.java
index 6ef8d39..88a5905 100644
--- a/remote/src/com/android/tradefed/command/remote/DeviceDescriptor.java
+++ b/remote/src/com/android/tradefed/command/remote/DeviceDescriptor.java
@@ -76,4 +76,12 @@
public String getBatteryLevel() {
return mBatteryLevel;
}
+
+ /**
+ * Provides a description with serials, product and build id
+ */
+ @Override
+ public String toString() {
+ return String.format("[%s %s:%s %s]", mSerial, mProduct, mProductVariant, mBuildId);
+ }
}
diff --git a/res/apks/wifiutil/PREBUILT b/res/apks/wifiutil/PREBUILT
index 66fee64..6cf18fc 100644
--- a/res/apks/wifiutil/PREBUILT
+++ b/res/apks/wifiutil/PREBUILT
@@ -1,4 +1,4 @@
This apk can be rebuilt from
platform/tools/tradefederation
-By running `m WifiUtil` on revision db5f64dd8f9ecb25b703b6564faa90e977ed56d6
+By running `m WifiUtil` on revision 5c4cc0d542cfdfcf92ce53db7a4fb30ea89fb9c7
diff --git a/res/apks/wifiutil/WifiUtil.apk b/res/apks/wifiutil/WifiUtil.apk
index a316b2d..2f0cb29 100644
--- a/res/apks/wifiutil/WifiUtil.apk
+++ b/res/apks/wifiutil/WifiUtil.apk
Binary files differ
diff --git a/script_help.sh b/script_help.sh
new file mode 100755
index 0000000..eddfe52
--- /dev/null
+++ b/script_help.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+# A library script to help other scripts run within an Android build environment, or while deployed
+# on a target host. Intended to be used with the `source` bash built-in command. Defines the
+# following environment variables:
+# JAVA_VERSION, RDBG_FLAG, TF_PATH, TRADEFED_OPTS
+#
+# It will react to the following environment variables, if they are set:
+# TF_DEBUG, TRADEFED_OPTS_FILE
+
+
+checkPath() {
+ if ! type -P "$1" &> /dev/null; then
+ echo "Unable to find $1 in path."
+ exit
+ fi;
+}
+
+checkFile() {
+ if [ ! -f "$1" ]; then
+ echo "Unable to locate $1"
+ exit
+ fi;
+}
+
+checkPath java
+
+# check java version
+java_version_string=$(java -version 2>&1)
+JAVA_VERSION=$(echo "$java_version_string" | grep '[ "]1\.7[\. "$$]')
+if [ "${JAVA_VERSION}" == "" ]; then
+ echo "Wrong java version. 1.7 is required."
+ exit
+fi
+
+# check debug flag and set up remote debugging
+if [ -n "${TF_DEBUG}" ]; then
+ if [ -z "${TF_DEBUG_PORT}" ]; then
+ TF_DEBUG_PORT=10088
+ fi
+ RDBG_FLAG="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=${TF_DEBUG_PORT}"
+fi
+
+# first try to find TF jars in same dir as this script
+CUR_DIR=$(dirname "$0")
+if [ -f "${CUR_DIR}/tradefed.jar" ]; then
+ TF_PATH="${CUR_DIR}/*"
+elif [ ! -z "${ANDROID_HOST_OUT}" ]; then
+ # in an Android build env, tradefed.jar should be in
+ # $ANDROID_HOST_OUT/tradefed/
+ if [ -f "${ANDROID_HOST_OUT}/tradefed/tradefed.jar" ]; then
+ # We intentionally pass the asterisk through without shell expansion
+ TF_PATH="${ANDROID_HOST_OUT}/tradefed/*"
+ fi
+fi
+
+if [ -z "${TF_PATH}" ]; then
+ echo "ERROR: Could not find tradefed jar files"
+ exit
+fi
+
+# set any host specific options
+# file format for file at $TRADEFED_OPTS_FILE is one line per host with the following format:
+# <hostname>=<options>
+# for example:
+# hostname.domain.com=-Djava.io.tmpdir=/location/on/disk -Danother=false ...
+# hostname2.domain.com=-Djava.io.tmpdir=/different/location -Danother=true ...
+if [ -e "${TRADEFED_OPTS_FILE}" ]; then
+ # pull the line for this host and take everything after the first =
+ export TRADEFED_OPTS=`grep "^$HOSTNAME=" "$TRADEFED_OPTS_FILE" | cut -d '=' -f 2-`
+fi
diff --git a/src/com/android/tradefed/build/BootstrapBuildProvider.java b/src/com/android/tradefed/build/BootstrapBuildProvider.java
index 3ff2ec8..4c16516 100644
--- a/src/com/android/tradefed/build/BootstrapBuildProvider.java
+++ b/src/com/android/tradefed/build/BootstrapBuildProvider.java
@@ -22,6 +22,8 @@
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
+import java.io.File;
+
/**
* A {@link IDeviceBuildProvider} that bootstraps build info from the test device
*
@@ -54,11 +56,17 @@
@Option(name="build-target", description="build target name to supply.")
private String mBuildTargetName = "bootstrapped";
+ @Option(name="branch", description="build branch name to supply.")
+ private String mBranch = null;
+
@Option(name="shell-available-timeout",
description="Time to wait in seconds for device shell to become available. " +
"Default to 300 seconds.")
private long mShellAvailableTimeout = 5 * 60;
+ @Option(name="tests-dir", description="Path to top directory of expanded tests zip")
+ private File mTestsDir = null;
+
@Override
public IBuildInfo getBuild() throws BuildRetrievalError {
throw new UnsupportedOperationException("Call getBuild(ITestDevice)");
@@ -78,19 +86,26 @@
@Override
public IBuildInfo getBuild(ITestDevice device) throws BuildRetrievalError,
DeviceNotAvailableException {
- IBuildInfo info = new BuildInfo(device.getBuildId(), mTestTag, mBuildTargetName);
+ String buildId = device.getBuildId();
+ IBuildInfo info = new DeviceBuildInfo(buildId, mTestTag, mBuildTargetName);
if (!device.waitForDeviceShell(mShellAvailableTimeout * 1000)) {
throw new DeviceNotAvailableException(
String.format("Shell did not become available in %d seconds",
mShellAvailableTimeout));
}
- info.setBuildBranch(String.format("%s-%s-%s-%s",
- device.getProperty("ro.product.brand"),
- device.getProperty("ro.product.name"),
- device.getProductVariant(),
- device.getProperty("ro.build.version.release")));
+ if (mBranch == null) {
+ mBranch = String.format("%s-%s-%s-%s",
+ device.getProperty("ro.product.brand"),
+ device.getProperty("ro.product.name"),
+ device.getProductVariant(),
+ device.getProperty("ro.build.version.release"));
+ }
+ info.setBuildBranch(mBranch);
info.setBuildFlavor(device.getBuildFlavor());
info.addBuildAttribute("build_alias", device.getBuildAlias());
+ if (mTestsDir != null && mTestsDir.isDirectory()) {
+ info.setFile("testsdir", mTestsDir, buildId);
+ }
return info;
}
}
diff --git a/src/com/android/tradefed/build/FolderBuildInfo.java b/src/com/android/tradefed/build/FolderBuildInfo.java
index 287bb7b..b213e77 100644
--- a/src/com/android/tradefed/build/FolderBuildInfo.java
+++ b/src/com/android/tradefed/build/FolderBuildInfo.java
@@ -91,6 +91,8 @@
// fall through
CLog.w("hardlink of %s %s failed: " + e.toString(), orig.getAbsolutePath(),
dest.getAbsolutePath());
+ // Clean up after failure.
+ FileUtil.recursiveDelete(dest);
}
FileUtil.recursiveCopy(orig, dest);
}
diff --git a/src/com/android/tradefed/command/CommandOptions.java b/src/com/android/tradefed/command/CommandOptions.java
index 16a746d..f207049 100644
--- a/src/com/android/tradefed/command/CommandOptions.java
+++ b/src/com/android/tradefed/command/CommandOptions.java
@@ -36,6 +36,9 @@
importance = Importance.ALWAYS)
private boolean mFullHelpMode = false;
+ @Option(name = "json-help", description = "display the full help in json format.")
+ private boolean mJsonHelpMode = false;
+
@Option(name = "dry-run",
description = "build but don't actually run the command. Intended as a quick check " +
"to ensure that a command is runnable.",
@@ -49,8 +52,7 @@
private boolean mNoisyDryRunMode = false;
@Option(name = "min-loop-time", description =
- "the minimum invocation time in ms when in loop mode.",
- updateRule = OptionUpdateRule.LEAST)
+ "the minimum invocation time in ms when in loop mode.")
private Long mMinLoopTime = 10L * 60L * 1000L;
@Option(name = "max-random-loop-time", description =
@@ -98,6 +100,22 @@
}
/**
+ * Set the json help mode for the config.
+ * <p/>
+ * Exposed for testing.
+ */
+ void setJsonHelpMode(boolean jsonHelpMode) {
+ mJsonHelpMode = jsonHelpMode;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isJsonHelpMode() {
+ return mJsonHelpMode;
+ }
+
+ /**
* Set the dry run mode for the config.
* <p/>
* Exposed for testing.
@@ -179,7 +197,7 @@
try {
OptionCopier.copyOptions(this, clone);
} catch (ConfigurationException e) {
- CLog.e("failed to clone command options", e);
+ CLog.e("failed to clone command options: %s", e.getMessage());
}
return clone;
}
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 00ec78a..65c4b18 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -17,6 +17,7 @@
package com.android.tradefed.command;
import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.command.CommandFileParser.CommandLine;
@@ -40,16 +41,22 @@
import com.android.tradefed.device.IDeviceManager;
import com.android.tradefed.device.IDeviceMonitor;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.NoDeviceException;
import com.android.tradefed.invoker.IRescheduler;
import com.android.tradefed.invoker.ITestInvocation;
import com.android.tradefed.invoker.TestInvocation;
import com.android.tradefed.log.LogRegistry;
import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.StubTestInvocationListener;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.ResultForwarder;
import com.android.tradefed.util.ArrayUtil;
+import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.QuotationAwareTokenizer;
+import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.TableFormatter;
+import org.json.JSONException;
+
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
@@ -66,8 +73,10 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
/**
* A scheduler for running TradeFederation commands across all available devices.
@@ -125,6 +134,10 @@
// FIXME: enable this to be enabled or disabled on a per-cmdfile basis
private boolean mReloadCmdfiles = false;
+ @Option(name = "max-poll-time", description =
+ "ms between forced command scheduler execution time")
+ private long mPollTime = 30 * 1000; // 30 seconds
+
private enum CommandState {
WAITING_FOR_DEVICE("Wait_for_device"),
EXECUTING("Executing"),
@@ -381,34 +394,40 @@
* <p/>
* Returns device to device manager and remote handover server if applicable.
*/
- private class FreeDeviceHandler extends StubTestInvocationListener implements
+ private class FreeDeviceHandler extends ResultForwarder implements
IScheduledInvocationListener {
private final IDeviceManager mDeviceManager;
- FreeDeviceHandler(IDeviceManager deviceManager) {
+ FreeDeviceHandler(IDeviceManager deviceManager,
+ IScheduledInvocationListener... listeners) {
+ super(listeners);
mDeviceManager = deviceManager;
}
@Override
public void invocationComplete(ITestDevice device, FreeDeviceState deviceState) {
+ for (ITestInvocationListener listener : getListeners()) {
+ ((IScheduledInvocationListener) listener).invocationComplete(device, deviceState);
+ }
+
mDeviceManager.freeDevice(device, deviceState);
remoteFreeDevice(device);
}
}
private class InvocationThread extends Thread {
- private final IScheduledInvocationListener mListener;
+ private final IScheduledInvocationListener[] mListeners;
private final ITestDevice mDevice;
private final ExecutableCommand mCmd;
private final ITestInvocation mInvocation;
private long mStartTime = -1;
- public InvocationThread(String name, IScheduledInvocationListener listener,
- ITestDevice device, ExecutableCommand command) {
+ public InvocationThread(String name, ITestDevice device, ExecutableCommand command,
+ IScheduledInvocationListener... listeners) {
// create a thread group so LoggerRegistry can identify this as an invocationThread
super(new ThreadGroup(name), name);
- mListener = listener;
+ mListeners = listeners;
mDevice = device;
mCmd = command;
mInvocation = createRunInstance();
@@ -424,10 +443,11 @@
mStartTime = System.currentTimeMillis();
ITestInvocation instance = getInvocation();
IConfiguration config = mCmd.getConfiguration();
+
try {
mCmd.commandStarted();
instance.invoke(mDevice, config, new Rescheduler(mCmd.getCommandTracker()),
- mListener);
+ mListeners);
} catch (DeviceUnresponsiveException e) {
CLog.w("Device %s is unresponsive. Reason: %s", mDevice.getSerialNumber(),
e.getMessage());
@@ -450,7 +470,9 @@
// when freed
removeInvocationThread(this);
mCmd.commandFinished(elapsedTime);
- mListener.invocationComplete(mDevice, deviceState);
+ for (final IScheduledInvocationListener listener : mListeners) {
+ listener.invocationComplete(mDevice, deviceState);
+ }
}
}
@@ -461,6 +483,50 @@
ITestDevice getDevice() {
return mDevice;
}
+
+ /**
+ * Stops a running invocation. {@link CommandScheduler#shutdownHard()} will stop
+ * all running invocations.
+ */
+ public void stopInvocation(String message) {
+ if (mDevice != null && mDevice.getIDevice().isOnline()) {
+ // Kill all running processes on device.
+ try {
+ mDevice.executeShellCommand("am kill-all");
+ } catch (DeviceNotAvailableException e) {
+ CLog.w("failed to kill process on device %s: %s", mDevice.getSerialNumber(), e);
+ }
+ }
+ RunUtil.getDefault().interrupt(this, message);
+ super.interrupt();
+ }
+
+ /**
+ * Checks whether the device battery level is above the required value to keep running the
+ * invocation.
+ */
+ public void checkDeviceBatteryLevel() {
+ final Integer cutoffBattery = mCmd.getConfiguration().getDeviceOptions()
+ .getCutoffBattery();
+ if (mDevice != null && cutoffBattery != null) {
+ final IDevice device = mDevice.getIDevice();
+ int batteryLevel = -1;
+ try {
+ batteryLevel = device.getBattery(0, TimeUnit.MILLISECONDS).get();
+ } catch (InterruptedException | ExecutionException e) {
+ // fall through
+ }
+ CLog.d("device %s: battey level=%d%%", device.getSerialNumber(), batteryLevel);
+ // This logic is based on the assumption that batterLevel will be 0 or -1 if TF
+ // fails to fetch a valid battery level or the device is not using a battery.
+ if (0 < batteryLevel && batteryLevel < cutoffBattery) {
+ CLog.i("Stopping %s: battery too low (%d%% < %d%%)",
+ getName(), batteryLevel, cutoffBattery);
+ stopInvocation(String.format(
+ "battery too low (%d%% < %d%%)", batteryLevel, cutoffBattery));
+ }
+ }
+ }
}
/**
@@ -590,7 +656,8 @@
while (!isShutdown()) {
// wait until processing is required again
- mCommandProcessWait.waitAndReset();
+ mCommandProcessWait.waitAndReset(mPollTime);
+ checkInvocations();
processReadyCommands(manager);
}
mCommandTimer.shutdown();
@@ -617,7 +684,18 @@
}
}
- private void processReadyCommands(IDeviceManager manager) {
+ void checkInvocations() {
+ CLog.d("Checking invocations...");
+ final List<InvocationThread> copy;
+ synchronized(this) {
+ copy = new ArrayList<InvocationThread>(mInvocationThreadMap.values());
+ }
+ for (InvocationThread thread : copy) {
+ thread.checkDeviceBatteryLevel();
+ }
+ }
+
+ protected void processReadyCommands(IDeviceManager manager) {
Map<ExecutableCommand, ITestDevice> scheduledCommandMap = new HashMap<>();
// minimize length of synchronized block by just matching commands with device first,
// then scheduling invocations/adding looping commands back to queue
@@ -642,8 +720,8 @@
for (Map.Entry<ExecutableCommand, ITestDevice> cmdDeviceEntry : scheduledCommandMap
.entrySet()) {
ExecutableCommand cmd = cmdDeviceEntry.getKey();
- startInvocation(new FreeDeviceHandler(getDeviceManager()), cmdDeviceEntry.getValue(),
- cmd);
+ startInvocation(cmdDeviceEntry.getValue(), cmd,
+ new FreeDeviceHandler(getDeviceManager()));
if (cmd.isLoopMode()) {
addNewExecCommandToQueue(cmd.getCommandTracker());
}
@@ -710,6 +788,13 @@
getConfigFactory().printHelpForConfig(args, true, System.out);
} else if (config.getCommandOptions().isFullHelpMode()) {
getConfigFactory().printHelpForConfig(args, false, System.out);
+ } else if (config.getCommandOptions().isJsonHelpMode()) {
+ try {
+ // Convert the JSON usage to a string (with 4 space indentation) and print to stdout
+ System.out.println(config.getJsonCommandUsage().toString(4));
+ } catch (JSONException e) {
+ CLog.logAndDisplay(LogLevel.ERROR, "Failed to get json command usage: %s", e);
+ }
} else if (config.getCommandOptions().isDryRunMode()) {
config.validateOptions();
String cmdLine = QuotationAwareTokenizer.combineTokens(args);
@@ -744,8 +829,9 @@
File cmdFile = new File(cmdFilePath);
if (mReloadCmdfiles && getCommandFileWatcher().isFileWatched(cmdFile)) {
CLog.logAndDisplay(LogLevel.INFO,
- "cmd file %s is already running and being watched for changes", cmdFilePath);
- return;
+ "cmd file %s is already running and being watched for changes. Reloading",
+ cmdFilePath);
+ removeCommandsFromFile(cmdFile);
}
internalAddCommandFile(cmdFile, extraArgs);
}
@@ -827,7 +913,7 @@
String commandFilePath) {
mCurrentCommandId++;
CLog.d("Creating command tracker id %d for command args: '%s'", mCurrentCommandId,
- ArrayUtil.join(" ", args));
+ ArrayUtil.join(" ", (Object[])args));
return new CommandTracker(mCurrentCommandId, args, commandFilePath);
}
@@ -905,6 +991,34 @@
* {@inheritDoc}
*/
@Override
+ public void execCommand(IScheduledInvocationListener listener, String[] args)
+ throws ConfigurationException, NoDeviceException {
+ assertStarted();
+ IDeviceManager manager = getDeviceManager();
+ CommandTracker cmdTracker = createCommandTracker(args, null);
+ IConfiguration config = getConfigFactory().createConfigurationFromArgs(
+ cmdTracker.getArgs());
+ config.validateOptions();
+
+ ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
+ ITestDevice device;
+
+ synchronized(this) {
+ device = manager.allocateDevice(config.getDeviceRequirements());
+ if (device == null) {
+ throw new NoDeviceException("no device is available for command: " + args);
+ }
+ CLog.i("Executing '%s' on '%s'", cmdTracker.getArgs()[0], device.getSerialNumber());
+ mExecutingCommands.add(execCmd);
+ }
+
+ startInvocation(device, execCmd, listener, new FreeDeviceHandler(manager));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public void execCommand(IScheduledInvocationListener listener, ITestDevice device, String[] args)
throws ConfigurationException {
assertStarted();
@@ -914,19 +1028,24 @@
config.validateOptions();
CLog.i("Executing '%s' on '%s'", cmdTracker.getArgs()[0], device.getSerialNumber());
ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
- startInvocation(listener, device, execCmd);
+
+ synchronized(this) {
+ mExecutingCommands.add(execCmd);
+ }
+
+ startInvocation(device, execCmd, listener);
}
/**
* Spawns off thread to run invocation for given device.
*
- * @param callback the {@link IInvocationCompleteHandler} to invoke when complete
* @param device the {@link ITestDevice}
* @param cmd the {@link ExecutableCommand} to execute
+ * @param listeners the {@link IScheduledInvocationLister}s to invoke when complete
* @return the thread that will run the invocation
*/
- private void startInvocation(IScheduledInvocationListener listener, ITestDevice device,
- ExecutableCommand cmd) {
+ private void startInvocation(ITestDevice device, ExecutableCommand cmd,
+ IScheduledInvocationListener... listeners) {
if (hasInvocationThread(device)) {
throw new IllegalStateException(
String.format("Attempting invocation on device %s when one is already running",
@@ -934,8 +1053,8 @@
}
CLog.d("starting invocation for command id %d", cmd.getCommandTracker().getId());
final String invocationName = String.format("Invocation-%s", device.getSerialNumber());
- InvocationThread invocationThread = new InvocationThread(invocationName, listener, device,
- cmd);
+ InvocationThread invocationThread = new InvocationThread(invocationName, device, cmd,
+ listeners);
invocationThread.start();
addInvocationThread(invocationThread);
}
@@ -958,11 +1077,11 @@
mInvocationThreadMap.put(invThread.getDevice(), invThread);
}
- private synchronized boolean isShutdown() {
+ protected synchronized boolean isShutdown() {
return mCommandTimer.isShutdown() || (mShutdownOnEmpty && getAllCommandsSize() == 0);
}
- private synchronized boolean isShuttingDown() {
+ protected synchronized boolean isShuttingDown() {
return mCommandTimer.isShutdown() || mShutdownOnEmpty;
}
@@ -1170,7 +1289,11 @@
@Override
public synchronized void shutdownHard() {
shutdown();
- CLog.logAndDisplay(LogLevel.WARN, "Force killing adb connection");
+
+ CLog.logAndDisplay(LogLevel.WARN, "Stopping invocation threads...");
+ for (InvocationThread thread : mInvocationThreadMap.values()) {
+ thread.stopInvocation("TF is shutting down");
+ }
getDeviceManager().terminateHard();
}
@@ -1235,22 +1358,89 @@
* {@inheritDoc}
*/
@Override
- public boolean stopInvocation(ITestInvocation invocation) throws UnsupportedOperationException {
- throw new UnsupportedOperationException();
+ public synchronized boolean stopInvocation(ITestInvocation invocation) {
+ for (InvocationThread thread : mInvocationThreadMap.values()) {
+ if (thread.getInvocation() == invocation) {
+ thread.interrupt();
+ return true;
+ }
+ }
+ return false;
}
/**
* {@inheritDoc}
*/
@Override
- public void displayCommandsInfo(PrintWriter printWriter) {
+ public void displayCommandsInfo(PrintWriter printWriter, String regex) {
assertStarted();
+ Pattern regexPattern = null;
+ if (regex != null) {
+ regexPattern = Pattern.compile(regex);
+ }
+
List<CommandTracker> cmds = getCommandTrackers();
Collections.sort(cmds, new CommandTrackerIdComparator());
for (CommandTracker cmd : cmds) {
- String cmdDesc = String.format("Command %d: [%s] %s", cmd.getId(),
- getTimeString(cmd.getTotalExecTime()), getArgString(cmd.getArgs()));
- printWriter.println(cmdDesc);
+ String argString = getArgString(cmd.getArgs());
+ if (regexPattern == null || regexPattern.matcher(argString).find()) {
+ String cmdDesc = String.format("Command %d: [%s] %s", cmd.getId(),
+ getTimeString(cmd.getTotalExecTime()), argString);
+ printWriter.println(cmdDesc);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dumpCommandsXml(PrintWriter printWriter, String regex) {
+ assertStarted();
+ Pattern regexPattern = null;
+ if (regex != null) {
+ regexPattern = Pattern.compile(regex);
+ }
+
+ List<ExecutableCommandState> cmdCopy = getAllCommands();
+ for (ExecutableCommandState cmd : cmdCopy) {
+ String[] args = cmd.cmd.getCommandTracker().getArgs();
+ String argString = getArgString(args);
+ if (regexPattern == null || regexPattern.matcher(argString).find()) {
+ // Use the config name prefixed by config__ for the file path
+ String xmlPrefix = "config__" + args[0].replace("/", "__") + "__";
+
+ // If the command line contains --template:map test config, use that config for the
+ // file path. This is because in the template system, many tests will have same
+ // base config and the distinguishing feature is the test included.
+ boolean templateIncludeFound = false;
+ boolean testFound = false;
+ for (String arg : args) {
+ if ("--template:map".equals(arg)) {
+ templateIncludeFound = true;
+ } else if (templateIncludeFound && "test".equals(arg)) {
+ testFound = true;
+ } else {
+ if (templateIncludeFound && testFound) {
+ xmlPrefix = "config__" + arg.replace("/", "__") + "__";
+ }
+ templateIncludeFound = false;
+ testFound = false;
+ }
+ }
+
+ try {
+ File xmlFile = FileUtil.createTempFile(xmlPrefix, ".xml");
+ PrintWriter writer = new PrintWriter(xmlFile);
+ cmd.cmd.getConfiguration().dumpXml(writer);
+ printWriter.println(String.format("Saved command dump to %s",
+ xmlFile.getAbsolutePath()));
+ } catch (IOException e) {
+ // Log exception and continue
+ CLog.e("Could not dump config xml");
+ CLog.e(e);
+ }
+ }
}
}
@@ -1361,6 +1551,9 @@
* @return true if event received before time elapsed, false otherwise
*/
public synchronized boolean waitForEvent(long maxWaitTime) {
+ if (maxWaitTime == 0) {
+ return waitForEvent();
+ }
long startTime = System.currentTimeMillis();
long remainingTime = maxWaitTime;
while (!mEventReceived && remainingTime > 0) {
@@ -1397,11 +1590,11 @@
}
/**
- * Wait indefinitely for event to be received, and reset state back to 'no event received'
+ * Wait for given ms for event to be received, and reset state back to 'no event received'
* upon completion.
*/
- public synchronized void waitAndReset() {
- waitForEvent();
+ public synchronized void waitAndReset(long maxWaitTime) {
+ waitForEvent(maxWaitTime);
reset();
}
diff --git a/src/com/android/tradefed/command/Console.java b/src/com/android/tradefed/command/Console.java
index cf723a8..4f7c5de 100644
--- a/src/com/android/tradefed/command/Console.java
+++ b/src/com/android/tradefed/command/Console.java
@@ -45,6 +45,7 @@
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
+import java.util.TreeMap;
import java.util.regex.Pattern;
/**
@@ -73,6 +74,7 @@
protected static final String VERSION_PATTERN = "version";
protected static final String REMOVE_PATTERN = "remove";
protected static final String DEBUG_PATTERN = "debug";
+ protected static final String LIST_COMMANDS_PATTERN = "c(?:ommands)?";
protected static final String LINE_SEPARATOR = System.getProperty("line.separator");
@@ -401,30 +403,39 @@
commandHelp.put(LIST_PATTERN, String.format(
"%s help:" + LINE_SEPARATOR +
- "\ti[nvocations] List all invocation threads" + LINE_SEPARATOR +
- "\td[evices] List all detected or known devices" + LINE_SEPARATOR +
- "\tc[ommands] List all commands currently waiting to be executed" +
+ "\ti[nvocations] List all invocation threads" + LINE_SEPARATOR +
+ "\td[evices] List all detected or known devices" + LINE_SEPARATOR +
+ "\tc[ommands] List all commands currently waiting to be executed" +
LINE_SEPARATOR +
- "\tconfigs List all known configurations" +
- LINE_SEPARATOR, LIST_PATTERN));
+ "\tc[ommands] [pattern] List all commands matching the pattern and currently " +
+ "waiting to be executed" + LINE_SEPARATOR +
+ "\tconfigs List all known configurations" + LINE_SEPARATOR,
+ LIST_PATTERN));
commandHelp.put(DUMP_PATTERN, String.format(
"%s help:" + LINE_SEPARATOR +
- "\ts[tack] Dump the stack traces of all threads" + LINE_SEPARATOR +
- "\tl[ogs] Dump the logs of all invocations to files" + LINE_SEPARATOR +
- "\tc[onfig] <config> Dump the content of the specified config" + LINE_SEPARATOR +
- "\tcommandQueue Dump the contents of the commmand execution queue" +
- LINE_SEPARATOR,
+ "\ts[tack] Dump the stack traces of all threads" + LINE_SEPARATOR +
+ "\tl[ogs] Dump the logs of all invocations to files" + LINE_SEPARATOR +
+ "\tc[onfig] <config> Dump the content of the specified config" + LINE_SEPARATOR +
+ "\tcommandQueue Dump the contents of the commmand execution queue" +
+ LINE_SEPARATOR +
+ "\tcommands Dump all the config XML for the commands waiting to be " +
+ "executed" + LINE_SEPARATOR +
+ "\tcommands [pattern] Dump all the config XML for the commands matching the " +
+ "pattern and waiting to be executed" + LINE_SEPARATOR +
+ "\te[nv] Dump the environment variables available to test harness " +
+ "process" + LINE_SEPARATOR,
DUMP_PATTERN));
commandHelp.put(RUN_PATTERN, String.format(
"%s help:" + LINE_SEPARATOR +
- "\tcommand <config> [options] Run the specified command" + LINE_SEPARATOR +
- "\t<config> [options] Shortcut for the above: run specified command" +
- LINE_SEPARATOR +
- "\tcmdfile <cmdfile.txt> Run the specified commandfile" + LINE_SEPARATOR +
+ "\tcommand <config> [options] Run the specified command" + LINE_SEPARATOR +
+ "\t<config> [options] Shortcut for the above: run specified " +
+ "command" + LINE_SEPARATOR +
+ "\tcmdfile <cmdfile.txt> Run the specified commandfile" +
+ LINE_SEPARATOR +
"\tcommandAndExit <config> [options] Run the specified command, and run " +
- "'exit -c' immediately afterward" + LINE_SEPARATOR,
+ "'exit -c' immediately afterward" + LINE_SEPARATOR +
"\tcmdfileAndExit <cmdfile.txt> Run the specified commandfile, and run " +
"'exit -c' immediately afterward" + LINE_SEPARATOR,
RUN_PATTERN));
@@ -437,8 +448,8 @@
commandHelp.put(REMOVE_PATTERN, String.format(
"%s help:" + LINE_SEPARATOR +
- "\tremove allCommands Remove all commands currently waiting to be executed"
- + LINE_SEPARATOR,
+ "\tremove allCommands Remove all commands currently waiting to be executed" +
+ LINE_SEPARATOR,
REMOVE_PATTERN));
commandHelp.put(DEBUG_PATTERN, String.format(
@@ -469,9 +480,18 @@
trie.put(new Runnable() {
@Override
public void run() {
- mScheduler.displayCommandsInfo(new PrintWriter(System.out, true));
+ mScheduler.displayCommandsInfo(new PrintWriter(System.out, true), null);
}
- }, LIST_PATTERN, "c(?:ommands)?");
+ }, LIST_PATTERN, LIST_COMMANDS_PATTERN);
+ ArgRunnable<CaptureList> listCmdRun = new ArgRunnable<CaptureList>() {
+ @Override
+ public void run(CaptureList args) {
+ // Skip 2 tokens to get past listPattern and "commands"
+ String pattern = args.get(2).get(0);
+ mScheduler.displayCommandsInfo(new PrintWriter(System.out, true), pattern);
+ }
+ };
+ trie.put(listCmdRun, LIST_PATTERN, LIST_COMMANDS_PATTERN, "(.*)");
trie.put(new Runnable() {
@Override
public void run() {
@@ -510,6 +530,29 @@
}
}, DUMP_PATTERN, "commandQueue");
+ trie.put(new Runnable() {
+ @Override
+ public void run() {
+ mScheduler.dumpCommandsXml(new PrintWriter(System.out, true), null);
+ }
+ }, DUMP_PATTERN, LIST_COMMANDS_PATTERN);
+ ArgRunnable<CaptureList> dumpCmdRun = new ArgRunnable<CaptureList>() {
+ @Override
+ public void run(CaptureList args) {
+ // Skip 2 tokens to get past listPattern and "commands"
+ String pattern = args.get(2).get(0);
+ mScheduler.dumpCommandsXml(new PrintWriter(System.out, true), pattern);
+ }
+ };
+ trie.put(dumpCmdRun, DUMP_PATTERN, LIST_COMMANDS_PATTERN, "(.*)");
+
+ trie.put(new Runnable() {
+ @Override
+ public void run() {
+ dumpEnv();
+ }
+ }, DUMP_PATTERN, "e(?:nv)?");
+
// Run commands
ArgRunnable<CaptureList> runRunCommand = new ArgRunnable<CaptureList>() {
@Override
@@ -873,6 +916,17 @@
}
/**
+ * Dumps the environment variables to console, sorted by variable names
+ */
+ private void dumpEnv() {
+ // use TreeMap to sort variables by name
+ Map<String, String> env = new TreeMap<>(System.getenv());
+ for (Map.Entry<String, String> entry : env.entrySet()) {
+ printLine(String.format("\t%s=%s", entry.getKey(), entry.getValue()));
+ }
+ }
+
+ /**
* Sets the console starting arguments.
*
* @param mainArgs the arguments
diff --git a/src/com/android/tradefed/command/ICommandOptions.java b/src/com/android/tradefed/command/ICommandOptions.java
index 38618dc..a4a5b56 100644
--- a/src/com/android/tradefed/command/ICommandOptions.java
+++ b/src/com/android/tradefed/command/ICommandOptions.java
@@ -32,6 +32,11 @@
public boolean isFullHelpMode();
/**
+ * Returns <code>true</code> if full json help mode has been requested
+ */
+ public boolean isJsonHelpMode();
+
+ /**
* Return <code>true</code> if we should <emph>skip</emph> adding this command to the queue.
*/
public boolean isDryRunMode();
diff --git a/src/com/android/tradefed/command/ICommandScheduler.java b/src/com/android/tradefed/command/ICommandScheduler.java
index b8aa37c..beaf4ee 100644
--- a/src/com/android/tradefed/command/ICommandScheduler.java
+++ b/src/com/android/tradefed/command/ICommandScheduler.java
@@ -20,6 +20,7 @@
import com.android.tradefed.config.IConfigurationFactory;
import com.android.tradefed.device.FreeDeviceState;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.NoDeviceException;
import com.android.tradefed.invoker.ITestInvocation;
import com.android.tradefed.result.ITestInvocationListener;
@@ -89,6 +90,18 @@
public boolean addCommand(String[] args, long totalExecTime) throws ConfigurationException;
/**
+ * Directly allocates a device and executes a command without adding it to the command queue.
+ *
+ * @param listener the {@link IScheduledInvocationListener} to be informed
+ * @param args the command arguments
+ *
+ * @throws ConfigurationException if command was invalid
+ * @throws NoDeviceException if there is no device to use
+ */
+ public void execCommand(IScheduledInvocationListener listener, String[] args)
+ throws ConfigurationException, NoDeviceException;
+
+ /**
* Directly execute command on already allocated device.
*
* @param listener the {@link IScheduledInvocationListener} to be informed
@@ -198,8 +211,20 @@
* Output a list of current commands.
*
* @param printWriter the {@link PrintWriter} to output to.
+ * @param regex the regular expression to which commands should be matched in order to be
+ * printed. If null, then all commands will be printed.
*/
- public void displayCommandsInfo(PrintWriter printWriter);
+ public void displayCommandsInfo(PrintWriter printWriter, String regex);
+
+ /**
+ * Dump the expanded xml file for the command with all
+ * {@link com.android.tradefed.config.Option} values specified for all current commands.
+ *
+ * @param printWriter the {@link PrintWriter} to output the status to.
+ * @param regex the regular expression to which commands should be matched in order for the
+ * xml file to be dumped. If null, then all commands will be dumped.
+ */
+ public void dumpCommandsXml(PrintWriter printWriter, String regex);
/**
* Output detailed debug info on state of command execution queue.
diff --git a/src/com/android/tradefed/command/Verify.java b/src/com/android/tradefed/command/Verify.java
new file mode 100644
index 0000000..a87861c
--- /dev/null
+++ b/src/com/android/tradefed/command/Verify.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.command;
+
+import com.android.tradefed.config.ArgsOptionParser;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Alternate Trade Federation entrypoint to validate command files
+ */
+public class Verify {
+
+ private static final int EXIT_STATUS_OKAY = 0x0;
+ private static final int EXIT_STATUS_FAILED = 0x1;
+
+ @Option(name = "cmdfile", description = "command file to verify")
+ private List<File> mCmdFiles = new ArrayList<>();
+
+ @Option(name = "show-commands", description = "Whether to print all generated commands")
+ private boolean mShowCommands = false;
+
+ @Option(name = "quiet", shortName = 'q', description = "Whether to silence all output. " +
+ "Overrides all other output-related settings.")
+ private boolean mQuiet = false;
+
+ @Option(name = "help", shortName = 'h', description = "Print help")
+ private boolean mHelp = false;
+
+ /**
+ * Returns whether the "--help"/"-h" option was passed to the instance
+ */
+ public boolean isHelpMode() {
+ return mHelp;
+ }
+
+ /**
+ * Program main entrypoint
+ */
+ public static void main(final String[] mainArgs) throws InterruptedException,
+ ConfigurationException {
+ try {
+ Verify verify = new Verify();
+ ArgsOptionParser optionSetter = new ArgsOptionParser(verify);
+ optionSetter.parse(mainArgs);
+ if (verify.isHelpMode()) {
+ // Print help, then exit
+ System.err.println(ArgsOptionParser.getOptionHelp(false, verify));
+ System.exit(EXIT_STATUS_OKAY);
+ }
+
+ if (verify.run()) {
+ // true == everything's good
+ System.exit(EXIT_STATUS_OKAY);
+ } else {
+ // false == whoopsie!
+ System.exit(EXIT_STATUS_FAILED);
+ }
+
+ } finally {
+ System.err.flush();
+ System.out.flush();
+ }
+ }
+
+ /**
+ * Start validating all specified cmdfiles
+ */
+ public boolean run() {
+ boolean anyFailures = false;
+
+ for (File cmdFile : mCmdFiles) {
+ try {
+ // if verify returns false, then we set anyFailures to true
+ anyFailures |= !runVerify(cmdFile);
+
+ } catch (Throwable t) {
+ if (!mQuiet) {
+ System.err.format("Caught exception while parsing \"%s\"\n", cmdFile);
+ System.err.println(t);
+ }
+ anyFailures = true;
+ }
+ }
+
+ return !anyFailures;
+ }
+
+ /**
+ * Validate the specified cmdfile
+ */
+ public boolean runVerify(File cmdFile) {
+ final CommandFileParser parser = new CommandFileParser();
+ try {
+ List<CommandFileParser.CommandLine> commands = parser.parseFile(cmdFile);
+ if (!mQuiet) {
+ System.out.format("Successfully parsed %d commands from cmdfile %s\n",
+ commands.size(), cmdFile);
+
+ if (mShowCommands) {
+ int i = 1;
+ int digits = (int) Math.ceil(Math.log10(commands.size()));
+ // Create a format string that will leave enough space for an index prefix
+ // without mucking up alignment
+ String format = String.format("%%%dd: %%s\n", digits);
+ for (CommandFileParser.CommandLine cmd : commands) {
+ System.out.format(format, i++, cmd);
+ }
+ }
+ System.out.println();
+ }
+ } catch (ConfigurationException | IOException e) {
+ if (!mQuiet) {
+ System.err.format("Failed to parse %s:\n", cmdFile);
+ System.err.println(e);
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/com/android/tradefed/config/ArgsOptionParser.java b/src/com/android/tradefed/config/ArgsOptionParser.java
index dc0d581..12d1efb 100644
--- a/src/com/android/tradefed/config/ArgsOptionParser.java
+++ b/src/com/android/tradefed/config/ArgsOptionParser.java
@@ -175,7 +175,71 @@
* @throws ConfigurationException if error occurred parsing the arguments.
*/
public List<String> parse(List<String> args) throws ConfigurationException {
- return parseOptions(args.listIterator());
+ final List<String> leftovers = new ArrayList<String>();
+ final ListIterator<String> argsIter = args.listIterator();
+
+ // Scan 'args'.
+ while (argsIter.hasNext() && parseArg(argsIter.next(), argsIter, leftovers)) {
+ // This loop has no body. All of the work happens by side-effect in parseArg(...)
+ }
+
+ // Package up the leftovers.
+ while (argsIter.hasNext()) {
+ leftovers.add(argsIter.next());
+ }
+ return leftovers;
+ }
+
+ /**
+ * A best-effort version of {@link #parse(String... args)}. If a ConfigurationException is
+ * thrown, that exception is captured internally, and the remaining arguments (including the
+ * argument which caused the exception to be thrown) are returned. This method does not throw.
+ *
+ * @return a {@link List} of the left over arguments
+ */
+ public List<String> parseBestEffort(String... args) {
+ return parseBestEffort(Arrays.asList(args));
+ }
+
+ /**
+ * Alternate {@link #parseBestEffort(String... args)} method that takes a {@link List} of
+ * arguments
+ *
+ * @return a {@link List} of the left over arguments
+ */
+ public List<String> parseBestEffort(List<String> args) {
+ final List<String> leftovers = new ArrayList<String>(args.size());
+ final ListIterator<String> argsIter = args.listIterator();
+ int lastProcessedIdx = -1;
+
+ /* (not javadoc)
+ * Scan 'args'. Note that each call to parseArg(...) will advance argsIter a number
+ * of places that depends on the arity of the particular Option being processed. For a
+ * boolean Option, it will not advance. For a standard unary Option, it will advance
+ * 1 place. For a Map Option, it will advance two places (1 for key, 1 for value).
+ *
+ * For this reason, we grab the index from the iterator itself, rather than trying to
+ * increment it ourselves.
+ */
+ while (argsIter.hasNext()) {
+ lastProcessedIdx = argsIter.nextIndex();
+ final String arg = argsIter.next();
+ try {
+ // All of the work happens within parseArg(...) by side-effect
+ if (!parseArg(arg, argsIter, leftovers)) break;
+ } catch (ConfigurationException e) {
+ // Something failed. Add all of the not-fully-processed and not-yet-processed args
+ // to leftovers and return
+ leftovers.addAll(args.subList(lastProcessedIdx, args.size()));
+ return leftovers;
+ }
+ }
+
+ // Package up the leftovers.
+ while (argsIter.hasNext()) {
+ leftovers.add(argsIter.next());
+ }
+ return leftovers;
}
/**
@@ -191,33 +255,32 @@
}
}
- private List<String> parseOptions(ListIterator<String> args) throws ConfigurationException {
- final List<String> leftovers = new ArrayList<String>();
+ /**
+ * Attempts to parse the specified argument.
+ *
+ * @return {@code true} if iteration should continue, or {@code false} if iteration should stop
+ */
+ private boolean parseArg(String arg, ListIterator<String> args, List<String> leftovers)
+ throws ConfigurationException {
+ if (arg.equals(OPTION_NAME_PREFIX)) {
+ // "--" marks the end of options and the beginning of positional arguments.
+ return false;
- // Scan 'args'.
- while (args.hasNext()) {
- final String arg = args.next();
- if (arg.equals(OPTION_NAME_PREFIX)) {
- // "--" marks the end of options and the beginning of positional arguments.
- break;
- } else if (arg.startsWith(OPTION_NAME_PREFIX)) {
- // A long option.
- parseLongOption(arg, args);
- } else if (arg.startsWith(SHORT_NAME_PREFIX)) {
- // A short option.
- parseGroupedShortOptions(arg, args);
- } else {
- // The first non-option marks the end of options.
- leftovers.add(arg);
- break;
- }
- }
+ } else if (arg.startsWith(OPTION_NAME_PREFIX)) {
+ // A long option.
+ parseLongOption(arg, args);
+ return true;
- // Package up the leftovers.
- while (args.hasNext()) {
- leftovers.add(args.next());
+ } else if (arg.startsWith(SHORT_NAME_PREFIX)) {
+ // A short option.
+ parseGroupedShortOptions(arg, args);
+ return true;
+
+ } else {
+ // The first non-option marks the end of options.
+ leftovers.add(arg);
+ return false;
}
- return leftovers;
}
private void parseLongOption(String arg, ListIterator<String> args)
diff --git a/src/com/android/tradefed/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index 166e90a..85587fb 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -34,14 +34,25 @@
import com.android.tradefed.targetprep.StubTargetPreparer;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.StubTest;
+import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.QuotationAwareTokenizer;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.kxml2.io.KXmlSerializer;
+
+import java.io.IOException;
import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
/**
* A concrete {@link IConfiguration} implementation that stores the loaded config objects in a map
@@ -60,12 +71,25 @@
public static final String DEVICE_REQUIREMENTS_TYPE_NAME = "device_requirements";
public static final String DEVICE_OPTIONS_TYPE_NAME = "device_options";
+ // additional element names used for emitting the configuration XML.
+ private static final String CONFIGURATION_NAME = "configuration";
+ private static final String OPTION_NAME = "option";
+ private static final String CLASS_NAME = "class";
+ private static final String NAME_NAME = "name";
+ private static final String KEY_NAME = "key";
+ private static final String VALUE_NAME = "value";
+
private static Map<String, ObjTypeInfo> sObjTypeMap = null;
/** Mapping of config object type name to config objects. */
private Map<String, List<Object>> mConfigMap;
+ /** Cached {@link OptionSetter} that must be kept in-sync with {@code mConfigMap} */
+ private OptionSetter mCachedOptionSetter = null;
private final String mName;
private final String mDescription;
+ // original command line used to create this given configuration.
+ private String[] mCommandLine;
+
/**
* Container struct for built-in config object type
@@ -145,6 +169,28 @@
return mDescription;
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setCommandLine(String[] arrayArgs) {
+ mCommandLine = arrayArgs;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getCommandLine() {
+ //FIXME: obfuscated passwords from command line.
+ if (mCommandLine != null && mCommandLine.length != 0) {
+ return QuotationAwareTokenizer.combineTokens(mCommandLine);
+ }
+ // If no args were available return null.
+ return null;
+ }
+
/**
* {@inheritDoc}
*/
@@ -271,13 +317,28 @@
}
/**
+ * Returns a cached OptionSetter which is appropriate for setting options on all objects which
+ * will be returned by {@link getAllConfigurationObjects()}. Note that, for cache coherency,
+ * the cache variable {@code mCachedOptionSetter} <emph>must</emph> be set to {@code null}
+ * anytime that {@code mConfigMap} is modified.
+ * <p />
+ * To improve thread-safety, all modifications of {@code mConfigMap} should be done atomically
+ * with an invalidation of {@code mCachedOptionSetter}, from the perspective of this method.
+ */
+ private synchronized OptionSetter getOptionSetter() throws ConfigurationException {
+ if (mCachedOptionSetter == null) {
+ mCachedOptionSetter = new OptionSetter(getAllConfigurationObjects());
+ }
+ return mCachedOptionSetter;
+ }
+
+ /**
* {@inheritDoc}
*/
@Override
public void injectOptionValue(String optionName, String optionValue)
throws ConfigurationException {
- OptionSetter optionSetter = new OptionSetter(getAllConfigurationObjects());
- optionSetter.setOptionValue(optionName, optionValue);
+ getOptionSetter().setOptionValue(optionName, optionValue);
}
/**
@@ -286,8 +347,7 @@
@Override
public void injectOptionValue(String optionName, String optionKey, String optionValue)
throws ConfigurationException {
- OptionSetter optionSetter = new OptionSetter(getAllConfigurationObjects());
- optionSetter.setOptionMapValue(optionName, optionKey, optionValue);
+ getOptionSetter().setOptionMapValue(optionName, optionKey, optionValue);
}
/**
@@ -402,11 +462,12 @@
* {@inheritDoc}
*/
@Override
- public void setConfigurationObject(String typeName, Object configObject)
+ public synchronized void setConfigurationObject(String typeName, Object configObject)
throws ConfigurationException {
if (configObject == null) {
throw new IllegalArgumentException("configObject cannot be null");
}
+ mCachedOptionSetter = null; // Keep this in sync with mConfigMap
mConfigMap.remove(typeName);
addObject(typeName, configObject);
}
@@ -415,11 +476,12 @@
* {@inheritDoc}
*/
@Override
- public void setConfigurationObjectList(String typeName, List<?> configList)
+ public synchronized void setConfigurationObjectList(String typeName, List<?> configList)
throws ConfigurationException {
if (configList == null) {
throw new IllegalArgumentException("configList cannot be null");
}
+ mCachedOptionSetter = null; // Keep this in sync with mConfigMap
mConfigMap.remove(typeName);
for (Object configObject : configList) {
addObject(typeName, configObject);
@@ -433,7 +495,9 @@
* @param configObject the configuration object
* @throws ConfigurationException if object was not the correct type
*/
- private void addObject(String typeName, Object configObject) throws ConfigurationException {
+ private synchronized void addObject(String typeName, Object configObject) throws ConfigurationException {
+ mCachedOptionSetter = null; // Keep this in sync with mConfigMap
+
List<Object> objList = mConfigMap.get(typeName);
if (objList == null) {
objList = new ArrayList<Object>(1);
@@ -499,13 +563,10 @@
* {@inheritDoc}
*/
@Override
- public void setOptionsFromCommandLineArgs(List<String> listArgs) throws ConfigurationException {
+ public List<String> setOptionsFromCommandLineArgs(List<String> listArgs)
+ throws ConfigurationException {
ArgsOptionParser parser = new ArgsOptionParser(getAllConfigurationObjects());
- List<String> unprocessedArgs = parser.parse(listArgs);
- if (unprocessedArgs.size() > 0) {
- throw new ConfigurationException(String.format(
- "Invalid arguments provided. Unprocessed arguments: %s", unprocessedArgs));
- }
+ return parser.parse(listArgs);
}
/**
@@ -546,6 +607,66 @@
}
/**
+ * {@inheritDoc}
+ */
+ @Override
+ public JSONArray getJsonCommandUsage() throws JSONException {
+ JSONArray ret = new JSONArray();
+ for (Map.Entry<String, List<Object>> configObjectsEntry : mConfigMap.entrySet()) {
+ for (Object optionObject : configObjectsEntry.getValue()) {
+
+ // Build a JSON representation of the current class
+ JSONObject jsonClass = new JSONObject();
+ jsonClass.put("name", configObjectsEntry.getKey());
+ if (optionObject.getClass().isAnnotationPresent(OptionClass.class)) {
+ OptionClass optionClass =
+ optionObject.getClass().getAnnotation(OptionClass.class);
+ jsonClass.put("alias", optionClass.alias());
+ }
+ jsonClass.put("class", optionObject.getClass().getName());
+
+ // For each of the @Option annotated fields
+ Collection<Field> optionFields =
+ OptionSetter.getOptionFieldsForClass(optionObject.getClass());
+ JSONArray jsonFields = new JSONArray();
+ for (Field field : optionFields) {
+ Option option = field.getAnnotation(Option.class);
+
+ // Build a JSON representation of the current field
+ JSONObject jsonField = new JSONObject();
+
+ jsonField.put("name", option.name());
+ if (option.shortName() != Option.NO_SHORT_NAME) {
+ jsonField.put("shortName", option.shortName());
+ }
+ jsonField.put("description", option.description());
+ jsonField.put("importance", option.importance());
+ jsonField.put("mandatory", option.mandatory());
+ jsonField.put("isTimeVal", option.isTimeVal());
+ jsonField.put("javaClass", field.getType().getName());
+ try {
+ field.setAccessible(true);
+ Object value = field.get(optionObject);
+ jsonField.put("defaultValue", value == null ? "null" : value.toString());
+ } catch (IllegalAccessException e) {
+ // Shouldn't happen
+ throw new RuntimeException(e);
+ }
+
+ // Add the JSON field representation to the JSON class representation
+ jsonFields.put(jsonField);
+ }
+ jsonClass.put("fields", jsonFields);
+
+ // Add the JSON class representation to the list
+ ret.put(jsonClass);
+ }
+ }
+
+ return ret;
+ }
+
+ /**
* Prints out the available config options for given configuration object.
*
* @param importantOnly print only the important options
@@ -567,4 +688,96 @@
public void validateOptions() throws ConfigurationException {
new ArgsOptionParser(getAllConfigurationObjects()).validateMandatoryOptions();
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dumpXml(PrintWriter output) throws IOException {
+ KXmlSerializer serializer = new KXmlSerializer();
+ serializer.setOutput(output);
+ serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ serializer.startDocument("UTF-8", null);
+ serializer.startTag(null, CONFIGURATION_NAME);
+
+ dumpClassToXml(serializer, BUILD_PROVIDER_TYPE_NAME, getBuildProvider());
+ for (ITargetPreparer preparer : getTargetPreparers()) {
+ dumpClassToXml(serializer, TARGET_PREPARER_TYPE_NAME, preparer);
+ }
+ for (IRemoteTest test : getTests()) {
+ dumpClassToXml(serializer, TEST_TYPE_NAME, test);
+ }
+ dumpClassToXml(serializer, DEVICE_RECOVERY_TYPE_NAME, getDeviceRecovery());
+ dumpClassToXml(serializer, LOGGER_TYPE_NAME, getLogOutput());
+ dumpClassToXml(serializer, LOG_SAVER_TYPE_NAME, getLogSaver());
+ for (ITestInvocationListener listener : getTestInvocationListeners()) {
+ dumpClassToXml(serializer, RESULT_REPORTER_TYPE_NAME, listener);
+ }
+ dumpClassToXml(serializer, CMD_OPTIONS_TYPE_NAME, getCommandOptions());
+ dumpClassToXml(serializer, DEVICE_REQUIREMENTS_TYPE_NAME, getDeviceRequirements());
+ dumpClassToXml(serializer, DEVICE_OPTIONS_TYPE_NAME, getDeviceOptions());
+
+ serializer.endTag(null, CONFIGURATION_NAME);
+ serializer.endDocument();
+ }
+
+ /**
+ * Add a class to the command XML dump.
+ */
+ private void dumpClassToXml(KXmlSerializer serializer, String classTypeName, Object obj)
+ throws IOException {
+ serializer.startTag(null, classTypeName);
+ serializer.attribute(null, CLASS_NAME, obj.getClass().getName());
+ dumpOptionsToXml(serializer, obj);
+ serializer.endTag(null, classTypeName);
+ }
+
+ /**
+ * Add all the options of class to the command XML dump.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void dumpOptionsToXml(KXmlSerializer serializer, Object obj) throws IOException {
+ for (Field field : OptionSetter.getOptionFieldsForClass(obj.getClass())) {
+ Option option = field.getAnnotation(Option.class);
+ Object fieldVal = OptionSetter.getFieldValue(field, obj);
+ if (fieldVal == null) {
+ continue;
+ } else if (fieldVal instanceof Collection) {
+ for (Object entry : (Collection) fieldVal) {
+ dumpOptionToXml(serializer, option.name(), null, entry.toString());
+ }
+ } else if (fieldVal instanceof Map) {
+ Map map = (Map) fieldVal;
+ for (Object entryObj : map.entrySet()) {
+ Map.Entry entry = (Entry) entryObj;
+ dumpOptionToXml(serializer, option.name(), entry.getKey().toString(),
+ entry.getValue().toString());
+ }
+ } else if (fieldVal instanceof MultiMap) {
+ MultiMap multimap = (MultiMap) fieldVal;
+ for (Object keyObj : multimap.keySet()) {
+ for (Object valueObj : multimap.get(keyObj)) {
+ dumpOptionToXml(serializer, option.name(), keyObj.toString(),
+ valueObj.toString());
+ }
+ }
+ } else {
+ dumpOptionToXml(serializer, option.name(), null, fieldVal.toString());
+ }
+ }
+ }
+
+ /**
+ * Add a single option to the command XML dump.
+ */
+ private void dumpOptionToXml(KXmlSerializer serializer, String name, String key, String value)
+ throws IOException {
+ serializer.startTag(null, OPTION_NAME);
+ serializer.attribute(null, NAME_NAME, name);
+ if (key != null) {
+ serializer.attribute(null, KEY_NAME, key);
+ }
+ serializer.attribute(null, VALUE_NAME, value);
+ serializer.endTag(null, OPTION_NAME);
+ }
}
diff --git a/src/com/android/tradefed/config/ConfigurationFactory.java b/src/com/android/tradefed/config/ConfigurationFactory.java
index 12532c8..811ee82 100644
--- a/src/com/android/tradefed/config/ConfigurationFactory.java
+++ b/src/com/android/tradefed/config/ConfigurationFactory.java
@@ -23,6 +23,7 @@
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -31,6 +32,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
@@ -48,8 +50,69 @@
private static IConfigurationFactory sInstance = null;
private static final String CONFIG_SUFFIX = ".xml";
private static final String CONFIG_PREFIX = "config/";
+ private static final String CONFIG_SPLIT = "|";
- private Map<String, ConfigurationDef> mConfigDefMap;
+ private Map<ConfigId, ConfigurationDef> mConfigDefMap;
+
+ /**
+ * A simple struct-like class that stores a configuration's name alongside the arguments for
+ * any {@code <template-include>} tags it may contain. Because the actual bits stored by the
+ * configuration may vary with template arguments, they must be considered as essential a part
+ * of the configuration's identity as the filename.
+ */
+ static class ConfigId {
+ public String name = null;
+ public Map<String, String> templateMap = new HashMap<>();
+
+ /**
+ * No-op constructor
+ */
+ public ConfigId() {}
+
+ /**
+ * Convenience constructor. Equivalent to calling two-arg constructor with {@code null}
+ * {@code templateMap}.
+ */
+ public ConfigId(String name) {
+ this(name, null);
+ }
+
+ /**
+ * Two-arg convenience constructor. {@code templateMap} may be null.
+ */
+ public ConfigId(String name, Map<String, String> templateMap) {
+ this.name = name;
+ if (templateMap != null) {
+ this.templateMap.putAll(templateMap);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return 2 * ((name == null) ? 0 : name.hashCode()) + 3 * templateMap.hashCode();
+ }
+
+ private boolean matches(Object a, Object b) {
+ if (a == null && b == null) return true;
+ if (a == null || b == null) return false;
+ return a.equals(b);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) return false;
+ if (!(other instanceof ConfigId)) return false;
+
+ final ConfigId otherConf = (ConfigId) other;
+ return matches(name, otherConf.name) && matches(templateMap, otherConf.templateMap);
+ }
+ }
/**
* A {@link IClassPathFilter} for configuration XML files.
@@ -62,8 +125,9 @@
@Override
public boolean accept(String pathName) {
// only accept entries that match the pattern, and that we don't already know about
+ final ConfigId pathId = new ConfigId(pathName);
return pathName.startsWith(CONFIG_PREFIX) && pathName.endsWith(CONFIG_SUFFIX) &&
- !mConfigDefMap.containsKey(pathName);
+ !mConfigDefMap.containsKey(pathId);
}
/**
@@ -111,27 +175,91 @@
* {@inheritDoc}
*/
@Override
- public ConfigurationDef getConfigurationDef(String name) throws ConfigurationException {
- // first attempt to load cached config def
- ConfigurationDef def = mConfigDefMap.get(name);
+ public ConfigurationDef getConfigurationDef(String name, Map<String, String> templateMap)
+ throws ConfigurationException {
+ // FIXME: Currently this does not support on the fly reload of configs.
+ // We need to clear the cache.
+ String configName = name;
+ if (!isBundledConfig(name)) {
+ configName = getAbsolutePath(null, name);
+ }
+
+ final ConfigId configId = new ConfigId(name, templateMap);
+ ConfigurationDef def = mConfigDefMap.get(configId);
+
if (def == null) {
- // not found - load from file
- def = new ConfigurationDef(name);
- loadConfiguration(name, def);
- mConfigDefMap.put(name, def);
+ def = new ConfigurationDef(configName);
+ loadConfiguration(configName, def, templateMap);
+
+ mConfigDefMap.put(configId, def);
}
return def;
}
- @Override
- public void loadIncludedConfiguration(ConfigurationDef parent, String name)
- throws ConfigurationException {
- if (mIncludedConfigs.contains(name)) {
- throw new ConfigurationException(String.format(
- "Circular configuration include: config '%s' is already included", name));
+ /**
+ * Returns true if it is a config file found inside the classpath.
+ */
+ private boolean isBundledConfig(String name) {
+ InputStream configStream = getClass().getResourceAsStream(
+ String.format("/%s%s%s", getConfigPrefix(), name, CONFIG_SUFFIX));
+ return configStream != null;
+ }
+
+ /**
+ * Get the absolute path of a local config file.
+ * @param root parent path of config file
+ * @param name config file
+ * @return absolute path for local config file.
+ * @throws ConfigurationException
+ */
+ private String getAbsolutePath(String root, String name) throws ConfigurationException {
+ File file = new File(name);
+ if (!file.isAbsolute()) {
+ if (root == null) {
+ // if root directory was not specified, get the current working directory.
+ root = System.getProperty("user.dir");
+ }
+ file = new File(root, name);
}
- mIncludedConfigs.add(name);
- loadConfiguration(name, parent);
+ try {
+ return file.getCanonicalPath();
+ } catch (IOException e) {
+ throw new ConfigurationException(String.format(
+ "Failure when trying to determine local file canonical path %s", e));
+ }
+ }
+
+ @Override
+ /**
+ * Configs that are bundled inside the tradefed.jar can only include other configs also
+ * bundled inside tradefed.jar. However, local (external) configs can include both local
+ * (external) and bundled configs.
+ */
+ public void loadIncludedConfiguration(ConfigurationDef def, String parentName, String name)
+ throws ConfigurationException {
+ String config_name = name;
+ if (!isBundledConfig(name)) {
+ try {
+ // Ensure bundled configs are not including local configs.
+ if (isBundledConfig(parentName)) {
+ throw new ConfigurationException(String.format("Invalid include; bundled " +
+ "config '%s' is trying to include local config '%s'.", parentName, name));
+ }
+ // Local configs' include should be relative to their parent's path.
+ String parentRoot = new File(parentName).getParentFile().getCanonicalPath();
+ config_name = getAbsolutePath(parentRoot, name);
+ } catch (IOException e) {
+ throw new ConfigurationException(String.format(
+ "Failure when trying to determine local file canonical path %s", e));
+ }
+ }
+ if (mIncludedConfigs.contains(config_name)) {
+ throw new ConfigurationException(String.format(
+ "Circular configuration include: config '%s' is already included",
+ config_name));
+ }
+ mIncludedConfigs.add(config_name);
+ loadConfiguration(config_name, def, null);
}
/**
@@ -139,15 +267,18 @@
*
* @param name the name of a built-in configuration to load or a file
* path to configuration xml to load
- * @return the loaded {@link ConfigurationDef}
+ * @param def the loaded {@link ConfigurationDef}
+ * @param templateMap map from template-include names to their respective concrete
+ * configuration files
* @throws ConfigurationException if a configuration with given
* name/file path cannot be loaded or parsed
*/
- void loadConfiguration(String name, ConfigurationDef def) throws ConfigurationException {
+ void loadConfiguration(String name, ConfigurationDef def, Map<String, String> templateMap)
+ throws ConfigurationException {
Log.i(LOG_TAG, String.format("Loading configuration '%s'", name));
BufferedInputStream bufStream = getConfigStream(name);
ConfigurationXmlParser parser = new ConfigurationXmlParser(this);
- parser.parse(def, name, bufStream);
+ parser.parse(def, name, bufStream, templateMap);
}
/**
@@ -160,7 +291,7 @@
}
ConfigurationFactory() {
- mConfigDefMap = new Hashtable<String, ConfigurationDef>();
+ mConfigDefMap = new Hashtable<ConfigId, ConfigurationDef>();
}
/**
@@ -181,9 +312,9 @@
* @return {@link ConfigurationDef}
* @throws ConfigurationException if an error occurred loading the config
*/
- private ConfigurationDef getConfigurationDef(String name, boolean isGlobal)
- throws ConfigurationException {
- return new ConfigLoader(isGlobal).getConfigurationDef(name);
+ private ConfigurationDef getConfigurationDef(String name, boolean isGlobal,
+ Map<String, String> templateMap) throws ConfigurationException {
+ return new ConfigLoader(isGlobal).getConfigurationDef(name, templateMap);
}
/**
@@ -192,9 +323,29 @@
@Override
public IConfiguration createConfigurationFromArgs(String[] arrayArgs)
throws ConfigurationException {
+ return createConfigurationFromArgs(arrayArgs, null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public IConfiguration createConfigurationFromArgs(String[] arrayArgs,
+ List<String> unconsumedArgs) throws ConfigurationException {
List<String> listArgs = new ArrayList<String>(arrayArgs.length);
IConfiguration config = internalCreateConfigurationFromArgs(arrayArgs, listArgs);
- config.setOptionsFromCommandLineArgs(listArgs);
+ config.setCommandLine(arrayArgs);
+ final List<String> tmpUnconsumedArgs = config.setOptionsFromCommandLineArgs(listArgs);
+
+ if (unconsumedArgs == null && tmpUnconsumedArgs.size() > 0) {
+ // (unconsumedArgs == null) is taken as a signal that the caller expects all args to
+ // be processed.
+ throw new ConfigurationException(String.format(
+ "Invalid arguments provided. Unprocessed arguments: %s", tmpUnconsumedArgs));
+ } else if (unconsumedArgs != null) {
+ // Return the unprocessed args
+ unconsumedArgs.addAll(tmpUnconsumedArgs);
+ }
return config;
}
@@ -205,9 +356,9 @@
* Note will not populate configuration with values from options
*
* @param arrayArgs the full list of command line arguments, including the config name
- * @param optionArgsRef an empty list, that will be populated with the remaining option
- * arguments
- * @return
+ * @param optionArgsRef an empty list, that will be populated with the option arguments left
+ * to be interpreted
+ * @return An {@link IConfiguration} object representing the configuration that was loaded
* @throws ConfigurationException
*/
private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs,
@@ -215,10 +366,17 @@
if (arrayArgs.length == 0) {
throw new ConfigurationException("Configuration to run was not specified");
}
- optionArgsRef.addAll(Arrays.asList(arrayArgs));
+ final List<String> listArgs = new ArrayList<>(Arrays.asList(arrayArgs));
// first arg is config name
- final String configName = optionArgsRef.remove(0);
- ConfigurationDef configDef = getConfigurationDef(configName, false);
+ final String configName = listArgs.remove(0);
+
+ // Steal ConfigurationXmlParser arguments from the command line
+ final ConfigurationXmlParserSettings parserSettings = new ConfigurationXmlParserSettings();
+ final ArgsOptionParser templateArgParser = new ArgsOptionParser(parserSettings);
+ optionArgsRef.addAll(templateArgParser.parseBestEffort(listArgs));
+
+ ConfigurationDef configDef = getConfigurationDef(configName, false,
+ parserSettings.templateMap);
return configDef.createConfiguration();
}
@@ -256,7 +414,7 @@
optionArgsRef.addAll(Arrays.asList(arrayArgs));
// first arg is config name
final String configName = optionArgsRef.remove(0);
- ConfigurationDef configDef = getConfigurationDef(configName, false);
+ ConfigurationDef configDef = getConfigurationDef(configName, false, null);
return configDef.createGlobalConfiguration();
}
@@ -302,9 +460,10 @@
ClassPathScanner cpScanner = new ClassPathScanner();
Set<String> configNames = cpScanner.getClassPathEntries(new ConfigClasspathFilter());
for (String configName : configNames) {
+ final ConfigId configId = new ConfigId(configName);
try {
- ConfigurationDef configDef = getConfigurationDef(configName, false);
- mConfigDefMap.put(configName, configDef);
+ ConfigurationDef configDef = getConfigurationDef(configName, false, null);
+ mConfigDefMap.put(configId, configDef);
} catch (ConfigurationException e) {
ps.printf("Failed to load %s: %s", configName, e.getMessage());
ps.println();
@@ -392,22 +551,22 @@
* @throws ConfigurationException if one or more configs failed to load
*/
void loadAndPrintAllConfigs() throws ConfigurationException {
- loadAllConfigs(false);
- boolean failed = false;
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- PrintStream ps = new PrintStream(baos);
- for (ConfigurationDef def : mConfigDefMap.values()) {
- try {
- def.createConfiguration().printCommandUsage(false,
- new PrintStream(StreamUtil.nullOutputStream()));
- } catch (ConfigurationException e) {
- ps.printf("Failed to print %s: %s", def.getName(), e.getMessage());
- ps.println();
- failed = true;
- }
- }
- if (failed) {
- throw new ConfigurationException(baos.toString());
- }
+ loadAllConfigs(false);
+ boolean failed = false;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PrintStream ps = new PrintStream(baos);
+ for (ConfigurationDef def : mConfigDefMap.values()) {
+ try {
+ def.createConfiguration().printCommandUsage(false,
+ new PrintStream(StreamUtil.nullOutputStream()));
+ } catch (ConfigurationException e) {
+ ps.printf("Failed to print %s: %s", def.getName(), e.getMessage());
+ ps.println();
+ failed = true;
+ }
+ }
+ if (failed) {
+ throw new ConfigurationException(baos.toString());
+ }
}
}
diff --git a/src/com/android/tradefed/config/ConfigurationXmlParser.java b/src/com/android/tradefed/config/ConfigurationXmlParser.java
index 1c9ed57..5cf10ce 100644
--- a/src/com/android/tradefed/config/ConfigurationXmlParser.java
+++ b/src/com/android/tradefed/config/ConfigurationXmlParser.java
@@ -23,6 +23,8 @@
import java.io.IOException;
import java.io.InputStream;
+import java.util.Collections;
+import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
@@ -37,21 +39,58 @@
/**
* SAX callback object. Handles parsing data from the xml tags.
*/
- private static class ConfigHandler extends DefaultHandler {
+ static class ConfigHandler extends DefaultHandler {
private static final String OBJECT_TAG = "object";
private static final String OPTION_TAG = "option";
private static final String INCLUDE_TAG = "include";
+ private static final String TEMPLATE_INCLUDE_TAG = "template-include";
private static final String CONFIG_TAG = "configuration";
- private ConfigurationDef mConfigDef;
- private String mCurrentConfigObject;
+ /**
+ * A simple class to encapsulate a failure to resolve a <template-include>. This
+ * allows the error to be easily detected programmatically.
+ */
+ @SuppressWarnings("serial")
+ private class TemplateResolutionError extends ConfigurationException {
+ TemplateResolutionError(String templateName) {
+ super(String.format(
+ "Failed to parse config xml '%s'. Reason: " +
+ "Couldn't resolve template-include named " +
+ "'%s': No 'default' attribute and no matching manual resolution. " +
+ "Try using argument --template:map %s (config path)",
+ mConfigDef.getName(), templateName, templateName));
+ }
+ }
+
+ /** Note that this simply hasn't been implemented; it is not intentionally forbidden. */
+ static final String INNER_TEMPLATE_INCLUDE_ERROR =
+ "Configurations which contain a <template-include> tag, not having a 'default' " +
+ "attribute, may not be the target of any <include> or <template-include> tag. " +
+ "However, configuration '%s' attempted to include configuration '%s', which " +
+ "contains a <template-include> tag without a 'default' attribute.";
+
+ // Settings
private final IConfigDefLoader mConfigDefLoader;
+ private final ConfigurationDef mConfigDef;
+ private final Map<String, String> mTemplateMap;
+ private final String mName;
+
+ // State-holding members
+ private String mCurrentConfigObject;
private Boolean isLocalConfig = null;
- ConfigHandler(ConfigurationDef def, IConfigDefLoader loader) {
+ ConfigHandler(ConfigurationDef def, String name, IConfigDefLoader loader,
+ Map<String, String> templateMap) {
+ mName = name;
mConfigDef = def;
mConfigDefLoader = loader;
+
+ if (templateMap == null) {
+ mTemplateMap = Collections.<String, String>emptyMap();
+ } else {
+ mTemplateMap = templateMap;
+ }
}
@Override
@@ -113,8 +152,44 @@
throwException("Missing 'name' attribute for include");
}
try {
- mConfigDefLoader.loadIncludedConfiguration(mConfigDef, includeName);
+ mConfigDefLoader.loadIncludedConfiguration(mConfigDef, mName, includeName);
} catch (ConfigurationException e) {
+ if (e instanceof TemplateResolutionError) {
+ // The actual cause of this error is that recursive <template-include>
+ // invocations aren't currently supported. So replace that exception
+ // with something more useful.
+ throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
+ mConfigDef.getName(), includeName));
+ }
+
+ throw new SAXException(e);
+ }
+
+ } else if (TEMPLATE_INCLUDE_TAG.equals(localName)) {
+ final String templateName = attributes.getValue("name");
+ if (templateName == null) {
+ throwException("Missing 'name' attribute for template-include");
+ }
+
+ String includeName = mTemplateMap.get(templateName);
+ if (includeName == null) {
+ includeName = attributes.getValue("default");
+ }
+ if (includeName == null) {
+ throw new SAXException(new TemplateResolutionError(templateName));
+ }
+
+ try {
+ mConfigDefLoader.loadIncludedConfiguration(mConfigDef, mName, includeName);
+ } catch (ConfigurationException e) {
+ if (e instanceof TemplateResolutionError) {
+ // The actual cause of this error is that recursive <template-include>
+ // invocations aren't currently supported. So replace that exception
+ // with something more useful.
+ throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
+ mConfigDef.getName(), includeName));
+ }
+
throw new SAXException(e);
}
@@ -165,13 +240,14 @@
* @param xmlInput the configuration xml to parse
* @throws ConfigurationException if input could not be parsed or had invalid format
*/
- void parse(ConfigurationDef configDef, String name, InputStream xmlInput)
- throws ConfigurationException {
+ void parse(ConfigurationDef configDef, String name, InputStream xmlInput,
+ Map<String, String> templateMap) throws ConfigurationException {
try {
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
parserFactory.setNamespaceAware(true);
SAXParser parser = parserFactory.newSAXParser();
- ConfigHandler configHandler = new ConfigHandler(configDef, mConfigDefLoader);
+ ConfigHandler configHandler = new ConfigHandler(configDef, name, mConfigDefLoader,
+ templateMap);
parser.parse(new InputSource(xmlInput), configHandler);
} catch (ParserConfigurationException e) {
throwConfigException(name, e);
diff --git a/src/com/android/tradefed/config/ConfigurationXmlParserSettings.java b/src/com/android/tradefed/config/ConfigurationXmlParserSettings.java
new file mode 100644
index 0000000..71729a6
--- /dev/null
+++ b/src/com/android/tradefed/config/ConfigurationXmlParserSettings.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A simple class to accept settings for the ConfigurationXmlParser
+ * <p />
+ * To pass settings to this class, the alias is mandatory. So something like
+ * {@code --template:map name filename.xml} will work, but this would NOT work:
+ * {@code --map name filename.xml}.
+ */
+@OptionClass(alias = "template", global_namespace = false)
+class ConfigurationXmlParserSettings {
+ @Option(name = "map", description = "Map the <template-include> tag with the specified " +
+ "name to the specified actual configuration file. Configuration file " +
+ "resolution will happen as with a standard <include> tag.")
+ public Map<String, String> templateMap = new HashMap<>();
+}
diff --git a/src/com/android/tradefed/config/GlobalConfiguration.java b/src/com/android/tradefed/config/GlobalConfiguration.java
index 64378fc..a50366c 100644
--- a/src/com/android/tradefed/config/GlobalConfiguration.java
+++ b/src/com/android/tradefed/config/GlobalConfiguration.java
@@ -23,6 +23,7 @@
import com.android.tradefed.device.IDeviceManager;
import com.android.tradefed.device.IDeviceMonitor;
import com.android.tradefed.device.IDeviceSelection;
+import com.android.tradefed.device.IMultiDeviceRecovery;
import com.android.tradefed.log.ITerribleFailureHandler;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.MultiMap;
@@ -48,6 +49,7 @@
public static final String HOST_OPTIONS_TYPE_NAME = "host_options";
public static final String DEVICE_REQUIREMENTS_TYPE_NAME = "device_requirements";
public static final String SCHEDULER_TYPE_NAME = "command_scheduler";
+ public static final String MULTI_DEVICE_RECOVERY_TYPE_NAME = "multi_device_recovery";
private static Map<String, ObjTypeInfo> sObjTypeMap = null;
private static IGlobalConfiguration sInstance = null;
@@ -191,6 +193,8 @@
new ObjTypeInfo(ITerribleFailureHandler.class, false));
sObjTypeMap.put(SCHEDULER_TYPE_NAME,
new ObjTypeInfo(ICommandScheduler.class, false));
+ sObjTypeMap.put(MULTI_DEVICE_RECOVERY_TYPE_NAME,
+ new ObjTypeInfo(IMultiDeviceRecovery.class, true));
}
return sObjTypeMap;
@@ -234,6 +238,7 @@
* {@inheritDoc}
*/
@Override
+ @SuppressWarnings("unchecked")
public List<IDeviceMonitor> getDeviceMonitors() {
return (List<IDeviceMonitor>) getConfigurationObjectList(DEVICE_MONITOR_TYPE_NAME);
}
@@ -273,6 +278,16 @@
/**
* {@inheritDoc}
*/
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<IMultiDeviceRecovery> getMultiDeviceRecoveryHandlers() {
+ return (List<IMultiDeviceRecovery>)getConfigurationObjectList(
+ MULTI_DEVICE_RECOVERY_TYPE_NAME);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
// @Override
public List<?> getConfigurationObjectList(String typeName) {
return mConfigMap.get(typeName);
diff --git a/src/com/android/tradefed/config/IConfigDefLoader.java b/src/com/android/tradefed/config/IConfigDefLoader.java
index a4cb980..b8e03b0 100644
--- a/src/com/android/tradefed/config/IConfigDefLoader.java
+++ b/src/com/android/tradefed/config/IConfigDefLoader.java
@@ -16,6 +16,8 @@
package com.android.tradefed.config;
+import java.util.Map;
+
/**
* Interface for retrieving a ConfigurationDef.
*/
@@ -25,21 +27,24 @@
* Retrieve the {@link ConfigurationDef} for the given name
*
* @param name
+ * @param templateMap map of template-include names to configuration filenames
* @return {@link ConfigurationDef}
* @throws ConfigurationException if an error occurred loading the config
*/
- ConfigurationDef getConfigurationDef(String name) throws ConfigurationException;
+ ConfigurationDef getConfigurationDef(String name, Map<String, String> templateMap)
+ throws ConfigurationException;
boolean isGlobalConfig();
/**
* Load a config's data into the given {@link ConfigurationDef}
*
- * @param parent the {@link ConfigurationDef} to load the data into
+ * @param def the {@link ConfigurationDef} to load the data into
+ * @param parentName the name of the parent config
* @param name the name of config to include
* @return {@link ConfigurationDef}
* @throws ConfigurationException if an error occurred loading the config
*/
- void loadIncludedConfiguration(ConfigurationDef parent, String name)
+ void loadIncludedConfiguration(ConfigurationDef def, String parentName, String name)
throws ConfigurationException;
}
diff --git a/src/com/android/tradefed/config/IConfiguration.java b/src/com/android/tradefed/config/IConfiguration.java
index 6e5a5b7..54cb674 100644
--- a/src/com/android/tradefed/config/IConfiguration.java
+++ b/src/com/android/tradefed/config/IConfiguration.java
@@ -27,7 +27,12 @@
import com.android.tradefed.targetprep.ITargetPreparer;
import com.android.tradefed.testtype.IRemoteTest;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.IOException;
import java.io.PrintStream;
+import java.io.PrintWriter;
import java.util.List;
/**
@@ -273,8 +278,10 @@
* @see {@link ArgsOptionParser} for expected format
*
* @param listArgs the command line arguments
+ * @return the unconsumed arguments
*/
- public void setOptionsFromCommandLineArgs(List<String> listArgs) throws ConfigurationException;
+ public List<String> setOptionsFromCommandLineArgs(List<String> listArgs)
+ throws ConfigurationException;
/**
* Outputs a command line usage help text for this configuration to given printStream.
@@ -287,6 +294,13 @@
throws ConfigurationException;
/**
+ * Returns a JSON representation of this configuration.
+ *
+ * @throws JSONException
+ */
+ public JSONArray getJsonCommandUsage() throws JSONException;
+
+ /**
* Validate option values.
* <p/>
* Currently this will just validate that all mandatory options have been set
@@ -294,4 +308,29 @@
* @throws ConfigurationException if config is not valid
*/
public void validateOptions() throws ConfigurationException;
+
+ /**
+ * Sets the command line used to create this {@link IConfiguration}.
+ * This stores the whole command line, including the configuration name,
+ * unlike setOptionsFromCommandLineArgs.
+ *
+ * @param arrayArgs the command line
+ */
+ public void setCommandLine(String[] arrayArgs);
+
+ /**
+ * Gets the the command line used to create this {@link IConfiguration}.
+ *
+ * @return the command line used to create this {@link IConfiguration}.
+ */
+ public String getCommandLine();
+
+ /**
+ * Gets the expanded XML file for the config with all options shown for this
+ * {@link IConfiguration} as a {@link String}.
+ *
+ * @param output the writer to print the xml to.
+ * @throws IOException
+ */
+ public void dumpXml(PrintWriter output) throws IOException;
}
diff --git a/src/com/android/tradefed/config/IConfigurationFactory.java b/src/com/android/tradefed/config/IConfigurationFactory.java
index e4b0bae..bb015f2 100644
--- a/src/com/android/tradefed/config/IConfigurationFactory.java
+++ b/src/com/android/tradefed/config/IConfigurationFactory.java
@@ -25,17 +25,32 @@
public interface IConfigurationFactory {
/**
+ * A convenience method which calls {@link createConfigurationFromArgs(String[], List<String>)}
+ * with a {@code null} second argument. Thus, it will throw {@link ConfigurationException} if
+ * any unconsumed arguments remain.
+ *
+ * @see createConfigurationFromArgs(String[] List<String>)
+ */
+ public IConfiguration createConfigurationFromArgs(String[] args) throws ConfigurationException;
+
+ /**
* Create the {@link IConfiguration} from command line arguments.
* <p/>
* Expected format is "CONFIG [options]", where CONFIG is the built-in configuration name or
* a file path to a configuration xml file.
*
* @param args the command line arguments
+ * @param unconsumedArgs a List which will be populated with the arguments that were not
+ * consumed by the Objects associated with the specified config. If this
+ * is {@code null}, then the implementation will throw
+ * {@link ConfigurationException} if any unprocessed args remain.
+ *
* @return the loaded {@link IConfiguration}. The delegate object {@link Option} fields have
* been populated with values in args.
* @throws {@link ConfigurationException} if configuration could not be loaded
*/
- public IConfiguration createConfigurationFromArgs(String[] args) throws ConfigurationException;
+ public IConfiguration createConfigurationFromArgs(String[] args, List<String> unconsumedArgs)
+ throws ConfigurationException;
/**
* Create a {@link IGlobalConfiguration} from command line arguments.
@@ -88,5 +103,4 @@
* @param out the {@link PrintStream} to dump output to
*/
public void dumpConfig(String configName, PrintStream out);
-
}
diff --git a/src/com/android/tradefed/config/IGlobalConfiguration.java b/src/com/android/tradefed/config/IGlobalConfiguration.java
index 808565a..0ac5028 100644
--- a/src/com/android/tradefed/config/IGlobalConfiguration.java
+++ b/src/com/android/tradefed/config/IGlobalConfiguration.java
@@ -21,6 +21,7 @@
import com.android.tradefed.device.IDeviceManager;
import com.android.tradefed.device.IDeviceMonitor;
import com.android.tradefed.device.IDeviceSelection;
+import com.android.tradefed.device.IMultiDeviceRecovery;
import com.android.tradefed.log.ITerribleFailureHandler;
import java.util.List;
@@ -153,6 +154,13 @@
public ICommandScheduler getCommandScheduler();
/**
+ * Gets the list of {@link IMultiDeviceRecovery} to use from the configuration.
+ *
+ * @return the list of {@link IMultiDeviceRecovery}, or <code>null</code> if not set.
+ */
+ public List<IMultiDeviceRecovery> getMultiDeviceRecoveryHandlers();
+
+ /**
* Set the {@link IDeviceManager}, replacing any existing values. This sets the manager
* for the test devices
*
diff --git a/src/com/android/tradefed/config/Option.java b/src/com/android/tradefed/config/Option.java
index c27eeb7..7bc8a5a 100644
--- a/src/com/android/tradefed/config/Option.java
+++ b/src/com/android/tradefed/config/Option.java
@@ -82,7 +82,26 @@
* <li>The field is an empty {@link java.util.Collection}.</li>
* </ul>
*/
- boolean mandatory() default false;
+ boolean mandatory() default false;
+
+ /**
+ * Whether the option represents a time value.
+ * <p />
+ * If this is a time value, time-specific suffixes will be parsed. The field <emph>MUST</emph>
+ * be a {@code long} or {@code Long} for this flag to be valid. A
+ * {@code ConfigurationException} will be thrown otherwise.
+ * <p />
+ * The default unit is millis. The configuration framework will accept {@code s} for seconds
+ * (1000 millis), {@code m} for minutes (60 seconds), {@code h} for hours (60 minutes),
+ * or {@code d} for days (24 hours).
+ * <p />
+ * Units may be mixed and matched, so long as each unit appears at most once, and so long as
+ * all units which do appear are listed in decreasing order of scale. So, for instance,
+ * {@code h} may only appear before {@code m}, and may only appear after {@code d}. As a
+ * specific example, "1d2h3m4s5ms" would be a valid time value, as would "4" or "4ms". All
+ * embedded whitespace is discarded.
+ */
+ boolean isTimeVal() default false;
/**
* Controls the behavior when an option is specified multiple times. Note that this rule is
diff --git a/src/com/android/tradefed/config/OptionSetter.java b/src/com/android/tradefed/config/OptionSetter.java
index e420a24..4cf4d0f 100644
--- a/src/com/android/tradefed/config/OptionSetter.java
+++ b/src/com/android/tradefed/config/OptionSetter.java
@@ -18,6 +18,8 @@
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.ArrayUtil;
+import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.TimeVal;
import com.google.common.base.Objects;
import java.io.File;
@@ -82,6 +84,7 @@
handlers.put(String.class, new StringHandler());
handlers.put(File.class, new FileHandler());
+ handlers.put(TimeVal.class, new TimeValHandler());
}
private static Handler getHandler(Type type) throws ConfigurationException {
@@ -96,7 +99,8 @@
"cannot handle nested parameterized type " + type);
}
return getHandler(actualType);
- } else if (Map.class.isAssignableFrom(rawClass)) {
+ } else if (Map.class.isAssignableFrom(rawClass) ||
+ MultiMap.class.isAssignableFrom(rawClass)) {
// handle Map
Type keyType = parameterizedType.getActualTypeArguments()[0];
Type valueType = parameterizedType.getActualTypeArguments()[1];
@@ -111,8 +115,8 @@
return new MapHandler(getHandler(keyType), getHandler(valueType));
} else {
throw new ConfigurationException(String.format(
- "can't handle parameterized type %s; only Collection and Map are supported",
- type));
+ "can't handle parameterized type %s; only Collection, Map, and MultiMap "
+ + "are supported", type));
}
}
if (type instanceof Class) {
@@ -134,6 +138,13 @@
throw new ConfigurationException(String.format(
"Cannot handle non-parameterized map %s. Use a generic Map to specify "
+ "desired element types.", type));
+ } else if (MultiMap.class.isAssignableFrom(cType)) {
+ // could handle by just having a default of treating
+ // contents as String but consciously decided this
+ // should be an error
+ throw new ConfigurationException(String.format(
+ "Cannot handle non-parameterized multimap %s. Use a generic MultiMap to "
+ + "specify desired element types.", type));
}
return handlers.get(cType);
}
@@ -141,6 +152,50 @@
type));
}
+ /**
+ * Does some magic to distinguish TimeVal long field from normal long fields, then calls
+ * {@see #getHandler(Type)} in the appropriate manner.
+ */
+ private Handler getHandlerOrTimeVal(Field field, Object optionSource)
+ throws ConfigurationException {
+ // Do some magic to distinguish TimeVal long fields from normal long fields
+ final Option option = field.getAnnotation(Option.class);
+ if (option == null) {
+ // Shouldn't happen, but better to check.
+ throw new ConfigurationException(String.format(
+ "internal error: @Option annotation for field %s in class %s was " +
+ "unexpectedly null",
+ field.getName(), optionSource.getClass().getName()));
+ }
+
+ final Type type = field.getGenericType();
+ if (option.isTimeVal()) {
+ // We've got a field that marks itself as a time value. First off, verify that it's
+ // a compatible type
+ if (type instanceof Class) {
+ final Class<?> cType = (Class<?>) type;
+ if (long.class.equals(cType) || Long.class.equals(cType)) {
+ // Parse time value and return a Long
+ return new TimeValLongHandler();
+
+ } else if (TimeVal.class.equals(cType)) {
+ // Parse time value and return a TimeVal object
+ return new TimeValHandler();
+ }
+ }
+
+ throw new ConfigurationException(String.format("Only fields of type long, " +
+ "Long, or TimeVal may be declared as isTimeVal. Field %s has " +
+ "incompatible type %s.", field.getName(), field.getGenericType()));
+
+ } else {
+ // Note that fields declared as TimeVal (or Generic types with TimeVal parameters) will
+ // follow this branch, but will still work as expected.
+ return getHandler(type);
+ }
+ }
+
+
private final Collection<Object> mOptionSources;
private final Map<String, OptionFieldsForName> mOptionMap;
@@ -243,13 +298,13 @@
* @throws ConfigurationException if Option cannot be found or valueText is wrong type
*/
public void setOptionValue(String optionName, String valueText) throws ConfigurationException {
- OptionFieldsForName optionFields = fieldsForArg(optionName);
+ final OptionFieldsForName optionFields = fieldsForArg(optionName);
for (Map.Entry<Object, Field> fieldEntry : optionFields) {
- Object optionSource = fieldEntry.getKey();
- Field field = fieldEntry.getValue();
- Handler handler = getHandler(field.getGenericType());
- Object value = handler.translate(valueText);
+ final Object optionSource = fieldEntry.getKey();
+ final Field field = fieldEntry.getValue();
+ final Handler handler = getHandlerOrTimeVal(field, optionSource);
+ final Object value = handler.translate(valueText);
if (value == null) {
final String type = field.getType().getSimpleName();
throw new ConfigurationException(
@@ -303,6 +358,22 @@
"for option '%s') in class '%s'",
field.getName(), optionName, optionSource.getClass().getName()));
}
+ } else if (MultiMap.class.isAssignableFrom(field.getType())) {
+ MultiMap multimap = (MultiMap)field.get(optionSource);
+ if (multimap == null) {
+ throw new ConfigurationException(String.format(
+ "internal error: no storage allocated for field '%s' (used for " +
+ "option '%s') in class '%s'",
+ field.getName(), optionName, optionSource.getClass().getName()));
+ }
+ if (value instanceof MultiMap) {
+ multimap.putAll((MultiMap)value);
+ } else {
+ throw new ConfigurationException(String.format(
+ "internal error: value provided for field '%s' is not a multimap " +
+ "(used for option '%s') in class '%s'",
+ field.getName(), optionName, optionSource.getClass().getName()));
+ }
} else {
final Option option = field.getAnnotation(Option.class);
if (option == null) {
@@ -378,18 +449,28 @@
}
try {
field.setAccessible(true);
- if (!Map.class.isAssignableFrom(field.getType())) {
+ if (Map.class.isAssignableFrom(field.getType())) {
+ Map map = (Map)field.get(optionSource);
+ if (map == null) {
+ throw new ConfigurationException(String.format(
+ "internal error: no storage allocated for field '%s' (used for " +
+ "option '%s') in class '%s'",
+ field.getName(), optionName, optionSource.getClass().getName()));
+ }
+ map.put(pair.mKey, pair.mValue);
+ } else if (MultiMap.class.isAssignableFrom(field.getType())) {
+ MultiMap multimap = (MultiMap)field.get(optionSource);
+ if (multimap == null) {
+ throw new ConfigurationException(String.format(
+ "internal error: no storage allocated for field '%s' (used for " +
+ "option '%s') in class '%s'",
+ field.getName(), optionName, optionSource.getClass().getName()));
+ }
+ multimap.put(pair.mKey, pair.mValue);
+ } else {
throw new ConfigurationException(String.format(
"internal error: not a map field!"));
}
- Map map = (Map)field.get(optionSource);
- if (map == null) {
- throw new ConfigurationException(String.format(
- "internal error: no storage allocated for field '%s' (used for " +
- "option '%s') in class '%s'",
- field.getName(), optionName, optionSource.getClass().getName()));
- }
- map.put(pair.mKey, pair.mValue);
} catch (IllegalAccessException e) {
throw new ConfigurationException(String.format(
"internal error when setting option '%s'", optionName), e);
@@ -553,6 +634,11 @@
if (m.isEmpty()) {
unsetOptions.add(realOptName);
}
+ } else if (value instanceof MultiMap) {
+ MultiMap m = (MultiMap) value;
+ if (m.isEmpty()) {
+ unsetOptions.add(realOptName);
+ }
}
}
}
@@ -614,6 +700,11 @@
if (map.isEmpty()) {
return null;
}
+ } else if (fieldValue instanceof MultiMap) {
+ MultiMap multimap = (MultiMap)fieldValue;
+ if (multimap.isEmpty()) {
+ return null;
+ }
}
return fieldValue.toString();
}
@@ -807,6 +898,36 @@
}
}
+ private static class TimeValLongHandler extends Handler {
+ /**
+ * We parse the string as a time value, and return a {@code long}
+ */
+ @Override
+ Object translate(String valueText) {
+ try {
+ return TimeVal.fromString(valueText);
+
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+ }
+
+ private static class TimeValHandler extends Handler {
+ /**
+ * We parse the string as a time value, and return a {@code TimeVal}
+ */
+ @Override
+ Object translate(String valueText) {
+ try {
+ return new TimeVal(valueText);
+
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+ }
+
private static class FloatHandler extends Handler {
@Override
Object translate(String valueText) {
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index 35c7e32..acd7255 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -34,6 +34,7 @@
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.SizeLimitedOutputStream;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.TableFormatter;
import com.google.common.annotations.VisibleForTesting;
@@ -49,8 +50,6 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/**
* {@inheritDoc}
@@ -61,13 +60,21 @@
/** max wait time in ms for fastboot devices command to complete */
private static final long FASTBOOT_CMD_TIMEOUT = 1 * 60 * 1000;
- /** time to wait in ms between fastboot devices requests */
+ /** time to wait in ms between fastboot devices requests */
private static final long FASTBOOT_POLL_WAIT_TIME = 5 * 1000;
- /** time to wait for device adb shell responsive connection before declaring it unavailable
- * for testing */
+ /**
+ * time to wait for device adb shell responsive connection before declaring it unavailable for
+ * testing
+ */
private static final int CHECK_WAIT_DEVICE_AVAIL_MS = 30 * 1000;
- /** a {@link DeviceSelectionOptions} that matches any device. Visible for testing. */
+ /* the max size of the emulator output in bytes */
+ private static final long MAX_EMULATOR_OUTPUT = 20 * 1024 * 1024;
+
+ /* the emulator output log name */
+ private static final String EMULATOR_OUTPUT = "emulator_log";
+
+ /** a {@link DeviceSelectionOptions} that matches any device. Visible for testing. */
static final IDeviceSelection ANY_DEVICE_OPTIONS = new DeviceSelectionOptions();
private static final String NULL_DEVICE_SERIAL_PREFIX = "null-device";
private static final String EMULATOR_SERIAL_PREFIX = "emulator";
@@ -85,15 +92,22 @@
private FastbootMonitor mFastbootMonitor;
private boolean mIsTerminated = false;
private IDeviceSelection mGlobalDeviceFilter;
- @Option(name="max-emulators",
+
+ @Option(name = "max-emulators",
description = "the maximum number of emulators that can be allocated at one time")
private int mNumEmulatorSupported = 1;
- @Option(name="max-null-devices",
+ @Option(name = "max-null-devices",
description = "the maximum number of no device runs that can be allocated at one time.")
private int mNumNullDevicesSupported = 1;
private boolean mSynchronousMode = false;
+ @Option(name = "device-recovery-interval",
+ description = "the interval in ms between attempts to recover unavailable devices.")
+ private long mDeviceRecoveryInterval = 10 * 60 * 1000;
+
+ private DeviceRecoverer mDeviceRecoverer;
+
/**
* Creator interface for {@link IManagedTestDevice}s
*/
@@ -109,7 +123,7 @@
@Override
public void init() {
- init(null,null);
+ init(null, null);
}
/**
@@ -160,7 +174,8 @@
}
mManagedDeviceList = new ManagedDeviceList(deviceFactory);
- if (isFastbootAvailable()) {
+ final FastbootHelper fastboot = new FastbootHelper(getRunUtil());
+ if (fastboot.isFastbootAvailable()) {
mFastbootListeners = Collections.synchronizedSet(new HashSet<IFastbootListener>());
mFastbootMonitor = new FastbootMonitor();
startFastbootMonitor();
@@ -178,7 +193,7 @@
// don't start adding devices until fastboot support has been established
// TODO: Temporarily increase default timeout as workaround for syncFiles timeouts
- DdmPreferences.setTimeOut(30*1000);
+ DdmPreferences.setTimeOut(30 * 1000);
mAdbBridge = createAdbBridge();
mManagedDeviceListener = new ManagedDeviceListener();
// It's important to add the listener before initializing the ADB bridge to avoid a race
@@ -199,6 +214,10 @@
mAdbBridge.init(false /* client support */, "adb");
addEmulators();
addNullDevices();
+
+ List<IMultiDeviceRecovery> recoverers = getGlobalConfig().getMultiDeviceRecoveryHandlers();
+ mDeviceRecoverer = new DeviceRecoverer(recoverers);
+ startDeviceRecoverer();
}
/**
@@ -219,23 +238,6 @@
}
/**
- * Determine if fastboot is available for use.
- */
- private boolean isFastbootAvailable() {
- CommandResult fastbootResult = getRunUtil().runTimedCmdSilently(5000, "fastboot", "help");
- if (fastbootResult.getStatus() == CommandStatus.SUCCESS) {
- return true;
- }
- if (fastbootResult.getStderr() != null &&
- fastbootResult.getStderr().indexOf("usage: fastboot") >= 0) {
- CLog.logAndDisplay(LogLevel.WARN,
- "You are running an older version of fastboot, please update it.");
- return true;
- }
- return false;
- }
-
- /**
* Start fastboot monitoring.
* <p/>
* Exposed for unit testing.
@@ -245,6 +247,15 @@
}
/**
+ * Start device recovery.
+ * <p/>
+ * Exposed for unit testing.
+ */
+ void startDeviceRecoverer() {
+ mDeviceRecoverer.start();
+ }
+
+ /**
* Get the {@link IGlobalConfiguration} instance to use.
* <p />
* Exposed for unit testing.
@@ -263,7 +274,17 @@
}
/**
+ * Create a {@link RunUtil} instance to use.
+ * <p/>
+ * Exposed for unit testing.
+ */
+ IRunUtil createRunUtil() {
+ return new RunUtil();
+ }
+
+ /**
* Asynchronously checks if device is available, and adds to queue
+ *
* @param device
*/
private void checkAndAddAvailableDevice(final IManagedTestDevice testDevice) {
@@ -280,7 +301,7 @@
public void run() {
CLog.d("checking new device %s responsiveness", testDevice.getSerialNumber());
if (testDevice.getMonitor().waitForDeviceShell(CHECK_WAIT_DEVICE_AVAIL_MS)) {
- DeviceEventResponse r = mManagedDeviceList.handleDeviceEvent(testDevice,
+ DeviceEventResponse r = mManagedDeviceList.handleDeviceEvent(testDevice,
DeviceEvent.AVAILABLE_CHECK_PASSED);
if (r.stateChanged && r.allocationState == DeviceAllocationState.Available) {
CLog.logAndDisplay(LogLevel.INFO, "Detected new device %s",
@@ -299,7 +320,7 @@
}
}
};
- if (mSynchronousMode ) {
+ if (mSynchronousMode) {
checkRunnable.run();
} else {
Thread checkThread = new Thread(checkRunnable, threadName);
@@ -341,9 +362,10 @@
}
private void addFastbootDevices() {
- Set<String> serials = getDevicesOnFastboot();
+ final FastbootHelper fastboot = new FastbootHelper(getRunUtil());
+ Set<String> serials = fastboot.getDevices();
if (serials != null) {
- for (String serial: serials) {
+ for (String serial : serials) {
FastbootDevice d = new FastbootDevice(serial);
if (mGlobalDeviceFilter != null && mGlobalDeviceFilter.matches(d)) {
addAvailableDevice(d);
@@ -424,6 +446,8 @@
if (ideviceToReturn.isEmulator() && managedDevice.getEmulatorProcess() != null) {
try {
killEmulator(device);
+ // stop emulator output log
+ device.stopEmulatorOutput();
// emulator killed - return a stub device
// TODO: this is a bit of a hack. Consider having DeviceManager inject a StubDevice
// when deviceDisconnected event is received
@@ -484,12 +508,15 @@
try {
CLog.i("launching emulator with %s", fullArgs.toString());
- Process p = runUtil.runCmdInBackground(fullArgs);
+ SizeLimitedOutputStream emulatorOutput = new SizeLimitedOutputStream(
+ MAX_EMULATOR_OUTPUT, EMULATOR_OUTPUT, ".txt");
+ Process p = runUtil.runCmdInBackground(fullArgs, emulatorOutput);
// sleep a small amount to wait for process to start successfully
getRunUtil().sleep(500);
assertEmulatorProcessAlive(p);
- IManagedTestDevice managedDevice = (IManagedTestDevice)device;
- managedDevice.setEmulatorProcess(p);
+ TestDevice testDevice = (TestDevice) device;
+ testDevice.setEmulatorProcess(p);
+ testDevice.setEmulatorOutputStream(emulatorOutput);
} catch (IOException e) {
// TODO: is this the most appropriate exception to throw?
throw new DeviceNotAvailableException("Failed to start emulator process", e);
@@ -536,13 +563,13 @@
if (console != null) {
console.kill();
// check and wait for device to become not avail
- device.waitForDeviceNotAvailable(5*1000);
+ device.waitForDeviceNotAvailable(5 * 1000);
// lets ensure process is killed too - fall through
} else {
CLog.w("Could not get emulator console for %s", device.getSerialNumber());
}
// lets try killing the process
- Process emulatorProcess = ((IManagedTestDevice)device).getEmulatorProcess();
+ Process emulatorProcess = ((IManagedTestDevice) device).getEmulatorProcess();
if (emulatorProcess != null) {
emulatorProcess.destroy();
if (isProcessRunning(emulatorProcess)) {
@@ -551,7 +578,7 @@
forceKillProcess(emulatorProcess, device.getSerialNumber());
}
}
- if (!device.waitForDeviceNotAvailable(20*1000)) {
+ if (!device.waitForDeviceNotAvailable(20 * 1000)) {
throw new DeviceNotAvailableException(String.format("Failed to kill emulator %s",
device.getSerialNumber()));
}
@@ -572,7 +599,7 @@
f.setAccessible(true);
Integer pid = (Integer)f.get(emulatorProcess);
if (pid != null) {
- RunUtil.getDefault().runTimedCmd(5*1000, "kill", "-9", pid.toString());
+ RunUtil.getDefault().runTimedCmd(5 * 1000, "kill", "-9", pid.toString());
}
} catch (NoSuchFieldException e) {
CLog.d("got NoSuchFieldException when attempting to read process pid");
@@ -613,7 +640,7 @@
CLog.i("Reconnecting device %s to adb over tcpip", usbDevice.getSerialNumber());
ITestDevice tcpDevice = null;
if (usbDevice instanceof IManagedTestDevice) {
- IManagedTestDevice managedUsbDevice = (IManagedTestDevice)usbDevice;
+ IManagedTestDevice managedUsbDevice = (IManagedTestDevice) usbDevice;
String ipAndPort = managedUsbDevice.switchToAdbTcp();
if (ipAndPort != null) {
CLog.d("Device %s was switched to adb tcp on %s", usbDevice.getSerialNumber(),
@@ -655,7 +682,7 @@
}
CLog.w("Failed to connect to device on %s, attempt %d of 3. Response: %s.",
ipAndPort, i, adbConnectResult);
- getRunUtil().sleep(5*1000);
+ getRunUtil().sleep(5 * 1000);
}
return false;
}
@@ -682,13 +709,16 @@
@Override
public synchronized void terminate() {
checkInit();
- if (!mIsTerminated ) {
+ if (!mIsTerminated) {
mIsTerminated = true;
+ if (mDeviceRecoverer != null) {
+ mDeviceRecoverer.terminate();
+ }
mAdbBridge.removeDeviceChangeListener(mManagedDeviceListener);
mAdbBridge.terminate();
- if (mFastbootMonitor != null) {
- mFastbootMonitor.terminate();
- }
+ // We are not terminating mFastbootMonitor here since it is a daemon thread.
+ // Early terminating it can cause other threads to be blocked if they check
+ // fastboot state of a device.
}
}
@@ -819,7 +849,7 @@
desc.getProductVariant(),
desc.getBuildId(),
desc.getBatteryLevel())
- );
+ );
}
}
@@ -832,7 +862,6 @@
return o == null ? "unknown" : o.toString();
}
-
/**
* A class to listen for and act on device presence updates from ddmlib
*/
@@ -925,12 +954,16 @@
}
}
+ /**
+ * A class to monitor and update fastboot state of devices.
+ */
private class FastbootMonitor extends Thread {
private boolean mQuit = false;
FastbootMonitor() {
super("FastbootMonitor");
+ setDaemon(true);
}
public void terminate() {
@@ -940,11 +973,12 @@
@Override
public void run() {
+ final FastbootHelper fastboot = new FastbootHelper(getRunUtil());
while (!mQuit) {
// only poll fastboot devices if there are listeners, as polling it
// indiscriminately can cause fastboot commands to hang
if (!mFastbootListeners.isEmpty()) {
- Set<String> serials = getDevicesOnFastboot();
+ Set<String> serials = fastboot.getDevices();
if (serials != null) {
mManagedDeviceList.updateFastbootStates(serials);
@@ -962,28 +996,37 @@
}
}
- private Set<String> getDevicesOnFastboot() {
- CommandResult fastbootResult = getRunUtil().runTimedCmd(FASTBOOT_CMD_TIMEOUT,
- "fastboot", "devices");
- if (fastbootResult.getStatus().equals(CommandStatus.SUCCESS)) {
- CLog.v("fastboot devices returned\n %s",
- fastbootResult.getStdout());
- return parseDevicesOnFastboot(fastbootResult.getStdout());
- } else {
- CLog.w("'fastboot devices' failed. Result: %s, stderr: %s", fastbootResult.getStatus(),
- fastbootResult.getStderr());
- }
- return null;
- }
+ /**
+ * A class for a thread which performs periodic device recovery operations.
+ */
+ private class DeviceRecoverer extends Thread {
- static Set<String> parseDevicesOnFastboot(String fastbootOutput) {
- Set<String> serials = new HashSet<String>();
- Pattern fastbootPattern = Pattern.compile("([\\w\\d]+)\\s+fastboot\\s*");
- Matcher fastbootMatcher = fastbootPattern.matcher(fastbootOutput);
- while (fastbootMatcher.find()) {
- serials.add(fastbootMatcher.group(1));
+ private boolean mQuit = false;
+ private List<IMultiDeviceRecovery> mMultiDeviceRecoverers;
+
+ public DeviceRecoverer(List<IMultiDeviceRecovery> multiDeviceRecoverers) {
+ super("DeviceRecoverer");
+ mMultiDeviceRecoverers = multiDeviceRecoverers;
}
- return serials;
+
+ @Override
+ public void run() {
+ while (!mQuit) {
+ getRunUtil().sleep(mDeviceRecoveryInterval);
+ if (mMultiDeviceRecoverers != null && !mMultiDeviceRecoverers.isEmpty()) {
+ for (IMultiDeviceRecovery m : mMultiDeviceRecoverers) {
+ // Always fetch a list of devices prior to running the recovery.
+ List<DeviceDescriptor> devices = listAllDevices();
+ m.recoverDevices(devices);
+ }
+ }
+ }
+ }
+
+ public void terminate() {
+ mQuit = true;
+ interrupt();
+ }
}
@VisibleForTesting
diff --git a/src/com/android/tradefed/device/DeviceSelectionOptions.java b/src/com/android/tradefed/device/DeviceSelectionOptions.java
index 6476295..16f70ce 100644
--- a/src/com/android/tradefed/device/DeviceSelectionOptions.java
+++ b/src/com/android/tradefed/device/DeviceSelectionOptions.java
@@ -82,7 +82,7 @@
"--max-battery is specified, skip devices that have an unknown battery level. Note " +
"that this may leave restart-looping devices in limbo indefinitely without manual " +
"intervention.")
- private boolean mRequireBatteryCheck = false;
+ private boolean mRequireBatteryCheck = true;
@Option(name = "min-sdk-level", description = "Only run this test on devices that support " +
"this Android SDK/API level")
@@ -436,8 +436,8 @@
try {
// use default 5 minutes freshness
Future<Integer> batteryFuture = device.getBattery();
- // don't block on battery level, get cached value
- return batteryFuture.get(1, TimeUnit.MILLISECONDS);
+ // get cached value or wait up to 500ms for battery level query
+ return batteryFuture.get(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException |
java.util.concurrent.TimeoutException e) {
CLog.w("Failed to query battery level for %s: %s", device.getSerialNumber(),
diff --git a/src/com/android/tradefed/device/DeviceStateMonitor.java b/src/com/android/tradefed/device/DeviceStateMonitor.java
index 7a28fe7..6f31a50 100644
--- a/src/com/android/tradefed/device/DeviceStateMonitor.java
+++ b/src/com/android/tradefed/device/DeviceStateMonitor.java
@@ -46,6 +46,7 @@
/** the time in ms to wait between 'poll for responsiveness' attempts */
private static final long CHECK_POLL_TIME = 3 * 1000;
+ private static final long MAX_CHECK_POLL_TIME = 30 * 1000;
/** the maximum operation time in ms for a 'poll for responsiveness' command */
private static final int MAX_OP_TIME = 10 * 1000;
@@ -160,6 +161,7 @@
CLog.i("Waiting %d ms for device %s shell to be responsive", waitTime,
getSerialNumber());
long startTime = System.currentTimeMillis();
+ int counter = 1;
while (System.currentTimeMillis() - startTime < waitTime) {
final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
final String cmd = "ls /system/bin/adb";
@@ -178,7 +180,8 @@
} catch (ShellCommandUnresponsiveException e) {
CLog.i("%s failed: %s", cmd, e.getMessage());
}
- getRunUtil().sleep(CHECK_POLL_TIME);
+ getRunUtil().sleep(Math.min(CHECK_POLL_TIME * counter, MAX_CHECK_POLL_TIME));
+ counter++;
}
CLog.w("Device %s shell is unresponsive", getSerialNumber());
return false;
@@ -234,6 +237,7 @@
@Override
public boolean waitForBootComplete(final long waitTime) {
CLog.i("Waiting %d ms for device %s boot complete", waitTime, getSerialNumber());
+ int counter = 1;
long startTime = System.currentTimeMillis();
final String cmd = "getprop " + BOOTCOMPLETE_PROP;
while ((System.currentTimeMillis() - startTime) < waitTime) {
@@ -247,7 +251,8 @@
} catch (ExecutionException e) {
CLog.i("%s on device %s failed: %s", cmd, getSerialNumber(), e.getMessage());
}
- getRunUtil().sleep(CHECK_POLL_TIME);
+ getRunUtil().sleep(Math.min(CHECK_POLL_TIME * counter, MAX_CHECK_POLL_TIME));
+ counter++;
}
CLog.w("Device %s did not boot after %d ms", getSerialNumber(), waitTime);
return false;
@@ -264,6 +269,7 @@
CLog.i("Waiting %d ms for device %s package manager",
waitTime, getSerialNumber());
long startTime = System.currentTimeMillis();
+ int counter = 1;
while (System.currentTimeMillis() - startTime < waitTime) {
final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
final String cmd = "pm path android";
@@ -287,7 +293,8 @@
Log.i(LOG_TAG, String.format("%s on device %s failed: %s", cmd, getSerialNumber(),
e.getMessage()));
}
- getRunUtil().sleep(CHECK_POLL_TIME);
+ getRunUtil().sleep(Math.min(CHECK_POLL_TIME * counter, MAX_CHECK_POLL_TIME));
+ counter++;
}
Log.w(LOG_TAG, String.format("Device %s package manager is unresponsive",
getSerialNumber()));
@@ -305,6 +312,7 @@
Log.i(LOG_TAG, String.format("Waiting %d ms for device %s external store", waitTime,
getSerialNumber()));
long startTime = System.currentTimeMillis();
+ int counter = 1;
while (System.currentTimeMillis() - startTime < waitTime) {
final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
final CollectingOutputReceiver bitBucket = new CollectingOutputReceiver();
@@ -354,7 +362,8 @@
Log.w(LOG_TAG, String.format("Failed to get external store mount point for %s",
getSerialNumber()));
}
- getRunUtil().sleep(CHECK_POLL_TIME);
+ getRunUtil().sleep(Math.min(CHECK_POLL_TIME * counter, MAX_CHECK_POLL_TIME));
+ counter++;
}
Log.w(LOG_TAG, String.format("Device %s external storage is not mounted after %d ms",
getSerialNumber(), waitTime));
diff --git a/src/com/android/tradefed/device/FastbootHelper.java b/src/com/android/tradefed/device/FastbootHelper.java
new file mode 100644
index 0000000..d5098e7
--- /dev/null
+++ b/src/com/android/tradefed/device/FastbootHelper.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.IRunUtil;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A helper class for fastboot operations.
+ */
+public class FastbootHelper {
+
+ /** max wait time in ms for fastboot devices command to complete */
+ private static final long FASTBOOT_CMD_TIMEOUT = 1 * 60 * 1000;
+
+ private IRunUtil mRunUtil;
+
+ /**
+ * Constructor.
+ *
+ * @param runUtil a {@link IRunUtil}.
+ */
+ public FastbootHelper(final IRunUtil runUtil) {
+ if (runUtil == null) {
+ throw new IllegalArgumentException("runUtil cannot be null");
+ }
+ mRunUtil = runUtil;
+ }
+
+ /**
+ * Determine if fastboot is available for use.
+ */
+ public boolean isFastbootAvailable() {
+ // Run "fastboot help" to check the existence and the version of fastboot
+ // (Old versions do not support "help" command).
+ CommandResult fastbootResult = mRunUtil.runTimedCmdSilently(5000, "fastboot", "help");
+ if (fastbootResult.getStatus() == CommandStatus.SUCCESS) {
+ return true;
+ }
+ if (fastbootResult.getStderr() != null &&
+ fastbootResult.getStderr().indexOf("usage: fastboot") >= 0) {
+ CLog.logAndDisplay(LogLevel.WARN,
+ "You are running an older version of fastboot, please update it.");
+ return true;
+ }
+ CLog.d("fastboot not available. stdout: %s, stderr: %s",
+ fastbootResult.getStdout(), fastbootResult.getStderr());
+ return false;
+ }
+
+
+ /**
+ * Returns a set of device serials in fastboot mode.
+ *
+ * @return a set of device serials.
+ */
+ public Set<String> getDevices() {
+ CommandResult fastbootResult = mRunUtil.runTimedCmd(FASTBOOT_CMD_TIMEOUT,
+ "fastboot", "devices");
+ if (fastbootResult.getStatus().equals(CommandStatus.SUCCESS)) {
+ CLog.v("fastboot devices returned\n %s",
+ fastbootResult.getStdout());
+ return parseDevices(fastbootResult.getStdout());
+ } else {
+ CLog.w("'fastboot devices' failed. Result: %s, stderr: %s", fastbootResult.getStatus(),
+ fastbootResult.getStderr());
+ }
+ return null;
+ }
+
+ /**
+ * Parses the output of "fastboot devices" command.
+ * Exposed for unit testing.
+ *
+ * @param fastbootOutput the output of fastboot command.
+ * @return a set of device serials.
+ */
+ Set<String> parseDevices(String fastbootOutput) {
+ Set<String> serials = new HashSet<String>();
+ Pattern fastbootPattern = Pattern.compile("([\\w\\d]+)\\s+fastboot\\s*");
+ Matcher fastbootMatcher = fastbootPattern.matcher(fastbootOutput);
+ while (fastbootMatcher.find()) {
+ serials.add(fastbootMatcher.group(1));
+ }
+ return serials;
+ }
+
+ /**
+ * Executes a fastboot command on a device and return the output.
+ *
+ * @param serial a device serial.
+ * @param command a fastboot command to run.
+ * @return the output of the fastboot command. null if the command failed.
+ */
+ public String executeCommand(String serial, String command) {
+ final CommandResult fastbootResult = mRunUtil.runTimedCmd(FASTBOOT_CMD_TIMEOUT,
+ "fastboot", "-s", serial, command);
+ if (fastbootResult.getStatus() != CommandStatus.SUCCESS) {
+ CLog.w("'fastboot -s %s %s' failed. Result: %s, stderr: %s", serial, command,
+ fastbootResult.getStatus(), fastbootResult.getStderr());
+ return null;
+ }
+ return fastbootResult.getStdout();
+ }
+}
diff --git a/src/com/android/tradefed/device/IMultiDeviceRecovery.java b/src/com/android/tradefed/device/IMultiDeviceRecovery.java
new file mode 100644
index 0000000..351dd19
--- /dev/null
+++ b/src/com/android/tradefed/device/IMultiDeviceRecovery.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device;
+
+import com.android.tradefed.command.remote.DeviceDescriptor;
+import com.android.tradefed.config.GlobalConfiguration;
+
+import java.util.List;
+
+/**
+ * Interface for recovering multiple offline devices. There are some device recovery methods which
+ * can affect multiple devices (ex) restarting adb, resetting usb, ...). We can implement those
+ * recovery methods through this interface. Once the implementation is configured through
+ * {@link GlobalConfiguration}, {@link #recoverDevices(List<DeviceDescriptor>)} will be called
+ * periodically from {@link DeviceManager}.
+ */
+public interface IMultiDeviceRecovery {
+
+ /**
+ * Recovers offline devices on host.
+ *
+ * @param managedDevices a list of {@link DeviceDescriptor}s.
+ */
+ void recoverDevices(List<DeviceDescriptor> managedDevices);
+
+}
diff --git a/src/com/android/tradefed/device/ITestDevice.java b/src/com/android/tradefed/device/ITestDevice.java
index d659895..9624954 100644
--- a/src/com/android/tradefed/device/ITestDevice.java
+++ b/src/com/android/tradefed/device/ITestDevice.java
@@ -27,6 +27,7 @@
import java.io.File;
import java.io.InputStream;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
@@ -409,7 +410,7 @@
* <p/>
* If connection with device is lost before test run completes, and recovery succeeds, all
* listeners will be informed of testRunFailed and "false" will be returned. The test command
- * will not be rerun.. It is left to callers to retry if necessary.
+ * will not be rerun. It is left to callers to retry if necessary.
* <p/>
* If connection with device is lost before test run completes, and recovery fails, all
* listeners will be informed of testRunFailed and DeviceNotAvailableException will be thrown.
@@ -426,8 +427,8 @@
/**
* Convenience method for performing
- * {@link #runInstrumentationTests(IRemoteAndroidTestRunner, Collection)} with one or listeners
- * passed as parameters.
+ * {@link #runInstrumentationTests(IRemoteAndroidTestRunner, Collection)} with one or more
+ * listeners passed as parameters.
*
* @param runner the {@link IRemoteAndroidTestRunner} which runs the tests
* @param listeners the test result listener(s)
@@ -440,6 +441,22 @@
ITestRunListener... listeners) throws DeviceNotAvailableException;
/**
+ * Same as {@link ITestDevice#runInstrumentationTests(IRemoteAndroidTestRunner, Collection)}
+ * but runs the test for the given user.
+ */
+
+ public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
+ Collection<ITestRunListener> listeners) throws DeviceNotAvailableException;
+
+ /**
+ * Same as
+ * {@link ITestDevice#runInstrumentationTests(IRemoteAndroidTestRunner, ITestRunListener...)}
+ * but runs the test for a given user.
+ */
+ public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
+ ITestRunListener... listeners) throws DeviceNotAvailableException;
+
+ /**
* Install an Android package on device.
*
* @param packageFile the apk file to install
@@ -454,6 +471,59 @@
throws DeviceNotAvailableException;
/**
+ * Install an Android package on device.
+ * <p>Note: Only use cases that requires explicit control of granting runtime permission at
+ * install time should call this function.
+ * @param packageFile the apk file to install
+ * @param reinstall <code>true</code> if a reinstall should be performed
+ * @param grantPermissions if all runtime permissions should be granted at install time
+ * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
+ * available options.
+ * @return a {@link String} with an error code, or <code>null</code> if success.
+ * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+ * recovered.
+ * @throws UnsupportedOperationException if runtime permission is not supported by the platform
+ * on device.
+ */
+ public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
+ String... extraArgs) throws DeviceNotAvailableException;
+
+ /**
+ * Install an Android package on device for a given user.
+ *
+ * @param packageFile the apk file to install
+ * @param reinstall <code>true</code> if a reinstall should be performed
+ * @param userId the integer user id to install for.
+ * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
+ * available options.
+ * @return a {@link String} with an error code, or <code>null</code> if success.
+ * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+ * recovered.
+ */
+ public String installPackageForUser(File packageFile, boolean reinstall, int userId,
+ String... extraArgs) throws DeviceNotAvailableException;
+
+ /**
+ * Install an Android package on device for a given user.
+ * <p>Note: Only use cases that requires explicit control of granting runtime permission at
+ * install time should call this function.
+ * @param packageFile the apk file to install
+ * @param reinstall <code>true</code> if a reinstall should be performed
+ * @param grantPermissions if all runtime permissions should be granted at install time
+ * @param userId the integer user id to install for.
+ * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
+ * available options.
+ * @return a {@link String} with an error code, or <code>null</code> if success.
+ * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+ * recovered.
+ * @throws UnsupportedOperationException if runtime permission is not supported by the platform
+ * on device.
+ */
+ public String installPackageForUser(File packageFile, boolean reinstall,
+ boolean grantPermissions, int userId, String... extraArgs)
+ throws DeviceNotAvailableException;
+
+ /**
* Uninstall an Android package from device.
*
* @param packageName the Android package to uninstall
@@ -704,11 +774,18 @@
public InputStreamSource getScreenshot() throws DeviceNotAvailableException;
/**
+ * Clears the last connected wifi network. This should be called when starting a new invocation
+ * to avoid connecting to the wifi network used in the previous test after device reboots.
+ */
+ public void clearLastConnectedWifiNetwork();
+
+ /**
* Connects to a wifi network.
* <p/>
* Turns on wifi and blocks until a successful connection is made to the specified wifi network.
* Once a connection is made, the instance will try to restore the connection after every reboot
- * until {@link ITestDevice#disconnectFromWifi()} is called.
+ * until {@link ITestDevice#disconnectFromWifi()} or
+ * {@link ITestDevice#clearLastConnectedWifiNetwork()} is called.
*
* @param wifiSsid the wifi ssid to connect to
* @param wifiPsk PSK passphrase or null if unencrypted
@@ -1107,4 +1184,79 @@
* @throws DeviceNotAvailableException
*/
public boolean waitForBootComplete(long timeOut) throws DeviceNotAvailableException;
+
+ /**
+ * Determines if multi user is supported.
+ *
+ * @return true if multi user is supported, false otherwise
+ * @throws DeviceNotAvailableException
+ */
+ public boolean isMultiUserSupported() throws DeviceNotAvailableException;
+
+ /**
+ * Create a user with a given name.
+ *
+ * @param name of the user to create on the device
+ * @return the integer for the user id created
+ * @throws DeviceNotAvailableException
+ */
+ public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException;
+
+ /**
+ * Remove a given user from the device.
+ *
+ * @param userId of the user to remove
+ * @return true if we were succesful in removing the user, false otherwise.
+ * @throws DeviceNotAvailableException
+ */
+ public boolean removeUser(int userId) throws DeviceNotAvailableException;
+
+ /**
+ * Gets the list of users on the device. Defaults to null.
+ *
+ * @return the list of user ids or null if there was an error.
+ * @throws DeviceNotAvailableException
+ */
+ ArrayList<Integer> listUsers() throws DeviceNotAvailableException;
+
+ /**
+ * Get the maximum number of supported users. Defaults to 0.
+ *
+ * @return an integer indicating the number of supported users
+ * @throws DeviceNotAvailableException
+ */
+ public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException;
+
+ /**
+ * Starts a given user in the background if it is currently stopped. If the user is already
+ * running in the background, this method is a NOOP.
+ * @param userId of the user to start in the background
+ * @return true if the user was succesfully started in the background.
+ * @throws DeviceNotAvailableException
+ */
+ public boolean startUser(int userId) throws DeviceNotAvailableException;
+
+ /**
+ * Stops a given user. If the user is already stopped, this method is a NOOP.
+ * @param userId of the user to stop.
+ * @throws DeviceNotAvailableException
+ */
+ public void stopUser(int userId) throws DeviceNotAvailableException;
+
+ /**
+ * Get the stream of emulator stdout and stderr
+ * @return emulator output
+ */
+ public InputStreamSource getEmulatorOutput();
+
+ /**
+ * Close and delete the emulator output.
+ */
+ public void stopEmulatorOutput();
+
+ /**
+ * Make the system partition on the device writable. May reboot the device.
+ * @throws DeviceNotAvailableException
+ */
+ public void remountSystemWritable() throws DeviceNotAvailableException;
}
diff --git a/src/com/android/tradefed/device/NoDeviceException.java b/src/com/android/tradefed/device/NoDeviceException.java
new file mode 100644
index 0000000..4e81b20
--- /dev/null
+++ b/src/com/android/tradefed/device/NoDeviceException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device;
+
+/**
+ * Thrown when there's no device to execute a given command.
+ */
+@SuppressWarnings("serial")
+public class NoDeviceException extends Exception {
+ /**
+ * Creates a {@link NoDeviceException}.
+ */
+ public NoDeviceException() {
+ super();
+ }
+
+ /**
+ * Creates a {@link NoDeviceException}.
+ *
+ * @param msg a descriptive message.
+ */
+ public NoDeviceException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Creates a {@link NoDeviceException}.
+ *
+ * @param msg a descriptive message.
+ * @param cause the root {@link Throwable} that caused the device to become unavailable.
+ */
+ public NoDeviceException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/src/com/android/tradefed/device/StubDevice.java b/src/com/android/tradefed/device/StubDevice.java
index 6dc783b..09148a9 100644
--- a/src/com/android/tradefed/device/StubDevice.java
+++ b/src/com/android/tradefed/device/StubDevice.java
@@ -31,6 +31,8 @@
import com.google.common.util.concurrent.SettableFuture;
import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -134,6 +136,7 @@
* {@inheritDoc}
*/
@Override
+ @Deprecated
public Map<String, String> getProperties() {
return null;
}
@@ -150,6 +153,7 @@
* {@inheritDoc}
*/
@Override
+ @Deprecated
public int getPropertyCount() {
return 0;
}
@@ -163,6 +167,16 @@
throw new IOException("stub");
}
+ /* (not javadoc)
+ * The parent method has no javadoc, so it's invalid for us to attempt to inherit
+ */
+ @Override
+ public RawImage getScreenshot(long timeout, TimeUnit unit)
+ throws TimeoutException, AdbCommandRejectedException, IOException {
+
+ throw new IOException("stub");
+ }
+
/**
* {@inheritDoc}
*/
@@ -209,6 +223,15 @@
* {@inheritDoc}
*/
@Override
+ public void installPackages(List<String> apkFilePaths, int timeOutInMs, boolean reinstall,
+ String... extraArgs) throws InstallException {
+ throw new InstallException(new IOException("stub"));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public String installRemotePackage(String remoteFilePath, boolean reinstall,
String... extraArgs) throws InstallException {
throw new InstallException(new IOException("stub"));
@@ -329,6 +352,7 @@
* {@inheritDoc}
*/
@Override
+ @Deprecated
public String getPropertySync(String name) throws TimeoutException,
AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
return null;
@@ -346,6 +370,7 @@
* {@inheritDoc}
*/
@Override
+ @Deprecated
public String getPropertyCacheOrSync(String name) throws TimeoutException,
AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
return null;
@@ -355,6 +380,7 @@
* {@inheritDoc}
*/
@Override
+ @Deprecated
public Integer getBatteryLevel() throws TimeoutException, AdbCommandRejectedException,
IOException, ShellCommandUnresponsiveException {
return null;
@@ -364,6 +390,7 @@
* {@inheritDoc}
*/
@Override
+ @Deprecated
public Integer getBatteryLevel(long freshnessMs) throws TimeoutException,
AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException {
return null;
@@ -423,6 +450,7 @@
public void startScreenRecorder(String remoteFilePath, ScreenRecorderOptions options,
IShellOutputReceiver receiver) throws TimeoutException, AdbCommandRejectedException,
IOException, ShellCommandUnresponsiveException {
+ // no-op
}
/* (non-Javadoc)
@@ -460,4 +488,36 @@
public Future<Integer> getBattery(long freshnessTime, TimeUnit timeUnit) {
return getBattery();
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<String> getAbis() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getDensity() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLanguage() {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getRegion() {
+ return null;
+ }
}
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index e075343..cd2f834 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -31,10 +31,12 @@
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.SnapshotInputStreamSource;
import com.android.tradefed.result.StubTestRunListener;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.CommandResult;
@@ -42,6 +44,7 @@
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.SizeLimitedOutputStream;
import com.android.tradefed.util.StreamUtil;
import java.awt.image.BufferedImage;
@@ -58,6 +61,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -80,6 +84,8 @@
private static final String TEST_INPUT_CMD = "dumpsys input";
static final String LIST_PACKAGES_CMD = "pm list packages -f";
private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)");
+ private static final Pattern PING_REGEX = Pattern.compile(
+ "(?<send>\\d+) packets transmitted, (?<recv>\\d+) received, (?<loss>\\d+)% packet loss");
/** regex to match input dispatch readiness line **/
static final Pattern INPUT_DISPATCH_STATE_REGEX =
Pattern.compile("DispatchEnabled:\\s?([01])");
@@ -95,7 +101,7 @@
/** Encrypting with inplace can take up to 2 hours. */
private static final int ENCRYPTION_INPLACE_TIMEOUT_MIN = 2 * 60;
/** Encrypting with wipe can take up to 5 minutes. */
- private static final int ENCRYPTION_WIPE_TIMEOUT_MIN = 5;
+ private static final long ENCRYPTION_WIPE_TIMEOUT_MIN = 20;
/** Timeout to wait for input dispatch to become ready **/
private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000;
/** Beginning of the string returned by vdc for "vdc cryptfs enablecrypto". */
@@ -115,14 +121,23 @@
/** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
static final String DISMISS_DIALOG_CMD = "input keyevent 23";
- private static final String BUILD_ID_PROP = "ro.build.version.incremental";
+ static final String BUILD_ID_PROP = "ro.build.version.incremental";
private static final String PRODUCT_NAME_PROP = "ro.product.name";
private static final String BUILD_TYPE_PROP = "ro.build.type";
private static final String BUILD_ALIAS_PROP = "ro.build.id";
+ private static final String BUILD_FLAVOR = "ro.build.flavor";
+
+ static final String BUILD_CODENAME_PROP = "ro.build.version.codename";
/** The network monitoring interval in ms. */
private static final int NETWORK_MONITOR_INTERVAL = 10 * 1000;
+ /** Wifi reconnect check interval in ms. */
+ private static final int WIFI_RECONNECT_CHECK_INTERVAL = 1 * 1000;
+
+ /** Wifi reconnect timeout in ms. */
+ private static final int WIFI_RECONNECT_TIMEOUT = 60 * 1000;
+
/** The time in ms to wait for a command to complete. */
private int mCmdTimeout = 2 * 60 * 1000;
/** The time in ms to wait for a 'long' command to complete. */
@@ -134,11 +149,11 @@
private TestDeviceState mState = TestDeviceState.ONLINE;
private final ReentrantLock mFastbootLock = new ReentrantLock();
private LogcatReceiver mLogcatReceiver;
- private IFileEntry mRootFile = null;
private boolean mFastbootEnabled = true;
private TestDeviceOptions mOptions = new TestDeviceOptions();
private Process mEmulatorProcess;
+ private SizeLimitedOutputStream mEmulatorOutput;
private RecoveryMode mRecoveryMode = RecoveryMode.AVAILABLE;
@@ -148,8 +163,8 @@
private DeviceAllocationState mAllocationState = DeviceAllocationState.Unknown;
private IDeviceMonitor mAllocationMonitor = null;
- private String mWifiSsid = null;
- private String mWifiPsk = null;
+ private String mLastConnectedWifiSsid = null;
+ private String mLastConnectedWifiPsk = null;
private boolean mNetworkMonitorEnabled = false;
/**
@@ -485,6 +500,10 @@
*/
@Override
public String getBuildFlavor() throws DeviceNotAvailableException {
+ String buildFlavor = getProperty(BUILD_FLAVOR);
+ if (buildFlavor != null && !buildFlavor.isEmpty()) {
+ return buildFlavor;
+ }
String productName = getProperty(PRODUCT_NAME_PROP);
String buildType = getProperty(BUILD_TYPE_PROP);
if (productName == null || buildType == null) {
@@ -584,6 +603,56 @@
return result;
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean runInstrumentationTestsAsUser(final IRemoteAndroidTestRunner runner,
+ int userId, final Collection<ITestRunListener> listeners)
+ throws DeviceNotAvailableException {
+ String oldRunTimeOptions = appendUserRunTimeOptionToRunner(runner, userId);
+ boolean result = runInstrumentationTests(runner, listeners);
+ resetUserRunTimeOptionToRunner(runner, oldRunTimeOptions);
+ return result;
+ }
+
+ /**
+ * Helper method to add user run time option to {@link RemoteAndroidTestRunner}
+ *
+ * @param runner {@link IRemoteAndroidTestRunner}
+ * @param userId the integer of the user id to run as.
+ * @return original run time options.
+ */
+ private String appendUserRunTimeOptionToRunner(final IRemoteAndroidTestRunner runner, int userId) {
+ if (runner instanceof RemoteAndroidTestRunner) {
+ String original = ((RemoteAndroidTestRunner) runner).getRunOptions();
+ String userRunTimeOption = String.format("--user %s", Integer.toString(userId));
+ ((RemoteAndroidTestRunner) runner).setRunOptions(userRunTimeOption);
+ return original;
+ } else {
+ throw new IllegalStateException(String.format("%s runner does not support multi-user",
+ runner.getClass().getName()));
+ }
+ }
+
+ /**
+ * Helper method to reset the run time options to {@link RemoteAndroidTestRunner}
+ *
+ * @param runner {@link IRemoteAndroidTestRunner}
+ * @param oldRunTimeOptions
+ */
+ private void resetUserRunTimeOptionToRunner(final IRemoteAndroidTestRunner runner,
+ String oldRunTimeOptions) {
+ if (runner instanceof RemoteAndroidTestRunner) {
+ if (oldRunTimeOptions != null) {
+ ((RemoteAndroidTestRunner) runner).setRunOptions(oldRunTimeOptions);
+ }
+ } else {
+ throw new IllegalStateException(String.format("%s runner does not support multi-user",
+ runner.getClass().getName()));
+ }
+ }
+
private static class RunFailureListener extends StubTestRunListener {
private boolean mIsRunFailure = false;
@@ -612,15 +681,68 @@
* {@inheritDoc}
*/
@Override
- public String installPackage(final File packageFile, final boolean reinstall,
- final String... extraArgs) throws DeviceNotAvailableException {
+ public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
+ ITestRunListener... listeners) throws DeviceNotAvailableException {
+ String oldRunTimeOptions = appendUserRunTimeOptionToRunner(runner, userId);
+ boolean result = runInstrumentationTests(runner, listeners);
+ resetUserRunTimeOptionToRunner(runner, oldRunTimeOptions);
+ return result;
+ }
+
+ /**
+ * Check whether platform on device supports runtime permission granting
+ * @return
+ * @throws DeviceNotAvailableException
+ */
+ boolean isRuntimePermissionSupported() throws DeviceNotAvailableException {
+ //TODO: change to API Level check once M is official
+ String codeName = getProperty(BUILD_CODENAME_PROP).trim();
+ if (!"MNC".equals(codeName)) {
+ // not MNC, probably REL or LMP or older
+ return false;
+ }
+ try {
+ long buildNumber = Long.parseLong(getBuildId());
+ // for platform commit 429270c3ed1da02914efb476be977dc3829d4c30
+ return buildNumber >= 1837705;
+ } catch (NumberFormatException nfe) {
+ // build id field is not a number, probably an eng build since we've already checked
+ // for MNC code name, assuming supported
+ return true;
+ }
+ }
+
+ /**
+ * helper method to throw exception if runtime permission isn't supported
+ * @throws DeviceNotAvailableException
+ */
+ private void ensureRuntimePermissionSupported() throws DeviceNotAvailableException {
+ boolean runtimePermissionSupported = isRuntimePermissionSupported();
+ if (!runtimePermissionSupported) {
+ throw new UnsupportedOperationException(
+ "platform on device does not support runtime permission granting!");
+ }
+ }
+
+ /**
+ * Core implementation of package installation, with retries around
+ * {@link IDevice#installPackage(String, boolean, String...)}
+ * @param packageFile
+ * @param reinstall
+ * @param extraArgs
+ * @return
+ * @throws DeviceNotAvailableException
+ */
+ private String internalInstallPackage(
+ final File packageFile, final boolean reinstall, final List<String> extraArgs)
+ throws DeviceNotAvailableException {
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction installAction = new DeviceAction() {
@Override
public boolean run() throws InstallException {
String result = getIDevice().installPackage(packageFile.getAbsolutePath(),
- reinstall, extraArgs);
+ reinstall, extraArgs.toArray(new String[]{}));
response[0] = result;
return result == null;
}
@@ -633,6 +755,69 @@
/**
* {@inheritDoc}
*/
+ @Override
+ public String installPackage(final File packageFile, final boolean reinstall,
+ final String... extraArgs) throws DeviceNotAvailableException {
+ boolean runtimePermissionSupported = isRuntimePermissionSupported();
+ List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
+ // grant all permissions by default if feature is supported
+ if (runtimePermissionSupported) {
+ args.add("-g");
+ }
+ return internalInstallPackage(packageFile, reinstall, args);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
+ String... extraArgs) throws DeviceNotAvailableException {
+ ensureRuntimePermissionSupported();
+ List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
+ if (grantPermissions) {
+ args.add("-g");
+ }
+ return internalInstallPackage(packageFile, reinstall, args);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String installPackageForUser(File packageFile, boolean reinstall, int userId,
+ String... extraArgs) throws DeviceNotAvailableException {
+ boolean runtimePermissionSupported = isRuntimePermissionSupported();
+ List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
+ // grant all permissions by default if feature is supported
+ if (runtimePermissionSupported) {
+ args.add("-g");
+ }
+ args.add("--user");
+ args.add(Integer.toString(userId));
+ return internalInstallPackage(packageFile, reinstall, args);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String installPackageForUser(File packageFile, boolean reinstall,
+ boolean grantPermissions, int userId, String... extraArgs)
+ throws DeviceNotAvailableException {
+ ensureRuntimePermissionSupported();
+ List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
+ if (grantPermissions) {
+ args.add("-g");
+ }
+ args.add("--user");
+ args.add(Integer.toString(userId));
+ return internalInstallPackage(packageFile, reinstall, args);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
public String installPackage(final File packageFile, final File certFile,
final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
// use array to store response, so it can be returned to caller
@@ -842,7 +1027,13 @@
CLog.i("Checking free space for %s", getSerialNumber());
String externalStorePath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
String output = getDfOutput(externalStorePath);
- Long available = parseFreeSpaceFromAvailable(output);
+ // Try coreutils/toybox style output first.
+ Long available = parseFreeSpaceFromModernOutput(externalStorePath, output);
+ if (available != null) {
+ return available;
+ }
+ // Then the two legacy toolbox formats.
+ available = parseFreeSpaceFromAvailable(output);
if (available != null) {
return available;
}
@@ -875,7 +1066,8 @@
}
/**
- * Parses a partitions available space from the legacy output of a 'df' command.
+ * Parses a partition's available space from the legacy output of a 'df' command, used
+ * pre-gingerbread.
* <p/>
* Assumes output format of:
* <br>/
@@ -900,7 +1092,8 @@
}
/**
- * Parses a partitions available space from the 'table-formatted' output of a 'df' command.
+ * Parses a partition's available space from the 'table-formatted' output of a toolbox 'df'
+ * command, used from gingerbread to lollipop.
* <p/>
* Assumes output format of:
* <br/>
@@ -937,6 +1130,35 @@
}
/**
+ * Parses a partition's available space from the modern coreutils/toybox 'df' output, used
+ * after lollipop.
+ * <p/>
+ * Assumes output format of:
+ * <br/>
+ * <code>
+ * Filesystem 1K-blocks Used Available Use% Mounted on
+ * <br/>
+ * /dev/fuse 11585536 1316348 10269188 12% /mnt/shell/emulated
+ * </code>
+ * @param dfOutput the output of df command to parse
+ * @return the available space in kilobytes or <code>null</code> if output could not be parsed
+ */
+ Long parseFreeSpaceFromModernOutput(String externalStorePath, String dfOutput) {
+ final Pattern pattern = Pattern.compile(String.format(
+ //Fs 1K-blks Used Available Use% Mounted on
+ "\\s+\\d+\\s+\\d+\\s+(\\d+)\\s+\\d+%%\\s+%s", externalStorePath));
+ Matcher matcher = pattern.matcher(dfOutput);
+ if (matcher.find()) {
+ try {
+ return Long.parseLong(matcher.group(1));
+ } catch (NumberFormatException e) {
+ // fall through
+ }
+ }
+ return null;
+ }
+
+ /**
* {@inheritDoc}
*/
@Override
@@ -950,7 +1172,7 @@
@Override
public List<MountPointInfo> getMountPointInfo() throws DeviceNotAvailableException {
final String mountInfo = executeShellCommand("cat /proc/mounts");
- final String[] mountInfoLines = mountInfo.split("\r\n");
+ final String[] mountInfoLines = mountInfo.split("\r?\n");
List<MountPointInfo> list = new ArrayList<MountPointInfo>(mountInfoLines.length);
for (String line : mountInfoLines) {
@@ -983,11 +1205,9 @@
public IFileEntry getFileEntry(String path) throws DeviceNotAvailableException {
path = interpolatePathVariables(path);
String[] pathComponents = path.split(FileListingService.FILE_SEPARATOR);
- if (mRootFile == null) {
- FileListingService service = getFileListingService();
- mRootFile = new FileEntryWrapper(this, service.getRoot());
- }
- return FileEntryWrapper.getDescendant(mRootFile, Arrays.asList(pathComponents));
+ FileListingService service = getFileListingService();
+ IFileEntry rootFile = new FileEntryWrapper(this, service.getRoot());
+ return FileEntryWrapper.getDescendant(rootFile, Arrays.asList(pathComponents));
}
/**
@@ -1070,7 +1290,7 @@
deviceFilePath = String.format("%s/%s", interpolatePathVariables(deviceFilePath),
localFileDir.getName());
if (!doesFileExist(deviceFilePath)) {
- executeShellCommand(String.format("mkdir %s", deviceFilePath));
+ executeShellCommand(String.format("mkdir -p %s", deviceFilePath));
}
IFileEntry remoteFileEntry = getFileEntry(deviceFilePath);
if (remoteFileEntry == null) {
@@ -1697,13 +1917,26 @@
* {@inheritDoc}
*/
@Override
+ public void clearLastConnectedWifiNetwork() {
+ mLastConnectedWifiSsid = null;
+ mLastConnectedWifiPsk = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk)
throws DeviceNotAvailableException {
// Clears the last connected wifi network.
- mWifiSsid = null;
- mWifiPsk = null;
+ mLastConnectedWifiSsid = null;
+ mLastConnectedWifiPsk = null;
+ // Connects to wifi network. It retries up to {@link TestDeviceOptions@getWifiAttempts()}
+ // times and uses binary exponential back-offs when retrying.
+ Random rnd = new Random();
+ int backoffSlotCount = 2;
IWifiHelper wifi = createWifiHelper();
for (int i = 1; i <= mOptions.getWifiAttempts(); i++) {
CLog.i("Connecting to wifi network %s on %s", wifiSsid, getSerialNumber());
@@ -1714,8 +1947,8 @@
CLog.i("Successfully connected to wifi network %s(%s) on %s",
wifiSsid, wifiInfo.get("bssid"), getSerialNumber());
- mWifiSsid = wifiSsid;
- mWifiPsk = wifiPsk;
+ mLastConnectedWifiSsid = wifiSsid;
+ mLastConnectedWifiPsk = wifiPsk;
return true;
} else {
@@ -1723,17 +1956,35 @@
wifiSsid, wifiInfo.get("bssid"), getSerialNumber(), i,
mOptions.getWifiAttempts());
}
+
+ if (i < mOptions.getWifiAttempts()) {
+ int waitTime = rnd.nextInt(backoffSlotCount) * mOptions.getWifiRetryWaitTime();
+ backoffSlotCount *= 2;
+ CLog.i("Waiting for %d ms before reconnecting to %s...", waitTime, wifiSsid);
+ getRunUtil().sleep(waitTime);
+ }
}
return false;
}
/**
- * Check that device has network connectivity.
+ * {@inheritDoc}
*/
@Override
public boolean checkConnectivity() throws DeviceNotAvailableException {
- final IWifiHelper wifi = createWifiHelper();
- return wifi.checkConnectivity(mOptions.getConnCheckUrl());
+ final int pingLoss = getPingLoss();
+ return (0 <= pingLoss && pingLoss < 100);
+ }
+
+ int getPingLoss() throws DeviceNotAvailableException {
+ final String output = executeShellCommand(
+ "ping -c 1 -w 5 -s 1024 " + mOptions.getPingIpOrHost());
+ final Matcher stat = PING_REGEX.matcher(output);
+ if (stat.find()) {
+ return Integer.parseInt(stat.group("loss"));
+ }
+ // Return -1 if we failed to parse output.
+ return -1;
}
/**
@@ -1803,8 +2054,8 @@
public boolean disconnectFromWifi() throws DeviceNotAvailableException {
CLog.i("Disconnecting from wifi on %s", getSerialNumber());
// Clears the last connected wifi network.
- mWifiSsid = null;
- mWifiPsk = null;
+ mLastConnectedWifiSsid = null;
+ mLastConnectedWifiPsk = null;
IWifiHelper wifi = createWifiHelper();
return wifi.disconnectFromNetwork();
@@ -1944,14 +2195,20 @@
if (mOptions.isDisableKeyguard()) {
disableKeyguard();
}
- if (mWifiSsid != null) {
- // mWifiSsid is set to null if connection fails
- final String wifiSsid = mWifiSsid;
- if (!connectToWifiNetworkIfNeeded(mWifiSsid, mWifiPsk)) {
- throw new NetworkNotAvailableException(
- String.format("Failed to connect to wifi network %s on %s after reboot",
- wifiSsid, getSerialNumber()));
- }
+ for (String command : mOptions.getPostBootCommands()) {
+ executeShellCommand(command);
+ }
+ }
+
+ /**
+ * Ensure wifi connection is re-established after boot. This is intended to be called after TF
+ * initiated reboots(ones triggered by {@link #reboot()}) only.
+ *
+ * @throws DeviceNotAvailableException
+ */
+ void postBootWifiSetup() throws DeviceNotAvailableException {
+ if (mLastConnectedWifiSsid != null) {
+ reconnectToWifiNetwork();
}
if (mNetworkMonitorEnabled) {
if (!enableNetworkMonitor()) {
@@ -1960,6 +2217,28 @@
}
}
+ void reconnectToWifiNetwork() throws DeviceNotAvailableException {
+ // First, wait for wifi to re-connect automatically.
+ long startTime = System.currentTimeMillis();
+ boolean isConnected = checkConnectivity();
+ while (!isConnected && (System.currentTimeMillis() - startTime) < WIFI_RECONNECT_TIMEOUT) {
+ getRunUtil().sleep(WIFI_RECONNECT_CHECK_INTERVAL);
+ isConnected = checkConnectivity();
+ }
+
+ if (isConnected) {
+ return;
+ }
+
+ // If wifi is still not connected, try to re-connect on our own.
+ final String wifiSsid = mLastConnectedWifiSsid;
+ if (!connectToWifiNetworkIfNeeded(mLastConnectedWifiSsid, mLastConnectedWifiPsk)) {
+ throw new NetworkNotAvailableException(
+ String.format("Failed to connect to wifi network %s on %s after reboot",
+ wifiSsid, getSerialNumber()));
+ }
+ }
+
// TODO: consider exposing this method
private void postOnlineSetup() throws DeviceNotAvailableException {
if (isEnableAdbRoot()) {
@@ -2074,6 +2353,7 @@
if (mStateMonitor.waitForDeviceAvailable(mOptions.getRebootTimeout()) != null) {
postBootSetup();
+ postBootWifiSetup();
return;
} else {
recoverDevice();
@@ -2154,6 +2434,11 @@
// 1. device API level >= 18
// 2. has adb root
// 3. framework is running
+ if (!isEnableAdbRoot()) {
+ CLog.i("framework reboot is not supported; when enable root is disabled");
+ return false;
+ }
+ enableAdbRoot();
if (getApiLevel() >= 18 && isAdbRoot()) {
try {
// check framework running
@@ -2275,7 +2560,7 @@
enableAdbRoot();
String encryptMethod;
- int timeout;
+ long timeout;
if (inplace) {
encryptMethod = "inplace";
timeout = ENCRYPTION_INPLACE_TIMEOUT_MIN;
@@ -2327,8 +2612,8 @@
if (!mOptions.getUseFastbootErase()) {
rebootIntoBootloader();
fastbootWipePartition("userdata");
- reboot();
-
+ rebootUntilOnline();
+ waitForDeviceAvailable(ENCRYPTION_WIPE_TIMEOUT_MIN * 60 * 1000);
return true;
}
@@ -2337,7 +2622,7 @@
String output = executeShellCommand("vdc volume list");
String[] splitOutput;
if (output != null) {
- splitOutput = output.split("\r\n");
+ splitOutput = output.split("\r?\n");
for (String line : splitOutput) {
if (line.startsWith("110 ") && line.contains("sdcard /mnt/sdcard") &&
!line.endsWith("0")) {
@@ -2371,7 +2656,7 @@
setRecoveryMode(cachedRecoveryMode);
return false;
}
- splitOutput = output.split("\r\n");
+ splitOutput = output.split("\r?\n");
if (!splitOutput[splitOutput.length - 1].startsWith("200 ")) {
CLog.e("Command vdc volume format sdcard failed for device %s:\n%s",
getSerialNumber(), output);
@@ -2664,6 +2949,46 @@
}
/**
+ * For emulator set {@link SizeLimitedOutputStream} to log output
+ * @param output to log the output
+ */
+ public void setEmulatorOutputStream(SizeLimitedOutputStream output) {
+ mEmulatorOutput = output;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stopEmulatorOutput() {
+ if (mEmulatorOutput != null) {
+ mEmulatorOutput.delete();
+ mEmulatorOutput = null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStreamSource getEmulatorOutput() {
+ if (getIDevice().isEmulator()) {
+ if (mEmulatorOutput == null) {
+ CLog.w("Emulator output for %s was not captured in background",
+ getSerialNumber());
+ } else {
+ try {
+ return new SnapshotInputStreamSource(mEmulatorOutput.getData());
+ } catch (IOException e) {
+ CLog.e("Failed to get %s data.", getSerialNumber());
+ CLog.e(e);
+ }
+ }
+ }
+ return new ByteArrayInputStreamSource(new byte[0]);
+ }
+
+ /**
* {@inheritDoc}
*/
@Override
@@ -2862,4 +3187,135 @@
public boolean waitForBootComplete(long timeOut) throws DeviceNotAvailableException {
return mStateMonitor.waitForBootComplete(timeOut);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
+ String command = "pm list users";
+ String commandOutput = executeShellCommand(command);
+ // Extract the id of all existing users.
+ String[] lines = commandOutput.split("\\r?\\n");
+ if (lines.length < 1) {
+ CLog.e("%s should contain at least one line", commandOutput);
+ return null;
+ }
+ if (!lines[0].equals("Users:")) {
+ CLog.e("%s in not a valid output for 'pm list users'", commandOutput);
+ return null;
+ }
+ ArrayList<Integer> users = new ArrayList<Integer>();
+ for (int i = 1; i < lines.length; i++) {
+ // Individual user is printed out like this:
+ // \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running]
+ String[] tokens = lines[i].split("\\{|\\}|:");
+ if (tokens.length != 4 && tokens.length != 5) {
+ CLog.e("%s doesn't contain 4 or 5 tokens", lines[i]);
+ return null;
+ }
+ users.add(Integer.parseInt(tokens[1]));
+ }
+ return users;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
+ String command = "pm get-max-users";
+ String commandOutput = executeShellCommand(command);
+ try {
+ return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
+ } catch (NumberFormatException e) {
+ CLog.e("Failed to parse result: %s", commandOutput);
+ }
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isMultiUserSupported() throws DeviceNotAvailableException {
+ return getMaxNumberOfUsersSupported() > 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException {
+ final String output = executeShellCommand(String.format("pm create-user %s", name));
+ if (output.startsWith("Success")) {
+ try {
+ return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
+ } catch (NumberFormatException e) {
+ CLog.e("Failed to parse result: %s", output);
+ }
+ } else {
+ CLog.e("Failed to create user: %s", output);
+ }
+ throw new IllegalStateException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean removeUser(int userId) throws DeviceNotAvailableException {
+ final String output = executeShellCommand(String.format("pm remove-user %s", userId));
+ if (output.startsWith("Error")) {
+ CLog.e("Failed to remove user: %s", output);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean startUser(int userId) throws DeviceNotAvailableException {
+ final String output = executeShellCommand(String.format("am start-user %s", userId));
+ if (output.startsWith("Error")) {
+ CLog.e("Failed to start user: %s", output);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stopUser(int userId) throws DeviceNotAvailableException {
+ // No error or status code is returned.
+ executeShellCommand(String.format("am stop-user %s", userId));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void remountSystemWritable() throws DeviceNotAvailableException {
+ String verity = getProperty("partition.system.verified");
+ long verityVersion = 0;
+ if (verity != null && !verity.isEmpty()) {
+ try {
+ verityVersion = Long.parseLong(verity);
+ } catch (NumberFormatException nfe) {
+ // ignore but assign an arbitrary number since it's non-empty
+ CLog.d("unrecognized property value partition.system.verified=%s", verity);
+ verityVersion = 1;
+ }
+ }
+ if (verityVersion > 0) {
+ executeAdbCommand("disable-verity");
+ reboot();
+ }
+ executeAdbCommand("remount");
+ waitForDeviceAvailable();
+ }
}
diff --git a/src/com/android/tradefed/device/TestDeviceOptions.java b/src/com/android/tradefed/device/TestDeviceOptions.java
index a657e32..b7b43a4 100644
--- a/src/com/android/tradefed/device/TestDeviceOptions.java
+++ b/src/com/android/tradefed/device/TestDeviceOptions.java
@@ -17,6 +17,9 @@
import com.android.tradefed.config.Option;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Container for {@link ITestDevice} {@link Option}s
*/
@@ -82,6 +85,19 @@
description = "default number of attempts to connect to wifi network.")
private int mWifiAttempts = 5;
+ @Option(name = "wifi-retry-wait-time",
+ description = "the base wait time in ms between wifi connect retries. "
+ + "The actual wait time would be a multiple of this value.")
+ private int mWifiRetryWaitTime = 60 * 1000;
+
+ @Option(name = "post-boot-command",
+ description = "shell command to run after reboots during invocation")
+ private List<String> mPostBootCommands = new ArrayList<String>();
+
+ @Option(name = "cutoff-battery", description =
+ "the minimum battery level required to continue the invocation. Scale: 0-100")
+ private Integer mCutoffBattery = null;
+
/**
* Check whether adb root should be enabled on boot for this device
*/
@@ -269,4 +285,24 @@
mWifiAttempts = wifiAttempts;
}
+ /**
+ * @return the base wait time between wifi connect retries.
+ */
+ public int getWifiRetryWaitTime() {
+ return mWifiRetryWaitTime;
+ }
+
+ /**
+ * @return a list of shell commands to run after reboots.
+ */
+ public List<String> getPostBootCommands() {
+ return mPostBootCommands;
+ }
+
+ /**
+ * @return the minimum battery level to continue the invocation.
+ */
+ public Integer getCutoffBattery() {
+ return mCutoffBattery;
+ }
}
diff --git a/src/com/android/tradefed/device/WaitDeviceRecovery.java b/src/com/android/tradefed/device/WaitDeviceRecovery.java
index 823b2aa..7a97952 100644
--- a/src/com/android/tradefed/device/WaitDeviceRecovery.java
+++ b/src/com/android/tradefed/device/WaitDeviceRecovery.java
@@ -21,6 +21,8 @@
import com.android.ddmlib.TimeoutException;
import com.android.tradefed.config.Option;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
@@ -58,6 +60,10 @@
description="maximum time in ms to wait for device shell to be responsive.")
protected long mShellWaitTime = 30 * 1000;
+ @Option(name="fastboot-wait-time",
+ description="maximum time in ms to wait for a fastboot command result.")
+ protected long mFastbootWaitTime = 30 * 1000;
+
@Option(name = "min-battery-after-recovery",
description = "require a min battery level after successful recovery, " +
"default to 0 for ignoring.")
@@ -106,7 +112,7 @@
"Found device %s in fastboot but expected online. Rebooting...",
monitor.getSerialNumber()));
// TODO: retry if failed
- getRunUtil().runTimedCmd(20*1000, "fastboot", "-s", monitor.getSerialNumber(),
+ getRunUtil().runTimedCmd(mFastbootWaitTime, "fastboot", "-s", monitor.getSerialNumber(),
"reboot");
}
@@ -115,7 +121,7 @@
if (device == null) {
handleDeviceNotAvailable(monitor, recoverUntilOnline);
// function returning implies that recovery is successful, check battery level here
- checkMinBatteryLevel(device);
+ checkMinBatteryLevel(getDeviceAfterRecovery(monitor));
return;
}
// occasionally device is erroneously reported as online - double check that we can shell
@@ -123,7 +129,7 @@
if (!monitor.waitForDeviceShell(mShellWaitTime)) {
// treat this as a not available device
handleDeviceNotAvailable(monitor, recoverUntilOnline);
- checkMinBatteryLevel(device);
+ checkMinBatteryLevel(getDeviceAfterRecovery(monitor));
return;
}
@@ -135,7 +141,17 @@
}
// do a final check here when all previous if blocks are skipped or the last
// handleDeviceUnresponsive was successful
- checkMinBatteryLevel(device);
+ checkMinBatteryLevel(getDeviceAfterRecovery(monitor));
+ }
+
+ private IDevice getDeviceAfterRecovery(IDeviceStateMonitor monitor)
+ throws DeviceNotAvailableException {
+ IDevice device = monitor.waitForDeviceOnline();
+ if (device == null) {
+ throw new DeviceNotAvailableException(
+ "Device still not online after successful recovery");
+ }
+ return device;
}
/**
@@ -262,7 +278,7 @@
CLog.i("Found device %s in fastboot but potentially unresponsive.",
monitor.getSerialNumber());
// TODO: retry reboot
- getRunUtil().runTimedCmd(20*1000, "fastboot", "-s", monitor.getSerialNumber(),
+ getRunUtil().runTimedCmd(mFastbootWaitTime, "fastboot", "-s", monitor.getSerialNumber(),
"reboot-bootloader");
// wait for device to reboot
monitor.waitForDeviceNotAvailable(20*1000);
@@ -270,6 +286,13 @@
throw new DeviceNotAvailableException(String.format(
"Device %s not in bootloader after reboot", monitor.getSerialNumber()));
}
+ // running a meaningless command just to see whether the device is responsive.
+ CommandResult result = getRunUtil().runTimedCmd(mFastbootWaitTime, "fastboot", "-s",
+ monitor.getSerialNumber(), "getvar", "product");
+ if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
+ throw new DeviceNotAvailableException(String.format(
+ "Device %s is in fastboot but unresponsive", monitor.getSerialNumber()));
+ }
}
/**
diff --git a/src/com/android/tradefed/device/WifiHelper.java b/src/com/android/tradefed/device/WifiHelper.java
index 2a9081c..2fb6fed 100644
--- a/src/com/android/tradefed/device/WifiHelper.java
+++ b/src/com/android/tradefed/device/WifiHelper.java
@@ -50,7 +50,7 @@
static final String CHECK_PACKAGE_CMD =
String.format("dumpsys package %s", INSTRUMENTATION_PKG);
static final Pattern PACKAGE_VERSION_PAT = Pattern.compile("versionCode=(\\d*)");
- static final int PACKAGE_VERSION_CODE = 20;
+ static final int PACKAGE_VERSION_CODE = 21;
private static final String WIFIUTIL_APK_NAME = "WifiUtil.apk";
@@ -352,9 +352,9 @@
if (result != null) {
try {
final JSONObject json = new JSONObject(result);
- final Iterator keys = json.keys();
+ final Iterator<String> keys = json.keys();
while (keys.hasNext()) {
- final String key = (String) keys.next();
+ final String key = keys.next();
info.put(key, json.getString(key));
}
} catch(final JSONException e) {
diff --git a/src/com/android/tradefed/invoker/ShardListener.java b/src/com/android/tradefed/invoker/ShardListener.java
index 273ef34..acc149f 100644
--- a/src/com/android/tradefed/invoker/ShardListener.java
+++ b/src/com/android/tradefed/invoker/ShardListener.java
@@ -16,14 +16,14 @@
package com.android.tradefed.invoker;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestResult.TestStatus;
-import com.android.tradefed.result.TestRunResult;
import java.util.Map;
@@ -104,12 +104,18 @@
private void forwardTestResults(Map<TestIdentifier, TestResult> testResults) {
for (Map.Entry<TestIdentifier, TestResult> testEntry : testResults.entrySet()) {
mMasterListener.testStarted(testEntry.getKey());
- if (testEntry.getValue().getStatus().equals(TestStatus.ERROR)) {
- mMasterListener.testFailed(TestFailure.ERROR, testEntry.getKey(),
- testEntry.getValue().getStackTrace());
- } else if (testEntry.getValue().getStatus().equals(TestStatus.FAILURE)) {
- mMasterListener.testFailed(TestFailure.FAILURE, testEntry.getKey(),
- testEntry.getValue().getStackTrace());
+ switch (testEntry.getValue().getStatus()) {
+ case FAILURE:
+ mMasterListener.testFailed(testEntry.getKey(),
+ testEntry.getValue().getStackTrace());
+ break;
+ case ASSUMPTION_FAILURE:
+ mMasterListener.testAssumptionFailure(testEntry.getKey(),
+ testEntry.getValue().getStackTrace());
+ break;
+ case IGNORED:
+ mMasterListener.testIgnored(testEntry.getKey());
+ break;
}
if (!testEntry.getValue().getStatus().equals(TestStatus.INCOMPLETE)) {
mMasterListener.testEnded(testEntry.getKey(), testEntry.getValue().getMetrics());
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 02d9740..4b32f36 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -49,6 +49,9 @@
import com.android.tradefed.testtype.IResumableTest;
import com.android.tradefed.testtype.IRetriableTest;
import com.android.tradefed.testtype.IShardableTest;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunInterruptedException;
+import com.android.tradefed.util.RunUtil;
import junit.framework.Test;
@@ -74,9 +77,11 @@
static final String TRADEFED_LOG_NAME = "host_log";
static final String DEVICE_LOG_NAME = "device_logcat";
+ static final String EMULATOR_LOG_NAME = "emulator_log";
static final String BUILD_ERROR_BUGREPORT_NAME = "build_error_bugreport";
static final String DEVICE_UNRESPONSIVE_BUGREPORT_NAME = "device_unresponsive_bugreport";
static final String INVOCATION_ENDED_BUGREPORT_NAME = "invocation_ended_bugreport";
+ static final String TARGET_SETUP_ERROR_BUGREPORT_NAME = "target_setup_error_bugreport";
static final String BATT_TAG = "[battery level]";
private String mStatus = "(not invoked)";
@@ -197,15 +202,21 @@
ITestInvocationListener listener = new LogSaverResultForwarder(config.getLogSaver(),
allListeners);
+ IBuildInfo info = null;
+
try {
mStatus = "fetching build";
config.getLogOutput().init();
getLogRegistry().registerLogger(config.getLogOutput());
+ device.clearLastConnectedWifiNetwork();
device.setOptions(config.getDeviceOptions());
if (config.getDeviceOptions().isLogcatCaptureEnabled()) {
device.startLogcat();
}
- IBuildInfo info = null;
+ String cmdLineArgs = config.getCommandLine();
+ if (cmdLineArgs != null) {
+ CLog.i("Invocation was started with cmd: %s", cmdLineArgs);
+ }
if (config.getBuildProvider() instanceof IDeviceBuildProvider) {
info = ((IDeviceBuildProvider)config.getBuildProvider()).getBuild(device);
} else {
@@ -428,32 +439,38 @@
IRescheduler rescheduler, ITestInvocationListener listener) throws Throwable {
boolean resumed = false;
+ String bugreportName = null;
long startTime = System.currentTimeMillis();
long elapsedTime = -1;
+ Throwable exception = null;
+ Exception tearDownException = null;
info.setDeviceSerial(device.getSerialNumber());
startInvocation(config, device, info, listener);
try {
logDeviceBatteryLevel(device, "initial");
-
prepareAndRun(config, device, info, listener);
} catch (BuildError e) {
+ exception = e;
CLog.w("Build %s failed on device %s. Reason: %s", info.getBuildId(),
device.getSerialNumber(), e.toString());
- takeBugreport(device, listener, BUILD_ERROR_BUGREPORT_NAME);
+ bugreportName = BUILD_ERROR_BUGREPORT_NAME;
reportFailure(e, listener, config, info, rescheduler);
} catch (TargetSetupError e) {
+ exception = e;
CLog.e("Caught exception while running invocation");
CLog.e(e);
+ bugreportName = TARGET_SETUP_ERROR_BUGREPORT_NAME;
reportFailure(e, listener, config, info, rescheduler);
} catch (DeviceNotAvailableException e) {
+ exception = e;
// log a warning here so its captured before reportLogs is called
CLog.w("Invocation did not complete due to device %s becoming not available. " +
"Reason: %s", device.getSerialNumber(), e.getMessage());
if ((e instanceof DeviceUnresponsiveException)
&& TestDeviceState.ONLINE.equals(device.getDeviceState())) {
// under certain cases it might still be possible to grab a bugreport
- takeBugreport(device, listener, DEVICE_UNRESPONSIVE_BUGREPORT_NAME);
+ bugreportName = DEVICE_UNRESPONSIVE_BUGREPORT_NAME;
}
resumed = resume(config, info, rescheduler, System.currentTimeMillis() - startTime);
if (!resumed) {
@@ -462,16 +479,45 @@
CLog.i("Rescheduled failed invocation for resume");
}
throw e;
- } catch (RuntimeException e) {
- // log a warning here so its captured before reportLogs is called
- CLog.e("Unexpected exception when running invocation: %s", e.toString());
+ } catch (RunInterruptedException e) {
+ CLog.w("Invocation interrupted");
+ reportFailure(e, listener, config, info, rescheduler);
+ } catch (AssertionError e) {
+ exception = e;
+ CLog.e("Caught AssertionError while running invocation: %s", e.toString());
CLog.e(e);
reportFailure(e, listener, config, info, rescheduler);
- throw e;
- } catch (AssertionError e) {
- CLog.w("Caught AssertionError while running invocation: ", e.toString());
- reportFailure(e, listener, config, info, rescheduler);
+ } catch (Throwable t) {
+ exception = t;
+ // log a warning here so its captured before reportLogs is called
+ CLog.e("Unexpected exception when running invocation: %s", t.toString());
+ CLog.e(t);
+ reportFailure(t, listener, config, info, rescheduler);
+ throw t;
} finally {
+ getRunUtil().allowInterrupt(false);
+ if (config.getCommandOptions().takeBugreportOnInvocationEnded()) {
+ if (bugreportName != null) {
+ CLog.i("Bugreport to be taken for failure instead of invocation ended.");
+ } else {
+ bugreportName = INVOCATION_ENDED_BUGREPORT_NAME;
+ }
+ }
+ if (bugreportName != null) {
+ takeBugreport(device, listener, bugreportName);
+ }
+ mStatus = "tearing down";
+ try {
+ doTeardown(config, device, info, exception);
+ } catch (DeviceNotAvailableException|RuntimeException e) {
+ tearDownException = e;
+ if (exception == null) {
+ // only log & report when the exception is new during tear down
+ CLog.e("Exception when tearing down invocation: %s", tearDownException.toString());
+ CLog.e(tearDownException);
+ reportFailure(tearDownException, listener, config, info, rescheduler);
+ }
+ }
mStatus = "done running tests";
try {
reportLogs(device, listener, config.getLogOutput());
@@ -483,6 +529,12 @@
config.getBuildProvider().cleanUp(info);
}
}
+ if (tearDownException != null) {
+ // this means a DNAE or RTE has happened during teardown, need to throw
+ // if there was a preceding RTE or DNAE stored in 'exception', it would have already
+ // been thrown before exiting the previous try...catch...finally block
+ throw tearDownException;
+ }
}
/**
@@ -490,37 +542,12 @@
*/
private void prepareAndRun(IConfiguration config, ITestDevice device, IBuildInfo info,
ITestInvocationListener listener) throws Throwable {
- // use the JUnit3 logic for handling exceptions when running tests
- Throwable exception = null;
-
- try {
- logDeviceBatteryLevel(device, "initial -> setup");
- doSetup(config, device, info);
- logDeviceBatteryLevel(device, "setup -> test");
- runTests(device, config, listener);
- logDeviceBatteryLevel(device, "after test");
- } catch (Throwable running) {
- exception = running;
- } finally {
- try {
- if (config.getCommandOptions().takeBugreportOnInvocationEnded()) {
- takeBugreport(device, listener, INVOCATION_ENDED_BUGREPORT_NAME);
- }
- } catch (Throwable bugreport) {
- exception = bugreport;
- } finally {
- try {
- doTeardown(config, device, info, exception);
- } catch (Throwable tearingDown) {
- if (exception == null) {
- exception = tearingDown;
- }
- }
- }
- }
- if (exception != null) {
- throw exception;
- }
+ getRunUtil().allowInterrupt(true);
+ logDeviceBatteryLevel(device, "initial -> setup");
+ doSetup(config, device, info);
+ logDeviceBatteryLevel(device, "setup -> test");
+ runTests(device, config, listener);
+ logDeviceBatteryLevel(device, "after test");
}
private void doSetup(IConfiguration config, ITestDevice device, IBuildInfo info)
@@ -532,7 +559,8 @@
private void doTeardown(IConfiguration config, ITestDevice device, IBuildInfo info,
Throwable exception) throws DeviceNotAvailableException {
-
+ // Clear wifi settings, to prevent wifi errors from interfering with teardown process.
+ device.clearLastConnectedWifiNetwork();
List<ITargetPreparer> preparers = config.getTargetPreparers();
ListIterator<ITargetPreparer> itr = preparers.listIterator(preparers.size());
while (itr.hasPrevious()) {
@@ -624,14 +652,21 @@
ILeveledLogOutput logger) {
InputStreamSource logcatSource = null;
InputStreamSource globalLogSource = logger.getLog();
+ InputStreamSource emulatorOutput = null;
if (device != null) {
logcatSource = device.getLogcat();
device.stopLogcat();
+ if (device.getIDevice() != null && device.getIDevice().isEmulator()) {
+ emulatorOutput = device.getEmulatorOutput();
+ }
}
if (logcatSource != null) {
listener.testLog(DEVICE_LOG_NAME, LogDataType.LOGCAT, logcatSource);
}
+ if (emulatorOutput != null) {
+ listener.testLog(EMULATOR_LOG_NAME, LogDataType.TEXT, emulatorOutput);
+ }
listener.testLog(TRADEFED_LOG_NAME, LogDataType.TEXT, globalLogSource);
@@ -639,6 +674,9 @@
if (logcatSource != null) {
logcatSource.cancel();
}
+ if (emulatorOutput != null) {
+ emulatorOutput.cancel();
+ }
globalLogSource.cancel();
// once tradefed log is reported, all further log calls for this invocation can get lost
@@ -671,6 +709,15 @@
}
/**
+ * Utility method to fetch the default {@link IRunUtil} singleton
+ * <p />
+ * Exposed for unit testing.
+ */
+ IRunUtil getRunUtil() {
+ return RunUtil.getDefault();
+ }
+
+ /**
* Runs the test.
*
* @param device the {@link ITestDevice} to run tests on
diff --git a/src/com/android/tradefed/log/TerribleFailureEmailHandler.java b/src/com/android/tradefed/log/TerribleFailureEmailHandler.java
index 80432e7..c89bc94 100644
--- a/src/com/android/tradefed/log/TerribleFailureEmailHandler.java
+++ b/src/com/android/tradefed/log/TerribleFailureEmailHandler.java
@@ -55,7 +55,14 @@
description = "The prefix to be added to the beginning of the email subject.")
private String mSubjectPrefix = DEFAULT_SUBJECT_PREFIX;
+ @Option(name = "min-email-interval",
+ description = "The minimum interval between emails in ms. " +
+ "If a new WTF happens within this interval from the previous one, " +
+ "it will be ignored.")
+ private long mMinEmailInterval = 5 * 60 * 1000;
+
private IEmail mMailer;
+ private long mLastEmailSentTime = 0;
/**
* Create a {@link TerribleFailureEmailHandler}
@@ -95,6 +102,15 @@
}
/**
+ * Sets the minimum email interval.
+ *
+ * @param interval
+ */
+ public void setMinEmailInterval(long interval) {
+ mMinEmailInterval = interval;
+ }
+
+ /**
* Gets the local host name of the machine.
*
* @return the name of the host machine, or "unknown host" if unknown
@@ -109,6 +125,13 @@
}
/**
+ * Gets the current time in milliseconds.
+ */
+ protected long getCurrentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ /**
* A method to generate the subject for email reports.
* The subject will be formatted as:
* "<subject-prefix> on <local-host-name>"
@@ -184,6 +207,14 @@
return false;
}
+ final long now = getCurrentTimeMillis();
+ if (0 < mMinEmailInterval && now - mLastEmailSentTime < mMinEmailInterval) {
+ // TODO: consider queuing up skipped failures and send it later.
+ CLog.w("Skipped to send %s email: email interval %dms < %dms", DEFAULT_SUBJECT_PREFIX,
+ now - mLastEmailSentTime, mMinEmailInterval);
+ return false;
+ }
+
Message msg = generateEmailMessage(description, cause);
try {
@@ -197,6 +228,8 @@
CLog.e(e);
return false;
}
+
+ mLastEmailSentTime = now;
return true;
}
diff --git a/src/com/android/tradefed/result/BugreportCollector.java b/src/com/android/tradefed/result/BugreportCollector.java
index 153d393..15d3247 100644
--- a/src/com/android/tradefed/result/BugreportCollector.java
+++ b/src/com/android/tradefed/result/BugreportCollector.java
@@ -16,6 +16,7 @@
package com.android.tradefed.result;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
@@ -337,7 +338,7 @@
break;
case FAILED_TESTCASE:
- if (curResult.getNumFailedTests() + curResult.getNumErrorTests() == 1) {
+ if (curResult.getNumAllFailedTests() == 1) {
applicableFreqs.add(Freq.FIRST);
}
break;
@@ -443,9 +444,20 @@
* {@inheritDoc}
*/
@Override
- public void testFailed(TestFailure status, TestIdentifier test, String trace) {
- mListener.testFailed(status, test, trace);
- mCollector.testFailed(status, test, trace);
+ public void testFailed(TestIdentifier test, String trace) {
+ mListener.testFailed(test, trace);
+ mCollector.testFailed(test, trace);
+ check(Relation.AFTER, Noun.FAILED_TESTCASE, test);
+ reset();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void testAssumptionFailure(TestIdentifier test, String trace) {
+ mListener.testAssumptionFailure(test, trace);
+ mCollector.testAssumptionFailure(test, trace);
check(Relation.AFTER, Noun.FAILED_TESTCASE, test);
reset();
}
@@ -548,5 +560,10 @@
public TestSummary getSummary() {
return mListener.getSummary();
}
+
+ @Override
+ public void testIgnored(TestIdentifier test) {
+ // ignore
+ }
}
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index f67a272..5572613 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -16,9 +16,10 @@
package com.android.tradefed.result;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
-import com.android.tradefed.result.TestResult.TestStatus;
import java.util.Collection;
import java.util.Collections;
@@ -40,6 +41,12 @@
Collections.synchronizedMap(new LinkedHashMap<String, TestRunResult>());
private TestRunResult mCurrentResults = new TestRunResult();
+ /** represents sums of tests in each TestStatus state for all runs.
+ * Indexed by TestStatus.ordinal() */
+ private int[] mStatusCounts = new int[TestStatus.values().length];
+ /** tracks if mStatusCounts is accurate, or if it needs to be recalculated */
+ private boolean mIsCountDirty = true;
+
@Option(name = "aggregate-metrics", description =
"attempt to add test metrics values for test runs with the same name." )
private boolean mIsAggregateMetrics = false;
@@ -87,11 +94,13 @@
mCurrentResults = mRunResultsMap.get(name);
} else {
// new run
- mCurrentResults = new TestRunResult(name);
+ mCurrentResults = new TestRunResult();
+ mCurrentResults.setAggregateMetrics(mIsAggregateMetrics);
+
mRunResultsMap.put(name, mCurrentResults);
}
- mCurrentResults.setRunComplete(false);
- mCurrentResults.setRunFailureError(null);
+ mCurrentResults.testRunStarted(name, numTests);
+ mIsCountDirty = true;
}
/**
@@ -99,7 +108,8 @@
*/
@Override
public void testStarted(TestIdentifier test) {
- mCurrentResults.reportTestStarted(test);
+ mIsCountDirty = true;
+ mCurrentResults.testStarted(test);
}
/**
@@ -107,19 +117,30 @@
*/
@Override
public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
- mCurrentResults.reportTestEnded(test, testMetrics);
+ mIsCountDirty = true;
+ mCurrentResults.testEnded(test, testMetrics);
}
/**
* {@inheritDoc}
*/
@Override
- public void testFailed(TestFailure testFailure, TestIdentifier test, String trace) {
- if (testFailure.equals(TestFailure.ERROR)) {
- mCurrentResults.reportTestFailure(test, TestStatus.ERROR, trace);
- } else {
- mCurrentResults.reportTestFailure(test, TestStatus.FAILURE, trace);
- }
+ public void testFailed(TestIdentifier test, String trace) {
+ mIsCountDirty = true;
+ mCurrentResults.testFailed(test, trace);
+ }
+
+ @Override
+ public void testAssumptionFailure(TestIdentifier test, String trace) {
+ mIsCountDirty = true;
+ mCurrentResults.testAssumptionFailure(test, trace);
+
+ }
+
+ @Override
+ public void testIgnored(TestIdentifier test) {
+ mIsCountDirty = true;
+ mCurrentResults.testIgnored(test);
}
/**
@@ -127,9 +148,8 @@
*/
@Override
public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
- mCurrentResults.setRunComplete(true);
- mCurrentResults.addMetrics(runMetrics, mIsAggregateMetrics);
- mCurrentResults.addElapsedTime(elapsedTime);
+ mIsCountDirty = true;
+ mCurrentResults.testRunEnded(elapsedTime, runMetrics);
}
/**
@@ -137,7 +157,8 @@
*/
@Override
public void testRunFailed(String errorMessage) {
- mCurrentResults.setRunFailureError(errorMessage);
+ mIsCountDirty = true;
+ mCurrentResults.testRunFailed(errorMessage);
}
/**
@@ -145,8 +166,8 @@
*/
@Override
public void testRunStopped(long elapsedTime) {
- mCurrentResults.setRunComplete(true);
- mCurrentResults.addElapsedTime(elapsedTime);
+ mIsCountDirty = true;
+ mCurrentResults.testRunStopped(elapsedTime);
}
/**
@@ -173,58 +194,35 @@
* Gets the total number of complete tests for all runs.
*/
public int getNumTotalTests() {
- return getNumFailedTests() + getNumErrorTests() + getNumPassedTests();
- }
-
- /**
- * Gets the total number of failed tests for all runs.
- */
- public int getNumFailedTests() {
- int numFailedTests = 0;
- for (TestRunResult result : mRunResultsMap.values()) {
- numFailedTests += result.getNumFailedTests();
+ int total = 0;
+ // force test count
+ getNumTestsInState(TestStatus.PASSED);
+ for (TestStatus s : TestStatus.values()) {
+ total += mStatusCounts[s.ordinal()];
}
- return numFailedTests;
+ return total;
}
/**
- * Gets the total number of error tests for all runs.
+ * Gets the number of tests in given state for this run.
*/
- public int getNumErrorTests() {
- int numErrorTests = 0;
- for (TestRunResult result : mRunResultsMap.values()) {
- numErrorTests += result.getNumErrorTests();
+ public int getNumTestsInState(TestStatus status) {
+ if (mIsCountDirty) {
+ for (TestRunResult result : mRunResultsMap.values()) {
+ for (TestStatus s : TestStatus.values()) {
+ mStatusCounts[s.ordinal()] += result.getNumTestsInState(s);
+ }
+ }
+ mIsCountDirty = false;
}
- return numErrorTests;
+ return mStatusCounts[status.ordinal()];
}
/**
- * Gets the total number of passed tests for all runs.
- */
- public int getNumPassedTests() {
- int numPassedTests = 0;
- for (TestRunResult result : mRunResultsMap.values()) {
- numPassedTests += result.getNumPassedTests();
- }
- return numPassedTests;
- }
-
- /**
- * Gets the total number of incomplete tests for all runs.
- */
- public int getNumIncompleteTests() {
- int numIncompleteTests = 0;
- for (TestRunResult result : mRunResultsMap.values()) {
- numIncompleteTests += result.getNumIncompleteTests();
- }
- return numIncompleteTests;
- }
-
- /**
- * @return true if invocation had any failed or error tests.
+ * @return true if invocation had any failed or assumption failed tests.
*/
public boolean hasFailedTests() {
- return getNumErrorTests() > 0 || getNumFailedTests() > 0;
+ return getNumAllFailedTests() > 0;
}
/**
@@ -259,4 +257,13 @@
public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
// ignore
}
+
+ /**
+ * Return total number of tests in a failure state (failed, assumption failure)
+ * @return
+ */
+ public int getNumAllFailedTests() {
+ return getNumTestsInState(TestStatus.FAILURE) +
+ getNumTestsInState(TestStatus.ASSUMPTION_FAILURE);
+ }
}
diff --git a/src/com/android/tradefed/result/ConsoleResultReporter.java b/src/com/android/tradefed/result/ConsoleResultReporter.java
new file mode 100644
index 0000000..f1174ea
--- /dev/null
+++ b/src/com/android/tradefed/result/ConsoleResultReporter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Result reporter to print the test results to the console.
+ * <p>
+ * Prints each test run, each test case, and test metrics, test logs, and test file locations.
+ * <p>
+ */
+public class ConsoleResultReporter extends CollectingTestListener implements ILogSaverListener {
+ private static final String LOG_TAG = ConsoleResultReporter.class.getSimpleName();
+
+ private List<LogFile> mLogFiles = new LinkedList<>();
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void invocationEnded(long elapsedTime) {
+ Log.logAndDisplay(LogLevel.INFO, LOG_TAG, getInvocationSummary());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream,
+ LogFile logFile) {
+ mLogFiles.add(logFile);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setLogSaver(ILogSaver logSaver) {
+ // Ignore. This class doesn't save any additional files.
+ }
+
+ /**
+ * Get the invocation summary as a string.
+ */
+ String getInvocationSummary() {
+ if (getRunResults().isEmpty() && mLogFiles.isEmpty()) {
+ return "No test results\n";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (TestRunResult testRunResult : getRunResults()) {
+ sb.append(getTestRunSummary(testRunResult));
+ }
+ if (!mLogFiles.isEmpty()) {
+ sb.append("Log Files:\n");
+ for (LogFile logFile : mLogFiles) {
+ final String url = logFile.getUrl();
+ sb.append(String.format(" %s\n", url != null ? url : logFile.getPath()));
+ }
+ }
+ return "Test results:\n" + sb.toString().trim() + "\n";
+ }
+
+ /**
+ * Get the test run summary as a string including run metrics.
+ */
+ String getTestRunSummary(TestRunResult testRunResult) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("%s:", testRunResult.getName()));
+ if (testRunResult.getNumTests() > 0) {
+ sb.append(String.format(" %d Test%s, %d Passed, %d Failed, %d Ignored",
+ testRunResult.getNumCompleteTests(),
+ testRunResult.getNumCompleteTests() == 1 ? "" : "s", // Pluralize Test
+ testRunResult.getNumTestsInState(TestStatus.PASSED),
+ testRunResult.getNumAllFailedTests(),
+ testRunResult.getNumTestsInState(TestStatus.IGNORED)));
+ } else if (testRunResult.getRunMetrics().size() == 0) {
+ sb.append(" No results");
+ }
+ sb.append("\n");
+ Map<TestIdentifier, TestResult> testResults = testRunResult.getTestResults();
+ for (Map.Entry<TestIdentifier, TestResult> entry : testResults.entrySet()) {
+ sb.append(getTestSummary(entry.getKey(), entry.getValue()));
+ }
+ Map<String, String> metrics = testRunResult.getRunMetrics();
+ if (metrics != null && !metrics.isEmpty()) {
+ List<String> metricKeys = new ArrayList<String>(metrics.keySet());
+ Collections.sort(metricKeys);
+ for (String metricKey : metricKeys) {
+ sb.append(String.format(" %s: %s\n", metricKey, metrics.get(metricKey)));
+ }
+ }
+ sb.append("\n");
+ return sb.toString();
+ }
+
+ /**
+ * Get the test summary as string including test metrics.
+ */
+ String getTestSummary(TestIdentifier testId, TestResult testResult) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format(" %s: %s\n", testId.toString(), testResult.getStatus()));
+ Map<String, String> metrics = testResult.getMetrics();
+ if (metrics != null && !metrics.isEmpty()) {
+ List<String> metricKeys = new ArrayList<String>(metrics.keySet());
+ Collections.sort(metricKeys);
+ for (String metricKey : metricKeys) {
+ sb.append(String.format(" %s: %s\n", metricKey, metrics.get(metricKey)));
+ }
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/src/com/android/tradefed/result/DeviceFileReporter.java b/src/com/android/tradefed/result/DeviceFileReporter.java
index cec6594..f3017cd 100644
--- a/src/com/android/tradefed/result/DeviceFileReporter.java
+++ b/src/com/android/tradefed/result/DeviceFileReporter.java
@@ -26,10 +26,13 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* A utility class that checks the device for files and sends them to
@@ -40,8 +43,30 @@
private final ITestInvocationListener mListener;
private final ITestDevice mDevice;
+ /** Whether to ignore files that have already been captured by a prior Pattern */
+ private boolean mSkipRepeatFiles = true;
+ /** The files which have already been reported */
+ private Set<String> mReportedFiles = new HashSet<String>();
+
+ /** Whether to attempt to infer data types for patterns with {@code UNKNOWN} data type */
+ private boolean mInferDataTypes = true;
+
private LogDataType mDefaultFileType = LogDataType.UNKNOWN;
+ private static final Map<String, LogDataType> DATA_TYPE_REVERSE_MAP = new HashMap<>();
+
+ static {
+ // Make it easy to map backward from file extension to LogDataType
+ for (LogDataType type : LogDataType.values()) {
+ // Extracted extension will contain a leading dot
+ final String ext = "." + type.getFileExt();
+ if (DATA_TYPE_REVERSE_MAP.containsKey(ext)) {
+ continue;
+ }
+
+ DATA_TYPE_REVERSE_MAP.put(ext, type);
+ }
+ }
/**
* Initialize a new DeviceFileReporter with the provided {@link ITestDevice}
*/
@@ -101,6 +126,30 @@
}
/**
+ * Whether or not to skip files which have already been reported. This is only relevant when
+ * multiple patterns are being used, and two or more of those patterns match the same file.
+ * <p />
+ * Note that this <emph>must only</emph> be called prior to calling {@see #run()}. Doing
+ * otherwise will cause undefined behavior.
+ */
+ public void setSkipRepeatFiles(boolean skip) {
+ mSkipRepeatFiles = skip;
+ }
+
+ /**
+ * Whether to <emph>attempt to</emph> infer the data types of {@code UNKNOWN} files by checking
+ * the file extensions against a list.
+ * <p />
+ * Note that, when enabled, these inferences will only be made for patterns with file type
+ * {@code UNKNOWN} (which includes patterns added without a specific type, and without the)
+ * default type having been set manually). If the inference fails, the data type will remain
+ * as {@code UNKNOWN}.
+ */
+ public void setInferUnknownDataTypes(boolean infer) {
+ mInferDataTypes = infer;
+ }
+
+ /**
* Actually search the filesystem for the specified patterns and send them to
* {@link ITestInvocationListener#testLog} if found
*/
@@ -109,10 +158,16 @@
for (Map.Entry<String, LogDataType> pat : mFilePatterns.entrySet()) {
final String searchCmd = String.format("ls '%s'", pat.getKey());
final String fileList = mDevice.executeShellCommand(searchCmd);
- for (String filename : fileList.split("\r\n")) {
+
+ for (String filename : fileList.split("\r?\n")) {
if (filename.isEmpty() || filename.endsWith(": No such file or directory")) {
continue;
}
+ if (mSkipRepeatFiles && mReportedFiles.contains(filename)) {
+ CLog.v("Skipping already-reported file %s", filename);
+ continue;
+ }
+
File file = null;
InputStreamSource iss = null;
try {
@@ -121,8 +176,10 @@
file = mDevice.pullFile(filename);
CLog.v("Local file %s has size %d", file, file.length());
iss = createIssForFile(file);
- mListener.testLog(filename, pat.getValue(), iss);
+ final LogDataType type = getDataType(filename, pat.getValue());
+ mListener.testLog(filename, type, iss);
filenames.add(filename);
+ mReportedFiles.add(filename);
} catch (IOException e) {
CLog.w("Failed to log file %s: %s", filename, e.getMessage());
} finally {
@@ -138,6 +195,32 @@
}
/**
+ * Returns the data type to use for a given file. Will attempt to infer the data type from the
+ * file's extension IFF inferences are enabled, and the current data type is {@code UNKNOWN}.
+ */
+ LogDataType getDataType(String filename, LogDataType defaultType) {
+ if (!mInferDataTypes) return defaultType;
+ if (!LogDataType.UNKNOWN.equals(defaultType)) return defaultType;
+
+ CLog.d("Running type inference for file %s with default type %s", filename, defaultType);
+ String ext = FileUtil.getExtension(filename);
+ CLog.v("Found raw extension \"%s\"", ext);
+
+ // Normalize the extension
+ if (ext == null) return defaultType;
+ ext = ext.toLowerCase();
+
+ if (DATA_TYPE_REVERSE_MAP.containsKey(ext)) {
+ final LogDataType newType = DATA_TYPE_REVERSE_MAP.get(ext);
+ CLog.d("Inferred data type %s", newType);
+ return newType;
+ } else {
+ CLog.v("Failed to find a reverse map for extension \"%s\"", ext);
+ return defaultType;
+ }
+ }
+
+ /**
* Create an {@link InputStreamSource} for a file
* <p />
* Exposed for unit testing
@@ -147,4 +230,3 @@
return new SnapshotInputStreamSource(bufStr);
}
}
-
diff --git a/src/com/android/tradefed/result/EmailResultReporter.java b/src/com/android/tradefed/result/EmailResultReporter.java
index b1ac059..a371915 100644
--- a/src/com/android/tradefed/result/EmailResultReporter.java
+++ b/src/com/android/tradefed/result/EmailResultReporter.java
@@ -15,6 +15,8 @@
*/
package com.android.tradefed.result;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
@@ -216,8 +218,8 @@
bodyBuilder.append(StreamUtil.getStackTrace(mInvocationThrowable));
bodyBuilder.append("\n");
}
- bodyBuilder.append(String.format("Test results: %d passed, %d failed, %d error\n\n",
- getNumPassedTests(), getNumFailedTests(), getNumErrorTests()));
+ bodyBuilder.append(String.format("Test results: %d passed, %d failed\n\n",
+ getNumTestsInState(TestStatus.PASSED), getNumAllFailedTests()));
for (TestRunResult result : getRunResults()) {
if (!result.getRunMetrics().isEmpty()) {
bodyBuilder.append(String.format("'%s' test run metrics: %s\n", result.getName(),
diff --git a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
index 2c05669..02ae4fc 100644
--- a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
+++ b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
@@ -24,6 +24,8 @@
import junit.framework.TestListener;
import junit.framework.TestResult;
+import org.junit.internal.AssumptionViolatedException;
+
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Map;
@@ -58,16 +60,17 @@
* {@inheritDoc}
*/
@Override
- public void testFailed(TestFailure status, TestIdentifier testId, String trace) {
+ public void testFailed(TestIdentifier testId, String trace) {
Test test = new TestIdentifierResult(testId);
+ // TODO: is it accurate to represent the trace as AssertionFailedError?
+ mJUnitListener.addFailure(test, new AssertionFailedError(trace));
+ }
- if (TestFailure.ERROR.equals(status)) {
- Throwable throwable = new RemoteException(trace);
- mJUnitListener.addError(test, throwable);
- } else {
- // TODO: is it accurate to represent the trace as AssertionFailedError?
- mJUnitListener.addFailure(test, new AssertionFailedError(trace));
- }
+ @Override
+ public void testAssumptionFailure(TestIdentifier testId, String trace) {
+ Test test = new TestIdentifierResult(testId);
+ AssumptionViolatedException throwable = new AssumptionViolatedException(trace);
+ mJUnitListener.addError(test, throwable);
}
/**
@@ -258,4 +261,9 @@
public void testLog(String dataName, LogDataType logData, InputStreamSource dataStream) {
// ignore
}
+
+ @Override
+ public void testIgnored(TestIdentifier test) {
+ // ignore
+ }
}
diff --git a/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java b/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java
index 653453c..e5e35a8 100644
--- a/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java
+++ b/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java
@@ -16,7 +16,6 @@
package com.android.tradefed.result;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import junit.framework.AssertionFailedError;
@@ -56,7 +55,7 @@
@Override
public void addError(Test test, Throwable t) {
for (ITestInvocationListener listener : mInvocationListeners) {
- listener.testFailed(TestFailure.ERROR, getTestId(test), getStackTrace(t));
+ listener.testFailed(getTestId(test), getStackTrace(t));
}
}
@@ -66,7 +65,7 @@
@Override
public void addFailure(Test test, AssertionFailedError t) {
for (ITestInvocationListener listener : mInvocationListeners) {
- listener.testFailed(TestFailure.FAILURE, getTestId(test), getStackTrace(t));
+ listener.testFailed(getTestId(test), getStackTrace(t));
}
}
diff --git a/src/com/android/tradefed/result/LogDataType.java b/src/com/android/tradefed/result/LogDataType.java
index 797cd60..d6185c1 100644
--- a/src/com/android/tradefed/result/LogDataType.java
+++ b/src/com/android/tradefed/result/LogDataType.java
@@ -22,6 +22,7 @@
TEXT("txt", false, true),
XML("xml", false, true),
+ HTML("html", true, true),
PNG("png", true, false),
ZIP("zip", true, false),
GZIP("gz", true, false),
diff --git a/src/com/android/tradefed/result/LogFilesReporter.java b/src/com/android/tradefed/result/LogFilesReporter.java
new file mode 100644
index 0000000..38c5995
--- /dev/null
+++ b/src/com/android/tradefed/result/LogFilesReporter.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.result;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IFileEntry;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Helper test component that pulls files located on a device and adds them to the test logs.
+ * The component provides {@link IRemoteTest} and {@link IDeviceTest} services.
+ * <p>
+ * The path to files on the device is specified with upload-dir or upload-pattern options.
+ * The files will be removed if clean-upload-pattern option is provided.
+ */
+public class LogFilesReporter implements IRemoteTest, IDeviceTest {
+
+ @Option(name = "upload-pattern",
+ description = "A path pattern of files on the device to be added to the test logs.")
+ private String mUploadPattern = null;
+
+ // TODO(mdzyuba): upload-pattern does not work at the moment. Remove upload-dir once resolved.
+ @Option(name = "upload-dir",
+ description = "A folder on the device to be added to the test logs.")
+ private String mUploadDir = null;
+
+ @Option(name = "clean-upload-pattern",
+ description = "Clean files specified in \"upload-pattern\" after the test is done.")
+ private boolean mRemoveFilesSpecifiedByUploadPattern = true;
+
+ // A device instance.
+ private ITestDevice mDevice;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setDevice(ITestDevice device) {
+ mDevice = device;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ITestDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+ if (mUploadPattern != null) {
+ uploadFilesOnDeviceToLogs(mUploadPattern, listener);
+ if (mRemoveFilesSpecifiedByUploadPattern) {
+ cleanFilesOnDevice(mUploadPattern);
+ }
+ }
+ if (mUploadDir != null) {
+ uploadFolderOnDeviceToLogs(mUploadDir, listener);
+ if (mRemoveFilesSpecifiedByUploadPattern) {
+ cleanFilesOnDevice(mUploadDir + "/*");
+ }
+ }
+ }
+
+ /**
+ * Uploads files from a device to test logs.
+ *
+ * @param filesPattern a path pattern to files on device to be added to the logs.
+ * @param listener a listener for test results from the test invocation.
+ * @throws DeviceNotAvailableException in case device is unavailable.
+ */
+ protected void uploadFilesOnDeviceToLogs(String filesPattern, ITestInvocationListener listener)
+ throws DeviceNotAvailableException {
+ final DeviceFileReporter reporter = new DeviceFileReporter(getDevice(), listener);
+ reporter.addPatterns(filesPattern);
+ reporter.run();
+ }
+
+ /**
+ * Uploads files from a device to test logs.
+ *
+ * @param dir a full path to a folder on device containing files to be added to the logs.
+ * @param listener a listener for test results from the test invocation.
+ * @throws DeviceNotAvailableException in case device is unavailable.
+ */
+ protected void uploadFolderOnDeviceToLogs(String dir, ITestInvocationListener listener)
+ throws DeviceNotAvailableException {
+ final DeviceFileReporter reporter = new DeviceFileReporter(getDevice(), listener);
+ Map<String, LogDataType> uploadFilePatterns = new LinkedHashMap<String, LogDataType>();
+ IFileEntry outputDir = getDevice().getFileEntry(dir);
+ if (outputDir != null) {
+ for (IFileEntry file : outputDir.getChildren(false)) {
+ LogDataType logDataType = LogDataType.UNKNOWN;
+ if (file.getName().endsWith(LogDataType.PNG.getFileExt())) {
+ logDataType = LogDataType.PNG;
+ } else if (file.getName().endsWith(LogDataType.XML.getFileExt())) {
+ logDataType = LogDataType.XML;
+ }
+ CLog.v(String.format("Adding file %s", file.getFullPath()));
+ uploadFilePatterns.put(file.getFullPath(), logDataType);
+ }
+ reporter.addPatterns(uploadFilePatterns);
+ reporter.run();
+ } else {
+ CLog.w("Directory not found on device: %s", dir);
+ }
+ }
+
+ /**
+ * Cleans the files on the device.
+ *
+ * @param pattern a path pattern to files to be removed.
+ * @throws DeviceNotAvailableException in case device is unavailable.
+ */
+ protected void cleanFilesOnDevice(String pattern) throws DeviceNotAvailableException {
+ String folder = pattern.substring(0, pattern.lastIndexOf('/'));
+ if (doesDirectoryExistOnDevice(folder)) {
+ getDevice().executeShellCommand(String.format("rm %s", pattern));
+ }
+ }
+
+ /**
+ * Checks to see if a directory exists on a device.
+ *
+ * @param folder a full path to a directory on a device to be verified.
+ * @return true if a directory exists, false otherwise.
+ * @throws DeviceNotAvailableException in case device is unavailable.
+ */
+ protected boolean doesDirectoryExistOnDevice(String folder) throws DeviceNotAvailableException {
+ IFileEntry outputDir = getDevice().getFileEntry(folder);
+ if (outputDir == null) {
+ CLog.w("Directory not found on device: %s", folder);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/tradefed/result/NameMangleListener.java b/src/com/android/tradefed/result/NameMangleListener.java
index d3f4978..5598d0f 100644
--- a/src/com/android/tradefed/result/NameMangleListener.java
+++ b/src/com/android/tradefed/result/NameMangleListener.java
@@ -19,6 +19,8 @@
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.build.IBuildInfo;
+import junit.framework.TestFailure;
+
import java.util.Map;
/**
@@ -91,9 +93,27 @@
* {@inheritDoc}
*/
@Override
- public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ public void testFailed(TestIdentifier test, String trace) {
final TestIdentifier mangledTestId = mangleTestId(test);
- mListener.testFailed(status, mangledTestId, trace);
+ mListener.testFailed(mangledTestId, trace);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void testAssumptionFailure(TestIdentifier test, String trace) {
+ final TestIdentifier mangledTestId = mangleTestId(test);
+ mListener.testAssumptionFailure(mangledTestId, trace);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void testIgnored(TestIdentifier test) {
+ final TestIdentifier mangledTestId = mangleTestId(test);
+ mListener.testIgnored(mangledTestId);
}
/**
diff --git a/src/com/android/tradefed/result/ResultForwarder.java b/src/com/android/tradefed/result/ResultForwarder.java
index 1ffbeb5..44e878c 100644
--- a/src/com/android/tradefed/result/ResultForwarder.java
+++ b/src/com/android/tradefed/result/ResultForwarder.java
@@ -184,9 +184,9 @@
* {@inheritDoc}
*/
@Override
- public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ public void testFailed(TestIdentifier test, String trace) {
for (ITestInvocationListener listener : mListeners) {
- listener.testFailed(status, test, trace);
+ listener.testFailed(test, trace);
}
}
@@ -199,4 +199,18 @@
listener.testEnded(test, testMetrics);
}
}
+
+ @Override
+ public void testAssumptionFailure(TestIdentifier test, String trace) {
+ for (ITestInvocationListener listener : mListeners) {
+ listener.testAssumptionFailure(test, trace);
+ }
+ }
+
+ @Override
+ public void testIgnored(TestIdentifier test) {
+ for (ITestInvocationListener listener : mListeners) {
+ listener.testIgnored(test);
+ }
+ }
}
diff --git a/src/com/android/tradefed/result/StubTestRunListener.java b/src/com/android/tradefed/result/StubTestRunListener.java
index d63f9c9..831c6a4 100644
--- a/src/com/android/tradefed/result/StubTestRunListener.java
+++ b/src/com/android/tradefed/result/StubTestRunListener.java
@@ -37,7 +37,23 @@
* {@inheritDoc}
*/
@Override
- public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ public void testFailed(TestIdentifier test, String trace) {
+ // ignore
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void testAssumptionFailure(TestIdentifier test, String trace) {
+ // ignore
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void testIgnored(TestIdentifier test) {
// ignore
}
diff --git a/src/com/android/tradefed/result/TestResult.java b/src/com/android/tradefed/result/TestResult.java
deleted file mode 100644
index ab84d1e..0000000
--- a/src/com/android/tradefed/result/TestResult.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.result;
-
-import com.android.ddmlib.testrunner.TestIdentifier;
-import com.google.common.base.Objects;
-
-import java.util.Map;
-
-/**
- * Container for a result of a single test.
- */
-public class TestResult {
-
- public enum TestStatus {
- /** Test error */
- ERROR,
- /** Test failed. */
- FAILURE,
- /** Test passed */
- PASSED,
- /** Test started but not ended */
- INCOMPLETE
- }
-
- private TestStatus mStatus;
- private String mStackTrace;
- private Map<String, String> mMetrics;
- // the start and end time of the test, measured via {@link System#currentTimeMillis()}
- private long mStartTime = 0;
- private long mEndTime = 0;
-
- public TestResult() {
- mStatus = TestStatus.INCOMPLETE;
- mStartTime = System.currentTimeMillis();
- }
-
- /**
- * Get the {@link TestStatus} result of the test.
- */
- public TestStatus getStatus() {
- return mStatus;
- }
-
- /**
- * Get the associated {@link String} stack trace. Should be <code>null</code> if
- * {@link #getStatus()} is {@link TestStatus#PASSED}.
- */
- public String getStackTrace() {
- return mStackTrace;
- }
-
- /**
- * Get the associated test metrics.
- */
- public Map<String, String> getMetrics() {
- return mMetrics;
- }
-
- /**
- * Set the test metrics, overriding any previous values.
- */
- public void setMetrics(Map<String, String> metrics) {
- mMetrics = metrics;
- }
-
- /**
- * Return the {@link System#currentTimeMillis()} time that the
- * {@link ITestInvocationListener#testStarted(TestIdentifier)} event was received.
- */
- public long getStartTime() {
- return mStartTime;
- }
-
- /**
- * Return the {@link System#currentTimeMillis()} time that the
- * {@link ITestInvocationListener#testEnded(TestIdentifier, Map)} event was received.
- */
- public long getEndTime() {
- return mEndTime;
- }
-
- /**
- * Set the {@link TestStatus}.
- */
- public TestResult setStatus(TestStatus status) {
- mStatus = status;
- return this;
- }
-
- /**
- * Set the stack trace.
- */
- public void setStackTrace(String trace) {
- mStackTrace = trace;
- }
-
- /**
- * Sets the end time
- */
- public void setEndTime(long currentTimeMillis) {
- mEndTime = currentTimeMillis;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int hashCode() {
- return Objects.hashCode(mMetrics, mStackTrace, mStatus);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- TestResult other = (TestResult) obj;
- return Objects.equal(mMetrics, other.mMetrics) &&
- Objects.equal(mStackTrace, other.mStackTrace) &&
- Objects.equal(mStatus, other.mStatus);
- }
-
-}
diff --git a/src/com/android/tradefed/result/TestRunResult.java b/src/com/android/tradefed/result/TestRunResult.java
deleted file mode 100644
index 20ac5ee..0000000
--- a/src/com/android/tradefed/result/TestRunResult.java
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.result;
-
-import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.TestResult.TestStatus;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Holds results from a single test run
- */
-public class TestRunResult {
- private final String mTestRunName;
- // Uses a synchronized map to make thread safe.
- // Uses a LinkedHashmap to have predictable iteration order
- private Map<TestIdentifier, TestResult> mTestResults =
- Collections.synchronizedMap(new LinkedHashMap<TestIdentifier, TestResult>());
- private Map<String, String> mRunMetrics = new HashMap<String, String>();
- private boolean mIsRunComplete = false;
- private long mElapsedTime = 0;
- private int mNumFailedTests = 0;
- private int mNumErrorTests = 0;
- private int mNumPassedTests = 0;
- private int mNumInCompleteTests = 0;
- private String mRunFailureError = null;
-
- /**
- * Create a {@link TestRunResult}.
- *
- * @param runName
- */
- public TestRunResult(String runName) {
- mTestRunName = runName;
- }
-
- /**
- * Create an empty{@link TestRunResult}.
- */
- public TestRunResult() {
- this("not started");
- }
-
- /**
- * @return the test run name
- */
- public String getName() {
- return mTestRunName;
- }
-
- /**
- * Gets a map of the test results.
- * @return
- */
- public Map<TestIdentifier, TestResult> getTestResults() {
- return mTestResults;
- }
-
- /**
- * Adds test run metrics.
- * <p/>
- * @param runMetrics the run metrics
- * @param aggregateMetrics if <code>true</code>, attempt to add given metrics values to any
- * currently stored values. If <code>false</code>, replace any currently stored metrics with
- * the same key.
- */
- public void addMetrics(Map<String, String> runMetrics, boolean aggregateMetrics) {
- if (aggregateMetrics) {
- for (Map.Entry<String, String> entry : runMetrics.entrySet()) {
- String existingValue = mRunMetrics.get(entry.getKey());
- String combinedValue = combineValues(existingValue, entry.getValue());
- mRunMetrics.put(entry.getKey(), combinedValue);
- }
- } else {
- mRunMetrics.putAll(runMetrics);
- }
- }
-
- /**
- * Combine old and new metrics value
- *
- * @param existingValue
- * @param value
- * @return
- */
- private String combineValues(String existingValue, String newValue) {
- if (existingValue != null) {
- try {
- Long existingLong = Long.parseLong(existingValue);
- Long newLong = Long.parseLong(newValue);
- return Long.toString(existingLong + newLong);
- } catch (NumberFormatException e) {
- // not a long, skip to next
- }
- try {
- Double existingDouble = Double.parseDouble(existingValue);
- Double newDouble = Double.parseDouble(newValue);
- return Double.toString(existingDouble + newDouble);
- } catch (NumberFormatException e) {
- // not a double either, fall through
- }
- }
- // default to overriding existingValue
- return newValue;
- }
-
- /**
- * @return a {@link Map} of the test test run metrics.
- */
- public Map<String, String> getRunMetrics() {
- return mRunMetrics;
- }
-
- /**
- * Gets the set of completed tests.
- */
- public Set<TestIdentifier> getCompletedTests() {
- Set<TestIdentifier> completedTests = new LinkedHashSet<TestIdentifier>();
- for (Map.Entry<TestIdentifier, TestResult> testEntry : getTestResults().entrySet()) {
- if (!testEntry.getValue().getStatus().equals(TestStatus.INCOMPLETE)) {
- completedTests.add(testEntry.getKey());
- }
- }
- return completedTests;
- }
-
- /**
- * @return <code>true</code> if test run failed.
- */
- public boolean isRunFailure() {
- return mRunFailureError != null;
- }
-
- /**
- * @return <code>true</code> if test run finished.
- */
- public boolean isRunComplete() {
- return mIsRunComplete;
- }
-
- void setRunComplete(boolean runComplete) {
- mIsRunComplete = runComplete;
- }
-
- void addElapsedTime(long elapsedTime) {
- mElapsedTime+= elapsedTime;
- }
-
- void setRunFailureError(String errorMessage) {
- mRunFailureError = errorMessage;
- }
-
- /**
- * Gets the number of passed tests for this run.
- */
- public int getNumPassedTests() {
- return mNumPassedTests;
- }
-
- /**
- * Gets the number of tests in this run.
- */
- public int getNumTests() {
- return mTestResults.size();
- }
-
- /**
- * Gets the number of complete tests in this run ie with status != incomplete.
- */
- public int getNumCompleteTests() {
- return getNumTests() - getNumIncompleteTests();
- }
-
- /**
- * Gets the number of failed tests in this run.
- */
- public int getNumFailedTests() {
- return mNumFailedTests;
- }
-
- /**
- * Gets the number of error tests in this run.
- */
- public int getNumErrorTests() {
- return mNumErrorTests;
- }
-
- /**
- * Gets the number of incomplete tests in this run.
- */
- public int getNumIncompleteTests() {
- return mNumInCompleteTests;
- }
-
- /**
- * @return <code>true</code> if test run had any failed or error tests.
- */
- public boolean hasFailedTests() {
- return getNumErrorTests() > 0 || getNumFailedTests() > 0;
- }
-
- /**
- * @return
- */
- public long getElapsedTime() {
- return mElapsedTime;
- }
-
- /**
- * Return the run failure error message, <code>null</code> if run did not fail.
- */
- public String getRunFailureMessage() {
- return mRunFailureError;
- }
-
- /**
- * Report the start of a test.
- * @param test
- */
- void reportTestStarted(TestIdentifier test) {
- TestResult result = mTestResults.get(test);
-
- if (result != null) {
- CLog.d("Replacing result for %s", test);
- switch (result.getStatus()) {
- case ERROR:
- mNumErrorTests--;
- break;
- case FAILURE:
- mNumFailedTests--;
- break;
- case PASSED:
- mNumPassedTests--;
- break;
- }
- } else {
- mNumInCompleteTests++;
- }
- mTestResults.put(test, new TestResult());
- }
-
- /**
- * Report a test failure.
- *
- * @param test
- * @param status
- * @param trace
- */
- void reportTestFailure(TestIdentifier test, TestStatus status, String trace) {
- TestResult result = mTestResults.get(test);
- if (result == null) {
- CLog.d("Received test failure for %s without testStarted", test);
- result = new TestResult();
- mTestResults.put(test, result);
- } else if (result.getStatus().equals(TestStatus.PASSED)) {
- // this should never happen...
- CLog.d("Replacing passed result for %s", test);
- mNumPassedTests--;
- }
-
- result.setStackTrace(trace);
- switch (status) {
- case ERROR:
- mNumErrorTests++;
- result.setStatus(TestStatus.ERROR);
- break;
- case FAILURE:
- result.setStatus(TestStatus.FAILURE);
- mNumFailedTests++;
- break;
- }
- }
-
- /**
- * Report the end of the test
- *
- * @param test
- * @param testMetrics
- * @return <code>true</code> if test was recorded as passed, false otherwise
- */
- boolean reportTestEnded(TestIdentifier test, Map<String, String> testMetrics) {
- TestResult result = mTestResults.get(test);
- if (result == null) {
- CLog.d("Received test ended for %s without testStarted", test);
- result = new TestResult();
- mTestResults.put(test, result);
- } else {
- mNumInCompleteTests--;
- }
-
- result.setEndTime(System.currentTimeMillis());
- result.setMetrics(testMetrics);
- if (result.getStatus().equals(TestStatus.INCOMPLETE)) {
- result.setStatus(TestStatus.PASSED);
- mNumPassedTests++;
- return true;
- }
- return false;
- }
-}
diff --git a/src/com/android/tradefed/result/TextResultReporter.java b/src/com/android/tradefed/result/TextResultReporter.java
index be84c17..ee8c3ee 100644
--- a/src/com/android/tradefed/result/TextResultReporter.java
+++ b/src/com/android/tradefed/result/TextResultReporter.java
@@ -44,9 +44,15 @@
* {@inheritDoc}
*/
@Override
- public void testFailed(TestFailure status, TestIdentifier testId, String trace) {
+ public void testFailed(TestIdentifier testId, String trace) {
ResultPrinter printer = (ResultPrinter)getJUnitListener();
- printer.getWriter().format("\nTest %s: %s \n stack: %s ", status, testId, trace);
+ printer.getWriter().format("\nTest %s: failed \n stack: %s ", testId, trace);
+ }
+
+ @Override
+ public void testAssumptionFailure(TestIdentifier testId, String trace) {
+ ResultPrinter printer = (ResultPrinter)getJUnitListener();
+ printer.getWriter().format("\nTest %s: assumption failed \n stack: %s ", testId, trace);
}
/**
diff --git a/src/com/android/tradefed/result/XmlResultReporter.java b/src/com/android/tradefed/result/XmlResultReporter.java
index d6fae59..d48359b 100644
--- a/src/com/android/tradefed/result/XmlResultReporter.java
+++ b/src/com/android/tradefed/result/XmlResultReporter.java
@@ -19,10 +19,12 @@
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.TestResult.TestStatus;
import com.android.tradefed.util.StreamUtil;
import org.kxml2.io.KXmlSerializer;
@@ -98,9 +100,9 @@
}
@Override
- public void testFailed(TestFailure status, TestIdentifier test, String trace) {
- super.testFailed(status, test, trace);
- CLog.d("%s %s: %s", test, status, trace);
+ public void testFailed(TestIdentifier test, String trace) {
+ super.testFailed(test, trace);
+ CLog.d("%s : %s", test, trace);
}
/**
@@ -128,8 +130,7 @@
inputStream);
String msg = String.format("XML test result file generated at %s. Total tests %d, " +
- "Failed %d, Error %d", log.getPath(), getNumTotalTests(), getNumFailedTests(),
- getNumErrorTests());
+ "Failed %d", log.getPath(), getNumTotalTests(), getNumAllFailedTests());
Log.logAndDisplay(LogLevel.INFO, LOG_TAG, msg);
} catch (IOException e) {
Log.e(LOG_TAG, "Failed to generate report data");
@@ -164,8 +165,9 @@
serializer.startTag(ns, TESTSUITE);
serializer.attribute(ns, ATTR_NAME, mBuildInfo.getTestTag());
serializer.attribute(ns, ATTR_TESTS, Integer.toString(getNumTotalTests()));
- serializer.attribute(ns, ATTR_FAILURES, Integer.toString(getNumFailedTests()));
- serializer.attribute(ns, ATTR_ERRORS, Integer.toString(getNumErrorTests()));
+ serializer.attribute(ns, ATTR_FAILURES,
+ Integer.toString(getNumTestsInState(TestStatus.FAILURE)));
+ serializer.attribute(ns, ATTR_ERRORS, "0");
serializer.attribute(ns, ATTR_TIME, Long.toString(elapsedTime));
serializer.attribute(ns, TIMESTAMP, timestamp);
serializer.attribute(ns, HOSTNAME, "localhost");
diff --git a/src/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java b/src/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
new file mode 100644
index 0000000..922255f
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.util.AaptParser;
+import com.android.tradefed.util.AbiFormatter;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A {@link ITargetPreparer} that installs all apps from a
+ * {@link IDeviceBuildInfo#getTestsDir()} folder onto device. For individual
+ * test app install please look at {@link TestAppInstallSetup}
+ */
+@OptionClass(alias = "all-tests-installer")
+public class AllTestAppsInstallSetup implements ITargetCleaner, IAbiReceiver {
+ @Option(name = AbiFormatter.FORCE_ABI_STRING,
+ description = AbiFormatter.FORCE_ABI_DESCRIPTION,
+ importance = Importance.IF_UNSET)
+ private String mForceAbi = null;
+
+ @Option(name = "install-arg",
+ description = "Additional arguments to be passed to install command, "
+ + "including leading dash, e.g. \"-d\"")
+ private Collection<String> mInstallArgs = new ArrayList<>();
+
+ @Option(name = "cleanup-apks",
+ description = "Whether apks installed should be uninstalled after test. Note that the "
+ + "preparer does not verify if the apks are successfully removed.")
+ private boolean mCleanup = false;
+
+ private IAbi mAbi = null;
+
+ private List<String> mPackagesInstalled = new ArrayList<>();
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ DeviceNotAvailableException {
+ if (!(buildInfo instanceof IDeviceBuildInfo)) {
+ throw new TargetSetupError("Invalid buildInfo, expecting an IDeviceBuildInfo");
+ }
+ // Locate test dir where the test zip file was unzip to.
+ File testsDir = ((IDeviceBuildInfo) buildInfo).getTestsDir();
+ if (testsDir == null || !testsDir.exists()) {
+ throw new TargetSetupError("Failed to find a valid test zip directory.");
+ }
+ resolveAbi(device);
+ installApksRecursively(testsDir, device);
+ }
+
+ /**
+ * Install all apks found in a given directory.
+ *
+ * @param directory {@link File} directory to install from.
+ * @param device {@link ITestDevice} to install all apks to.
+ * @throws TargetSetupError
+ * @throws DeviceNotAvailableException
+ */
+ void installApksRecursively(File directory, ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ if (directory == null || !directory.isDirectory()) {
+ throw new TargetSetupError("Invalid test zip directory!");
+ }
+ CLog.d("Installing all apks found in dir %s ...", directory.getAbsolutePath());
+ File[] files = directory.listFiles();
+ for (File f : files) {
+ if (f.isDirectory()) {
+ installApksRecursively(f, device);
+ }
+ if (FileUtil.getExtension(f.getAbsolutePath()).toLowerCase().equals(".apk")) {
+ installApk(f, device);
+ } else {
+ CLog.d("Skipping %s because it is not an apk", f.getAbsolutePath());
+ }
+ }
+ }
+
+ /**
+ * Installs a single app to the device.
+ *
+ * @param appFile {@link File} of the apk to install.
+ * @param device {@link ITestDevice} to install the apk to.
+ * @throws TargetSetupError
+ * @throws DeviceNotAvailableException
+ */
+ void installApk(File appFile, ITestDevice device) throws TargetSetupError,
+ DeviceNotAvailableException {
+
+ CLog.d("Installing apk from %s ...", appFile.getAbsolutePath());
+ String result = device.installPackage(appFile, true,
+ mInstallArgs.toArray(new String[] {}));
+ if (result != null) {
+ throw new TargetSetupError(
+ String.format("Failed to install %s on %s. Reason: '%s'", appFile,
+ device.getSerialNumber(), result));
+ }
+ if (mCleanup) {
+ AaptParser parser = AaptParser.parse(appFile);
+ if (parser == null) {
+ throw new TargetSetupError("apk installed but AaptParser failed");
+ }
+ mPackagesInstalled.add(parser.getPackageName());
+ }
+ }
+
+ /**
+ * Determines the abi arguments when installing the apk, if needed.
+ *
+ * @param device {@link ITestDevice}
+ * @throws DeviceNotAvailableException
+ */
+ void resolveAbi(ITestDevice device) throws DeviceNotAvailableException {
+ if (mAbi != null && mForceAbi != null) {
+ throw new IllegalStateException("cannot specify both abi flags");
+ }
+ String abiName = null;
+ if (mAbi != null) {
+ abiName = mAbi.getName();
+ } else if (mForceAbi != null) {
+ abiName = AbiFormatter.getDefaultAbi(device, mForceAbi);
+ }
+ if (abiName != null) {
+ mInstallArgs.add(String.format("--abi %s", abiName));
+ }
+ }
+
+ @Override
+ public void setAbi(IAbi abi) {
+ mAbi = abi;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+ if (mCleanup && !(e instanceof DeviceNotAvailableException)) {
+ for (String packageName : mPackagesInstalled) {
+ String msg = device.uninstallPackage(packageName);
+ if (msg != null) {
+ CLog.w(String.format("error uninstalling package '%s': %s",
+ packageName, msg));
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/tradefed/targetprep/AltDirBehavior.java b/src/com/android/tradefed/targetprep/AltDirBehavior.java
new file mode 100644
index 0000000..f5e5cad
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/AltDirBehavior.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+/**
+ * An enum to define alternative directory behaviors for various test artifact installers/pushers
+ * <p>
+ * @see {@link TestAppInstallSetup}, {@link TestFilePushSetup}
+ */
+public enum AltDirBehavior {
+ /**
+ * The alternative directories should be used as a fallback to look up the build artifacts. That
+ * is, if build artifacts cannot be found at the regularly configured location.
+ */
+ FALLBACK,
+
+ /**
+ * The alternative directories should be used as an override to look up the build artifacts.
+ * That is, alternative directories should be searched first for build artifacts.
+ */
+ OVERRIDE,
+}
diff --git a/src/com/android/tradefed/targetprep/AppSetup.java b/src/com/android/tradefed/targetprep/AppSetup.java
index 0f1fec8..089d83d 100644
--- a/src/com/android/tradefed/targetprep/AppSetup.java
+++ b/src/com/android/tradefed/targetprep/AppSetup.java
@@ -28,6 +28,7 @@
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
/**
@@ -60,6 +61,10 @@
"optional flag(s) to provide when installing apks.")
private ArrayList<String> mInstallFlags = new ArrayList<>();
+ @Option(name = "post-install-cmd", description =
+ "optional post-install adb shell commands; can be repeated.")
+ private List<String> mPostInstallCmds = new ArrayList<>();
+
/** contains package names of installed apps. Used for uninstall */
private Set<String> mInstalledPkgs = new HashSet<String>();
@@ -102,6 +107,14 @@
}
}
+ if (mPostInstallCmds != null && !mPostInstallCmds.isEmpty()){
+ for (String cmd : mPostInstallCmds) {
+ // If the command had any output, the executeShellCommand method will log it at the
+ // VERBOSE level; so no need to do any logging from here.
+ CLog.d("About to run setup command on device %s: %s", device.getSerialNumber(), cmd);
+ device.executeShellCommand(cmd);
+ }
+ }
}
private void addPackageNameToUninstall(File apkFile) throws TargetSetupError {
diff --git a/src/com/android/tradefed/targetprep/BuildInfoRecorder.java b/src/com/android/tradefed/targetprep/BuildInfoRecorder.java
new file mode 100644
index 0000000..a567408
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/BuildInfoRecorder.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * An {@link ITargetPreparer} that writes build info meta data into a specified file.
+ * <p>
+ * The file is written in simple key-value pair format; each line of the file has:<br>
+ * <code>key=value</code><br>
+ * where <code>key</code> is a meta data field from {@link IBuildInfo}
+ * <p>
+ * Currently, only build id is written.
+ */
+@OptionClass(alias = "build-info-recorder")
+public class BuildInfoRecorder implements ITargetPreparer {
+
+ @Option(name = "build-info-file", description = "when specified, build info will be written "
+ + "into the file specified. Any existing file will be overwritten.")
+ private File mBuildInfoFile = null;
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ BuildError, DeviceNotAvailableException {
+ if (mBuildInfoFile != null) {
+ try {
+ String alias = buildInfo.getBuildAttributes().get("build_alias");
+ if (alias == null) {
+ alias = buildInfo.getBuildId();
+ }
+ FileUtil.writeToFile(String.format("%s=%s\n%s=%s\n",
+ "build_id", buildInfo.getBuildId(),
+ "build_alias", alias),
+ mBuildInfoFile);
+ } catch (IOException ioe) {
+ CLog.e("Exception while writing build info into %s",
+ mBuildInfoFile.getAbsolutePath());
+ CLog.e(ioe);
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/tradefed/targetprep/DeviceCleaner.java b/src/com/android/tradefed/targetprep/DeviceCleaner.java
index a7c2151..e2d1c18 100644
--- a/src/com/android/tradefed/targetprep/DeviceCleaner.java
+++ b/src/com/android/tradefed/targetprep/DeviceCleaner.java
@@ -20,6 +20,7 @@
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.TestDeviceState;
import com.android.tradefed.log.LogUtil.CLog;
@@ -77,7 +78,16 @@
@Override
public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
throws DeviceNotAvailableException {
- clean(device);
+ if (e instanceof DeviceFailedToBootError) {
+ CLog.w("boot failure: attempting to stop runtime instead of cleanup");
+ try {
+ device.executeShellCommand("stop");
+ } catch (DeviceUnresponsiveException due) {
+ CLog.w("boot failure: ignored DeviceUnresponsiveException during forced cleanup");
+ }
+ } else {
+ clean(device);
+ }
}
/**
diff --git a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
index b9b4ce8..c2ca9e2 100644
--- a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
@@ -59,6 +59,14 @@
"specify if system should always be flashed even if already running desired build.")
private boolean mForceSystemFlash = false;
+ /*
+ * A temporary workaround for special builds. Should be removed after changes from build team.
+ * Bug: 18078421
+ */
+ @Option(name = "skip-post-flash-flavor-check", description =
+ "specify if system flavor should not be checked after flash")
+ private boolean mSkipPostFlashFlavorCheck = false;
+
@Option(name = "wipe-skip-list", description =
"list of /data subdirectories to NOT wipe when doing UserDataFlashOption.TESTS_ZIP")
private Collection<String> mDataWipeSkipList = new ArrayList<String>();
@@ -192,42 +200,48 @@
if (!(buildInfo instanceof IDeviceBuildInfo)) {
throw new IllegalArgumentException("Provided buildInfo is not a IDeviceBuildInfo");
}
- IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo)buildInfo;
- device.setRecoveryMode(RecoveryMode.ONLINE);
- IDeviceFlasher flasher = createFlasher(device);
- // only surround fastboot related operations with flashing permit restriction
+ // don't allow interruptions during flashing operations.
+ getRunUtil().allowInterrupt(false);
try {
- takeFlashingPermit();
+ IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo)buildInfo;
+ device.setRecoveryMode(RecoveryMode.ONLINE);
+ IDeviceFlasher flasher = createFlasher(device);
+ // only surround fastboot related operations with flashing permit restriction
+ try {
+ takeFlashingPermit();
- flasher.overrideDeviceOptions(device);
- flasher.setUserDataFlashOption(mUserDataFlashOption);
- flasher.setForceSystemFlash(mForceSystemFlash);
- flasher.setDataWipeSkipList(mDataWipeSkipList);
- preEncryptDevice(device, flasher);
- flasher.flash(device, deviceBuild);
+ flasher.overrideDeviceOptions(device);
+ flasher.setUserDataFlashOption(mUserDataFlashOption);
+ flasher.setForceSystemFlash(mForceSystemFlash);
+ flasher.setDataWipeSkipList(mDataWipeSkipList);
+ preEncryptDevice(device, flasher);
+ flasher.flash(device, deviceBuild);
+ } finally {
+ returnFlashingPermit();
+ }
+ device.waitForDeviceOnline();
+ // device may lose date setting if wiped, update with host side date in case anything on
+ // device side malfunction with an invalid date
+ if (device.enableAdbRoot()) {
+ device.setDate(null);
+ }
+ checkBuild(device, deviceBuild);
+ postEncryptDevice(device, flasher);
+ // only want logcat captured for current build, delete any accumulated log data
+ device.clearLogcat();
+ try {
+ device.setRecoveryMode(RecoveryMode.AVAILABLE);
+ device.waitForDeviceAvailable(mDeviceBootTime);
+ } catch (DeviceUnresponsiveException e) {
+ // assume this is a build problem
+ throw new DeviceFailedToBootError(String.format(
+ "Device %s did not become available after flashing %s",
+ device.getSerialNumber(), deviceBuild.getDeviceBuildId()));
+ }
+ device.postBootSetup();
} finally {
- returnFlashingPermit();
+ getRunUtil().allowInterrupt(true);
}
- device.waitForDeviceOnline();
- // device may lose date setting if wiped, update with host side date in case anything on
- // device side malfunction with an invalid date
- if (device.enableAdbRoot()) {
- device.setDate(null);
- }
- checkBuild(device, deviceBuild);
- postEncryptDevice(device, flasher);
- // only want logcat captured for current build, delete any accumulated log data
- device.clearLogcat();
- try {
- device.setRecoveryMode(RecoveryMode.AVAILABLE);
- device.waitForDeviceAvailable(mDeviceBootTime);
- } catch (DeviceUnresponsiveException e) {
- // assume this is a build problem
- throw new DeviceFailedToBootError(String.format(
- "Device %s did not become available after flashing %s",
- device.getSerialNumber(), deviceBuild.getDeviceBuildId()));
- }
- device.postBootSetup();
}
/**
@@ -236,8 +250,13 @@
*/
private void checkBuild(ITestDevice device, IDeviceBuildInfo deviceBuild)
throws DeviceNotAvailableException {
- checkBuildAttribute(deviceBuild.getBuildId(), device.getBuildId());
- checkBuildAttribute(deviceBuild.getBuildFlavor(), device.getBuildFlavor());
+ // Need to use deviceBuild.getDeviceBuildId instead of getBuildId because the build info
+ // could be an AppBuildInfo and return app build id. Need to be more explicit that we
+ // check for the device build here.
+ checkBuildAttribute(deviceBuild.getDeviceBuildId(), device.getBuildId());
+ if (!mSkipPostFlashFlavorCheck) {
+ checkBuildAttribute(deviceBuild.getBuildFlavor(), device.getBuildFlavor());
+ }
// TODO: check bootloader and baseband versions too
}
diff --git a/src/com/android/tradefed/targetprep/DeviceSetup.java b/src/com/android/tradefed/targetprep/DeviceSetup.java
index 1de8e04..424e64c 100644
--- a/src/com/android/tradefed/targetprep/DeviceSetup.java
+++ b/src/com/android/tradefed/targetprep/DeviceSetup.java
@@ -17,323 +17,1077 @@
package com.android.tradefed.targetprep;
import com.android.ddmlib.IDevice;
-import com.android.ddmlib.Log;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.MultiMap;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.regex.Pattern;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/**
* A {@link ITargetPreparer} that configures a device for testing based on provided {@link Option}s.
- * <p/>
+ * <p>
* Requires a device where 'adb root' is possible, typically a userdebug build type.
- * <p/>
- * Should be performed *after* a new build is flashed.
+ * </p><p>
+ * Should be performed <strong>after</strong> a new build is flashed.
+ * </p>
*/
@OptionClass(alias = "device-setup")
public class DeviceSetup implements ITargetPreparer, ITargetCleaner {
- private static final String LOG_TAG = "DeviceSetup";
- private static final Pattern RELEASE_BUILD_NAME_PATTERN =
- Pattern.compile("[A-Z]{3}\\d{2}[A-Z]?");
- private static final String PERSIST_PREFIX = "persist.";
+ /**
+ * Enum used to record ON/OFF state with a IGNORE no-op state.
+ */
+ public enum BinaryState {
+ IGNORE,
+ ON,
+ OFF;
+ }
- @Option(name="wifi-network", description="the name of wifi network to connect to.")
- private String mWifiNetwork = null;
+ // Networking
+ @Option(name = "airplane-mode",
+ description = "Turn airplane mode on or off")
+ protected BinaryState mAirplaneMode = BinaryState.IGNORE;
+ // ON: settings put global airplane_mode_on 1
+ // am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true
+ // OFF: settings put global airplane_mode_on 0
+ // am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false
- @Option(name="wifi-psk", description="WPA-PSK passphrase of wifi network to connect to.")
- private String mWifiPsk = null;
+ @Option(name = "wifi",
+ description = "Turn wifi on or off")
+ protected BinaryState mWifi = BinaryState.IGNORE;
+ // ON: settings put global wifi_on 1
+ // svc wifi enable
+ // OFF: settings put global wifi_off 0
+ // svc wifi disable
- @Option(name = "disconnect-wifi-after-test", description =
- "disconnect from wifi network after test completes.")
+ @Option(name = "wifi-network",
+ description = "The SSID of the network to connect to. Will only attempt to " +
+ "connect to a network if set")
+ protected String mWifiSsid = null;
+
+ @Option(name = "wifi-psk",
+ description = "The passphrase used to connect to a secured network")
+ protected String mWifiPsk = null;
+
+ @Option(name = "wifi-watchdog",
+ description = "Turn wifi watchdog on or off")
+ protected BinaryState mWifiWatchdog = BinaryState.IGNORE;
+ // ON: settings put global wifi_watchdog 1
+ // OFF: settings put global wifi_watchdog 0
+
+ @Option(name = "wifi-scan-always-enabled",
+ description = "Turn wifi scan always enabled on or off")
+ protected BinaryState mWifiScanAlwaysEnabled = BinaryState.IGNORE;
+ // ON: settings put global wifi_scan_always_enabled 1
+ // OFF: settings put global wifi_scan_always_enabled 0
+
+ @Option(name = "ethernet",
+ description = "Turn ethernet on or off")
+ protected BinaryState mEthernet = BinaryState.IGNORE;
+ // ON: ifconfig eth0 up
+ // OFF: ifconfig eth0 down
+
+ @Option(name = "bluetooth",
+ description = "Turn bluetooth on or off")
+ protected BinaryState mBluetooth = BinaryState.IGNORE;
+ // ON: service call bluetooth_manager 6
+ // OFF: service call bluetooth_manager 8
+
+ // Screen
+ @Option(name = "screen-adaptive-brightness",
+ description = "Turn screen adaptive brightness on or off")
+ protected BinaryState mScreenAdaptiveBrightness = BinaryState.IGNORE;
+ // ON: settings put system screen_brightness_mode 1
+ // OFF: settings put system screen_brightness_mode 0
+
+ @Option(name = "screen-brightness",
+ description = "Set the screen brightness. This is uncalibrated from product to product")
+ protected Integer mScreenBrightness = null;
+ // settings put system screen_brightness $N
+
+ @Option(name = "screen-always-on",
+ description = "Turn 'screen always on' on or off. If ON, then screen-timeout-secs " +
+ "must be unset. Will only work when the device is plugged in")
+ protected BinaryState mScreenAlwaysOn = BinaryState.ON;
+ // ON: svc power stayon true
+ // OFF: svc power stayon false
+
+ @Option(name = "screen-timeout-secs",
+ description = "Set the screen timeout in seconds. If set, then screen-always-on must " +
+ "be OFF or DEFAULT")
+ protected Long mScreenTimeoutSecs = null;
+ // settings put system screen_off_timeout $(N * 1000)
+
+ @Option(name = "screen-ambient-mode",
+ description = "Turn screen ambient mode on or off")
+ protected BinaryState mScreenAmbientMode = BinaryState.IGNORE;
+ // ON: settings put secure doze_enabled 1
+ // OFF: settings put secure doze_enabled 0
+
+ @Option(name = "wake-gesture",
+ description = "Turn wake gesture on or off")
+ protected BinaryState mWakeGesture = BinaryState.IGNORE;
+ // ON: settings put secure wake_gesture_enabled 1
+ // OFF: settings put secure wake_gesture_enabled 0
+
+ @Option(name = "screen-saver",
+ description = "Turn screen saver on or off")
+ protected BinaryState mScreenSaver = BinaryState.IGNORE;
+ // ON: settings put secure screensaver_enabled 1
+ // OFF: settings put secure screensaver_enabled 0
+
+ @Option(name = "notification-led",
+ description = "Turn the notification led on or off")
+ protected BinaryState mNotificationLed = BinaryState.IGNORE;
+ // ON: settings put system notification_light_pulse 1
+ // OFF: settings put system notification_light_pulse 0
+
+ // Media
+ @Option(name = "trigger-media-mounted",
+ description = "Trigger a MEDIA_MOUNTED broadcast")
+ protected boolean mTriggerMediaMounted = false;
+ // am broadcast -a android.intent.action.MEDIA_MOUNTED -d file://${EXTERNAL_STORAGE}
+
+ // Location
+ @Option(name = "location-gps",
+ description = "Turn the GPS location on or off")
+ protected BinaryState mLocationGps = BinaryState.IGNORE;
+ // ON: settings put secure location_providers_allowed +gps
+ // OFF: settings put secure location_providers_allowed -gps
+
+ @Option(name = "location-network",
+ description = "Turn the network location on or off")
+ protected BinaryState mLocationNetwork = BinaryState.IGNORE;
+ // ON: settings put secure location_providers_allowed +network
+ // OFF: settings put secure location_providers_allowed -network
+
+ // Sensor
+ @Option(name = "auto-rotate",
+ description = "Turn auto rotate on or off")
+ protected BinaryState mAutoRotate = BinaryState.IGNORE;
+ // ON: settings put system accelerometer_rotation 1
+ // OFF: settings put system accelerometer_rotation 0
+
+ // Power
+ @Option(name = "battery-saver-mode",
+ description = "Turn battery saver mode manually on or off. If OFF but battery is " +
+ "less battery-saver-trigger, the device will still go into battery saver mode")
+ protected BinaryState mBatterySaver = BinaryState.IGNORE;
+ // ON: dumpsys battery set usb 0
+ // settings put global low_power 1
+ // OFF: settings put global low_power 0
+
+ @Option(name = "battery-saver-trigger",
+ description = "Set the battery saver trigger level. Should be [1-99] to enable, or " +
+ "0 to disable automatic battery saver mode")
+ protected Integer mBatterySaverTrigger = null;
+ // settings put global low_power_trigger_level $N
+
+ @Option(name = "disable-doze",
+ description = "Disable device from going into doze mode. This option is only " +
+ "applicable for M+")
+ protected boolean mDisableDoze = false;
+ // dumpsys deviceidle disable
+
+ // Time
+ @Option(name = "auto-update-time",
+ description = "Turn auto update time on or off")
+ protected BinaryState mAutoUpdateTime = BinaryState.IGNORE;
+ // ON: settings put system auto_time 1
+ // OFF: settings put system auto_time 0
+
+ @Option(name = "auto-update-timezone",
+ description = "Turn auto update timezone on or off")
+ protected BinaryState mAutoUpdateTimezone = BinaryState.IGNORE;
+ // ON: settings put system auto_timezone 1
+ // OFF: settings put system auto_timezone 0
+
+ // Calling
+ @Option(name = "disable-dialing",
+ description = "Disable dialing")
+ protected boolean mDisableDialing = true;
+ // setprop ro.telephony.disable-call true"
+
+ @Option(name = "default-sim-data",
+ description = "Set the default sim card slot for data. Leave unset for single SIM " +
+ "devices")
+ protected Integer mDefaultSimData = null;
+ // settings put global multi_sim_data_call $N
+
+ @Option(name = "default-sim-voice",
+ description = "Set the default sim card slot for voice calls. Leave unset for single " +
+ "SIM devices")
+ protected Integer mDefaultSimVoice = null;
+ // settings put global multi_sim_voice_call $N
+
+ @Option(name = "default-sim-sms",
+ description = "Set the default sim card slot for SMS. Leave unset for single SIM " +
+ "devices")
+ protected Integer mDefaultSimSms = null;
+ // settings put global multi_sim_sms $N
+
+ // Audio
+ private static final boolean DEFAULT_DISABLE_AUDIO = true;
+ @Option(name = "disable-audio",
+ description = "Disable the audio")
+ protected boolean mDisableAudio = DEFAULT_DISABLE_AUDIO;
+ // setprop ro.audio.silent 1"
+
+ // Test harness
+ @Option(name = "disable",
+ description = "Disable the device setup")
+ protected boolean mDisable = false;
+
+ @Option(name = "force-skip-system-props",
+ description = "Force setup to not modify any device system properties. All other " +
+ "system property options will be ignored")
+ protected boolean mForceSkipSystemProps = false;
+
+ @Option(name = "force-skip-settings",
+ description = "Force setup to not modify any device settings. All other setting " +
+ "options will be ignored.")
+ protected boolean mForceSkipSettings = false;
+
+ @Option(name = "force-skip-run-commands",
+ description = "Force setup to not run any additional commands. All other commands " +
+ "will be ignored.")
+ protected boolean mForceSkipRunCommands = false;
+
+ @Option(name = "set-test-harness",
+ description = "Set the read-only test harness flag on boot")
+ protected boolean mSetTestHarness = true;
+ // setprop ro.monkey 1
+ // setprop ro.test_harness 1
+
+ @Option(name = "disable-dalvik-verifier",
+ description = "Disable the dalvik verifier on device. Allows package-private " +
+ "framework tests to run.")
+ protected boolean mDisableDalvikVerifier = false;
+ // setprop dalvik.vm.dexopt-flags v=n
+
+ @Option(name = "set-property",
+ description = "Set the specified property on boot. Option may be repeated but only " +
+ "the last value for a given key will be set.")
+ protected Map<String, String> mSetProps = new HashMap<>();
+
+ @Option(name = "set-system-setting",
+ description = "Change a system (non-secure) setting. Option may be repeated and all " +
+ "key/value pairs will be set in order.")
+ // Use a Multimap since it is possible for a setting to have multiple values for the same key
+ protected MultiMap<String, String> mSystemSettings = new MultiMap<>();
+
+ @Option(name = "set-secure-setting",
+ description = "Change a secure setting. Option may be repeated and all key/value " +
+ "pairs will be set in order.")
+ // Use a Multimap since it is possible for a setting to have multiple values for the same key
+ protected MultiMap<String, String> mSecureSettings = new MultiMap<>();
+
+ @Option(name = "set-global-setting",
+ description = "Change a global setting. Option may be repeated and all key/value " +
+ "pairs will be set in order.")
+ // Use a Multimap since it is possible for a setting to have multiple values for the same key
+ protected MultiMap<String, String> mGlobalSettings = new MultiMap<>();
+
+ protected List<String> mRunCommandBeforeSettings = new ArrayList<>();
+
+ @Option(name = "run-command",
+ description = "Run an adb shell command. Option may be repeated")
+ protected List<String> mRunCommandAfterSettings = new ArrayList<>();
+
+ @Option(name = "disconnect-wifi-after-test",
+ description = "Disconnect from wifi network after test completes.")
private boolean mDisconnectWifiAfterTest = true;
- @Option(name="min-external-store-space", description="the minimum amount of free space in KB" +
- " that must be present on device's external storage.")
- // require 500K by default. Values <=0 mean external storage is not required
- private long mMinExternalStoreSpace = 500;
+ private static final long DEFAULT_MIN_EXTERNAL_STORAGE_KB = 500;
+ @Option(name = "min-external-storage-kb",
+ description="The minimum amount of free space in KB that must be present on device's " +
+ "external storage.")
+ protected long mMinExternalStorageKb = DEFAULT_MIN_EXTERNAL_STORAGE_KB;
@Option(name = "local-data-path",
- description = "optional local file path of test data to sync to device's external " +
+ description = "Optional local file path of test data to sync to device's external " +
"storage. Use --remote-data-path to set remote location.")
- private File mLocalDataFile = null;
+ protected File mLocalDataFile = null;
@Option(name = "remote-data-path",
- description = "optional file path on device's external storage to sync test data. " +
+ description = "Optional file path on device's external storage to sync test data. " +
"Must be used with --local-data-path.")
- private String mRemoteDataPath = null;
+ protected String mRemoteDataPath = null;
- @Option(name = "force-skip-system-props", description =
- "force setup to not modify any device system properties. " +
- "All other system property options will be ignored.")
- private boolean mForceNoSystemProps = false;
+ // Deprecated options follow
+ @Option(name = "min-external-store-space",
+ description = "deprecated, use option min-external-storage-kb. The minimum amount of " +
+ "free space in KB that must be present on device's external storage.")
+ @Deprecated
+ private long mDeprecatedMinExternalStoreSpace = DEFAULT_MIN_EXTERNAL_STORAGE_KB;
- @Option(name="disable-dialing", description="set disable dialing property on boot.")
- private boolean mDisableDialing = true;
+ @Option(name = "audio-silent",
+ description = "deprecated, use option disable-audio. set ro.audio.silent on boot.")
+ @Deprecated
+ private boolean mDeprecatedSetAudioSilent = DEFAULT_DISABLE_AUDIO;
- @Option(name="set-test-harness", description="set the read-only test harness flag on boot. " +
- "Requires adb root.")
- private boolean mSetTestHarness = true;
+ @Option(name = "setprop",
+ description = "deprecated, use option set-property. set the specified property on " +
+ "boot. Format: --setprop key=value. May be repeated.")
+ @Deprecated
+ private Collection<String> mDeprecatedSetProps = new ArrayList<String>();
- @Option(name="audio-silent", description="set ro.audio.silent on boot.")
- private boolean mSetAudioSilent = true;
-
- @Option(name="disable-dalvik-verifier", description="disable the dalvik verifier on device. "
- + "Allows package-private framework tests to run.")
- private boolean mDisableDalvikVerifier = false;
-
- @Option(name="setprop", description="set the specified property on boot. " +
- "Format: --setprop key=value. May be repeated.")
- private Collection<String> mSetProps = new ArrayList<String>();
-
- /**
- * Sets the local data path to use
- * <p/>
- * Exposed for unit testing
- */
- void setLocalDataPath(File localPath) {
- mLocalDataFile = localPath;
- }
-
- /**
- * Sets the remote data path to use
- * <p/>
- * Exposed for unit testing
- */
- void setRemoteDataPath(String remotePath) {
- mRemoteDataPath = remotePath;
- }
-
- /**
- * Sets the wifi network ssid to setup.
- * <p/>
- * Exposed for unit testing
- */
- void setWifiNetwork(String network) {
- mWifiNetwork = network;
- }
-
- /**
- * Sets the minimum external store space
- * <p/>
- * Exposed for unit testing
- */
- void setMinExternalStoreSpace(int minKBytes) {
- mMinExternalStoreSpace = minKBytes;
- }
-
- /**
- * Adds a property to the list of properties to set
- * <p/>
- * Exposed for unit testing
- */
- void addSetProperty(String prop) {
- mSetProps.add(prop);
- }
+ private static final String PERSIST_PREFIX = "persist.";
/**
* {@inheritDoc}
*/
@Override
- public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
- DeviceNotAvailableException, BuildError {
- Log.i(LOG_TAG, String.format("Performing setup on %s", device.getSerialNumber()));
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws DeviceNotAvailableException,
+ TargetSetupError {
+ if (mDisable) {
+ return;
+ }
+
+ CLog.i("Performing setup on %s", device.getSerialNumber());
if (!device.enableAdbRoot()) {
- throw new TargetSetupError(String.format("failed to enable adb root on %s",
+ throw new TargetSetupError(String.format("Failed to enable adb root on %s",
device.getSerialNumber()));
}
- configureSystemProperties(device);
-
+ // Convert deprecated options into current options
+ processDeprecatedOptions();
+ // Convert options into settings and run commands
+ processOptions(device);
+ // Change system props (will reboot device)
+ changeSystemProps(device);
+ // Run commands designated to be run before changing settings
+ runCommands(device, mRunCommandBeforeSettings);
+ // Change settings
changeSettings(device);
-
- keepScreenOn(device);
-
- connectToWifi(device);
-
+ // Connect wifi after settings since this may take a while
+ connectWifi(device);
+ // Sync data after settings since this may take a while
syncTestData(device);
-
+ // Run commands designated to be run after changing settings
+ runCommands(device, mRunCommandAfterSettings);
+ // Throw an error if there is not enough storage space
checkExternalStoreSpace(device);
device.clearErrorDialogs();
}
/**
- * Configures device system properties.
- * <p/>
- * Device will be rebooted if any property is changed.
- *
- * @param device
- * @throws TargetSetupError
- * @throws DeviceNotAvailableException
- */
- private void configureSystemProperties(ITestDevice device) throws TargetSetupError,
- DeviceNotAvailableException {
- if (mForceNoSystemProps) {
- return;
- }
- // build the local.prop file contents with properties to change
- StringBuilder propertyBuilder = new StringBuilder();
- if (mDisableDialing) {
- propertyBuilder.append("ro.telephony.disable-call=true\n");
- }
- if (mSetTestHarness) {
- // set both ro.monkey and ro.test_harness, for compatibility with older platforms
- propertyBuilder.append("ro.monkey=1\n");
- propertyBuilder.append("ro.test_harness=1\n");
- }
- if (mSetAudioSilent) {
- propertyBuilder.append("ro.audio.silent=1\n");
- }
- if (mDisableDalvikVerifier) {
- propertyBuilder.append("dalvik.vm.dexopt-flags = v=n\n");
- }
- for (String prop : mSetProps) {
- if (prop.startsWith(PERSIST_PREFIX)) {
- prop = prop.replace('=', ' ');
- device.executeShellCommand("setprop " + prop);
- } else {
- propertyBuilder.append(prop);
- propertyBuilder.append("\n");
- }
- }
- if (propertyBuilder.length() > 0) {
- // create a local.prop file, and push it to /data/local.prop
- boolean result = device.pushString(propertyBuilder.toString(), "/data/local.prop");
- if (!result) {
- throw new TargetSetupError(String.format("Failed to push file to %s",
- device.getSerialNumber()));
- }
- // Set reasonable permissions for /data/local.prop
- device.executeShellCommand("chmod 644 /data/local.prop");
- Log.i(LOG_TAG, String.format(
- "Setup requires system property change. Reboot of %s required",
- device.getSerialNumber()));
- device.reboot();
- }
- }
-
- /**
- * Change additional settings for the device. This is intended to be overridden by subclass for
- * additional change of settings.
- *
- * @param device
- * @throws DeviceNotAvailableException
- * @throws TargetSetupError
- */
- protected void changeSettings(ITestDevice device) throws DeviceNotAvailableException,
- TargetSetupError {
- // ignore
- }
-
- /**
- * @param device
- * @throws DeviceNotAvailableException
- */
- private void keepScreenOn(ITestDevice device) throws DeviceNotAvailableException {
- device.executeShellCommand("svc power stayon true");
- }
-
- /**
- * Check that device external store has the required space
- *
- * @param device
- * @throws DeviceNotAvailableException if device does not have required space
- */
- private void checkExternalStoreSpace(ITestDevice device) throws DeviceNotAvailableException {
- if (mMinExternalStoreSpace > 0) {
- long freeSpace = device.getExternalStoreFreeSpace();
- if (freeSpace < mMinExternalStoreSpace) {
- throw new DeviceNotAvailableException(String.format(
- "External store free space %dK is less than required %dK for device %s",
- freeSpace , mMinExternalStoreSpace, device.getSerialNumber()));
- }
- }
- }
-
- /**
- * Connect to wifi network if specified
- *
- * @param device
- * @throws DeviceNotAvailableException
- * @throws TargetSetupError if failed to connect to wifi
- */
- private void connectToWifi(ITestDevice device) throws DeviceNotAvailableException,
- TargetSetupError {
- if (mWifiNetwork != null) {
- if (!device.connectToWifiNetwork(mWifiNetwork, mWifiPsk)) {
- throw new TargetSetupError(String.format(
- "Failed to connect to wifi network %s on %s", mWifiNetwork,
- device.getSerialNumber()));
- }
- }
- }
-
- /**
- * Syncs a set of test data files, specified via local-data-path, to devices external storage.
- *
- * @param device the {@link ITestDevice} to sync data to
- * @throws TargetSetupError if data fails to sync
- */
- void syncTestData(ITestDevice device) throws TargetSetupError, DeviceNotAvailableException {
- if (mLocalDataFile != null) {
- if (!mLocalDataFile.exists() || !mLocalDataFile.isDirectory()) {
- throw new TargetSetupError(String.format("local-data-path %s is not a directory",
- mLocalDataFile.getAbsolutePath()));
-
- }
- String fullRemotePath = device.getIDevice().getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
- if (fullRemotePath == null) {
- throw new TargetSetupError(String.format(
- "failed to get external storage path on device %s",
- device.getSerialNumber()));
- }
- if (mRemoteDataPath != null) {
- fullRemotePath = String.format("%s/%s", fullRemotePath, mRemoteDataPath);
- }
- boolean result = device.syncFiles(mLocalDataFile, fullRemotePath);
- if (!result) {
- // TODO: get exact error code and respond accordingly
- throw new TargetSetupError(String.format(
- "failed to sync test data from local-data-path %s to %s on device %s",
- mLocalDataFile.getAbsolutePath(), fullRemotePath,
- device.getSerialNumber()));
- }
- }
- }
-
- protected boolean isReleaseBuildName(String name) {
- return RELEASE_BUILD_NAME_PATTERN.matcher(name).matches();
- }
-
- /**
* {@inheritDoc}
*/
@Override
public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
throws DeviceNotAvailableException {
- Log.i(LOG_TAG, String.format("Performing teardown on %s", device.getSerialNumber()));
+ if (mDisable) {
+ return;
+ }
- if (mWifiNetwork != null && mDisconnectWifiAfterTest) {
- disconnectFromWifi(device);
+ CLog.i("Performing teardown on %s", device.getSerialNumber());
+
+ if (e instanceof DeviceFailedToBootError) {
+ CLog.d("boot failure: skipping teardown");
+ return;
+ }
+
+ // Only try to disconnect if wifi ssid is set since isWifiEnabled() is a heavy operation
+ // which should be avoided when possible
+ if (mDisconnectWifiAfterTest && mWifiSsid != null && device.isWifiEnabled()) {
+ boolean result = device.disconnectFromWifi();
+ if (result) {
+ CLog.i("Successfully disconnected from wifi network on %s",
+ device.getSerialNumber());
+ } else {
+ CLog.w("Failed to disconnect from wifi network on %s", device.getSerialNumber());
+ }
}
}
- private void disconnectFromWifi(ITestDevice device) throws DeviceNotAvailableException {
- if (device.isWifiEnabled()) {
- if (!device.disconnectFromWifi()) {
- CLog.w("Failed to disconnect from wifi network on %s", device.getSerialNumber());
- return;
+ /**
+ * Processes the deprecated options converting them into the currently used options.
+ * <p>
+ * This method should be run before any other processing methods. Will throw a
+ * {@link TargetSetupError} if the deprecated option overrides a specified non-deprecated
+ * option.
+ * </p>
+ * @throws TargetSetupError if there is a conflict
+ */
+ public void processDeprecatedOptions() throws TargetSetupError {
+ if (mDeprecatedMinExternalStoreSpace != DEFAULT_MIN_EXTERNAL_STORAGE_KB) {
+ if (mMinExternalStorageKb != DEFAULT_MIN_EXTERNAL_STORAGE_KB) {
+ throw new TargetSetupError("Deprecated option min-external-store-space conflicts " +
+ "with option min-external-storage-kb");
}
- CLog.i("Successfully disconnected from wifi network on %s", device.getSerialNumber());
+ mMinExternalStorageKb = mDeprecatedMinExternalStoreSpace;
}
+
+ if (mDeprecatedSetAudioSilent != DEFAULT_DISABLE_AUDIO) {
+ if (mDisableAudio != DEFAULT_DISABLE_AUDIO) {
+ throw new TargetSetupError("Deprecated option audio-silent conflicts with " +
+ "option disable-audio");
+ }
+ mDisableAudio = mDeprecatedSetAudioSilent;
+ }
+
+ if (!mDeprecatedSetProps.isEmpty()) {
+ if (!mSetProps.isEmpty()) {
+ throw new TargetSetupError("Deprecated option setprop conflicts with option " +
+ "set-property ");
+ }
+ for (String prop : mDeprecatedSetProps) {
+ String[] parts = prop.split("=", 2);
+ String key = parts[0].trim();
+ String value = parts.length == 2 ? parts[1].trim() : "";
+ mSetProps.put(key, value);
+ }
+ }
+ }
+
+ /**
+ * Process all the {@link Option}s and turn them into system props, settings, or run commands.
+ * Does not run any commands on the device at this time.
+ * <p>
+ * Exposed so that children classes may override this.
+ * </p>
+ *
+ * @param device The {@link ITestDevice}
+ * @throws DeviceNotAvailableException if the device is not available
+ * @throws TargetSetupError if the {@link Option}s conflict
+ */
+ public void processOptions(ITestDevice device) throws DeviceNotAvailableException,
+ TargetSetupError {
+ setSettingForBinaryState(mWifi, mGlobalSettings, "wifi_on", "1", "0");
+ setCommandForBinaryState(mWifi, mRunCommandAfterSettings,
+ "svc wifi enable", "svc wifi disable");
+
+ setSettingForBinaryState(mWifiWatchdog, mGlobalSettings, "wifi_watchdog", "1", "0");
+
+ setSettingForBinaryState(mWifiScanAlwaysEnabled, mGlobalSettings,
+ "wifi_scan_always_enabled", "1", "0");
+
+ setCommandForBinaryState(mEthernet, mRunCommandAfterSettings,
+ "ifconfig eth0 up", "ifconfig eth0 down");
+
+ setCommandForBinaryState(mBluetooth, mRunCommandAfterSettings,
+ "service call bluetooth_manager 6", "service call bluetooth_manager 8");
+
+ if (mScreenBrightness != null && BinaryState.ON.equals(mScreenAdaptiveBrightness)) {
+ throw new TargetSetupError("Option screen-brightness cannot be set when " +
+ "screen-adaptive-brightness is set to ON");
+ }
+
+ setSettingForBinaryState(mScreenAdaptiveBrightness, mSystemSettings,
+ "screen_brightness_mode", "1", "0");
+
+ if (mScreenBrightness != null) {
+ mSystemSettings.put("screen_brightness", Integer.toString(mScreenBrightness));
+ }
+
+ setCommandForBinaryState(mScreenAlwaysOn, mRunCommandBeforeSettings,
+ "svc power stayon true", "svc power stayon false");
+
+ if (mScreenTimeoutSecs != null) {
+ mSystemSettings.put("screen_off_timeout", Long.toString(mScreenTimeoutSecs * 1000));
+ }
+
+ setSettingForBinaryState(mScreenAmbientMode, mSecureSettings, "doze_enabled", "1", "0");
+
+ setSettingForBinaryState(mWakeGesture, mSecureSettings, "wake_gesture_enabled", "1", "0");
+
+ setSettingForBinaryState(mScreenSaver, mSecureSettings, "screensaver_enabled", "1", "0");
+
+ setSettingForBinaryState(mNotificationLed, mSystemSettings,
+ "notification_light_pulse", "1", "0");
+
+ if (mTriggerMediaMounted) {
+ mRunCommandAfterSettings.add("am broadcast -a android.intent.action.MEDIA_MOUNTED -d " +
+ "file://${EXTERNAL_STORAGE}");
+ }
+
+ setSettingForBinaryState(mLocationGps, mSecureSettings,
+ "location_providers_allowed", "+gps", "-gps");
+
+ setSettingForBinaryState(mLocationNetwork, mSecureSettings,
+ "location_providers_allowed", "+network", "-network");
+
+ setSettingForBinaryState(mAutoRotate, mSystemSettings, "accelerometer_rotation", "1", "0");
+
+ setCommandForBinaryState(mBatterySaver, mRunCommandBeforeSettings,
+ "dumpsys battery set usb 0", null);
+ setSettingForBinaryState(mBatterySaver, mGlobalSettings, "low_power", "1", "0");
+
+ if (mBatterySaverTrigger != null) {
+ mGlobalSettings.put("low_power_trigger_level", Integer.toString(mBatterySaverTrigger));
+ }
+
+ if (mDisableDoze) {
+ mRunCommandAfterSettings.add("dumpsys deviceidle disable");
+ }
+
+ setSettingForBinaryState(mAutoUpdateTime, mSystemSettings, "auto_time", "1", "0");
+
+ setSettingForBinaryState(mAutoUpdateTimezone, mSystemSettings, "auto_timezone", "1", "0");
+
+ if (mDisableDialing) {
+ mSetProps.put("ro.telephony.disable-call", "true");
+ }
+
+ if (mDefaultSimData != null) {
+ mGlobalSettings.put("multi_sim_data_call", Integer.toString(mDefaultSimData));
+ }
+
+ if (mDefaultSimVoice != null) {
+ mGlobalSettings.put("multi_sim_voice_call", Integer.toString(mDefaultSimVoice));
+ }
+
+ if (mDefaultSimSms != null) {
+ mGlobalSettings.put("multi_sim_sms", Integer.toString(mDefaultSimSms));
+ }
+
+ if (mDisableAudio) {
+ mSetProps.put("ro.audio.silent", "1");
+ }
+
+ if (mSetTestHarness) {
+ // set both ro.monkey and ro.test_harness, for compatibility with older platforms
+ mSetProps.put("ro.monkey", "1");
+ mSetProps.put("ro.test_harness", "1");
+ }
+
+ if (mDisableDalvikVerifier) {
+ mSetProps.put("dalvik.vm.dexopt-flags", "v=n");
+ }
+ }
+
+ /**
+ * Change the system properties on the device.
+ *
+ * @param device The {@link ITestDevice}
+ * @throws DeviceNotAvailableException if the device is not available
+ * @throws TargetSetupError if there was a failure setting the system properties
+ */
+ private void changeSystemProps(ITestDevice device) throws DeviceNotAvailableException,
+ TargetSetupError {
+ if (mForceSkipSystemProps) {
+ CLog.d("Skipping system props due to force-skip-system-props");
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String, String> prop : mSetProps.entrySet()) {
+ if (prop.getKey().startsWith(PERSIST_PREFIX)) {
+ String command = String.format("setprop \"%s\" \"%s\"",
+ prop.getKey(), prop.getValue());
+ device.executeShellCommand(command);
+ } else {
+ sb.append(String.format("%s=%s\n", prop.getKey(), prop.getValue()));
+ }
+ }
+
+ if (sb.length() == 0) {
+ return;
+ }
+
+ boolean result = device.pushString(sb.toString(), "/data/local.prop");
+ if (!result) {
+ throw new TargetSetupError(String.format("Failed to push /data/local.prop to %s",
+ device.getSerialNumber()));
+ }
+ // Set reasonable permissions for /data/local.prop
+ device.executeShellCommand("chmod 644 /data/local.prop");
+ CLog.i("Rebooting %s due to system property change", device.getSerialNumber());
+ device.reboot();
+ }
+
+ /**
+ * Change the settings on the device.
+ * <p>
+ * Exposed so children classes may override.
+ * </p>
+ *
+ * @param device The {@link ITestDevice}
+ * @throws DeviceNotAvailableException if the device is not available
+ * @throws TargetSetupError if there was a failure setting the settings
+ */
+ public void changeSettings(ITestDevice device) throws DeviceNotAvailableException,
+ TargetSetupError {
+ if (mForceSkipSettings) {
+ CLog.d("Skipping settings due to force-skip-setttings");
+ return;
+ }
+
+ if (mSystemSettings.isEmpty() && mSecureSettings.isEmpty() && mGlobalSettings.isEmpty() &&
+ BinaryState.IGNORE.equals(mAirplaneMode)) {
+ CLog.d("No settings to change");
+ return;
+ }
+
+ if (device.getApiLevel() < 22) {
+ throw new TargetSetupError(String.format("Changing setting not supported on %s, " +
+ "must be API 22+", device.getSerialNumber()));
+ }
+
+ // Special case airplane mode since it needs to be set before other connectivity settings
+ // For example, it is possible to enable airplane mode and then turn wifi on
+ String command = "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state %s";
+ switch (mAirplaneMode) {
+ case ON:
+ CLog.d("Changing global setting airplane_mode_on to 1");
+ device.executeShellCommand("settings put global \"airplane_mode_on\" \"1\"");
+ if (!mForceSkipRunCommands) {
+ device.executeShellCommand(String.format(command, "true"));
+ }
+ break;
+ case OFF:
+ CLog.d("Changing global setting airplane_mode_on to 0");
+ device.executeShellCommand("settings put global \"airplane_mode_on\" \"0\"");
+ if (!mForceSkipRunCommands) {
+ device.executeShellCommand(String.format(command, "false"));
+ }
+ break;
+ case IGNORE:
+ // No-op
+ break;
+ }
+
+ String settingCommand = "settings put %s \"%s\" \"%s\"";
+ for (String key : mSystemSettings.keySet()) {
+ for (String value : mSystemSettings.get(key)) {
+ CLog.d("Changing system setting %s to %s", key, value);
+ device.executeShellCommand(String.format(settingCommand, "system", key, value));
+ }
+ }
+ for (String key : mSecureSettings.keySet()) {
+ for (String value : mSecureSettings.get(key)) {
+ CLog.d("Changing secure setting %s to %s", key, value);
+ device.executeShellCommand(String.format(settingCommand, "secure", key, value));
+ }
+ }
+
+ for (String key : mGlobalSettings.keySet()) {
+ for (String value : mGlobalSettings.get(key)) {
+ CLog.d("Changing global setting %s to %s", key, value);
+ device.executeShellCommand(String.format(settingCommand, "global", key, value));
+ }
+ }
+ }
+
+ /**
+ * Execute additional commands on the device.
+ *
+ * @param device The {@link ITestDevice}
+ * @param commands The list of commands to run
+ * @throws DeviceNotAvailableException if the device is not available
+ * @throws TargetSetupError if there was a failure setting the settings
+ */
+ private void runCommands(ITestDevice device, List<String> commands)
+ throws DeviceNotAvailableException, TargetSetupError {
+ if (mForceSkipRunCommands) {
+ CLog.d("Skipping run commands due to force-skip-run-commands");
+ return;
+ }
+
+ for (String command : commands) {
+ device.executeShellCommand(command);
+ }
+ }
+
+ /**
+ * Connects device to Wifi if SSID is specified.
+ *
+ * @param device The {@link ITestDevice}
+ * @throws DeviceNotAvailableException if the device is not available
+ * @throws TargetSetupError if there was a failure setting the settings
+ */
+ private void connectWifi(ITestDevice device) throws DeviceNotAvailableException,
+ TargetSetupError {
+ if (mForceSkipRunCommands) {
+ CLog.d("Skipping connect wifi due to force-skip-run-commands");
+ return;
+ }
+
+ if (mWifiSsid != null) {
+ if (!device.connectToWifiNetwork(mWifiSsid, mWifiPsk)) {
+ throw new TargetSetupError(String.format(
+ "Failed to connect to wifi network %s on %s", mWifiSsid,
+ device.getSerialNumber()));
+ }
+ }
+ }
+
+ /**
+ * Syncs a set of test data files, specified via local-data-path, to devices external storage.
+ *
+ * @param device The {@link ITestDevice}
+ * @throws DeviceNotAvailableException if the device is not available
+ * @throws TargetSetupError if data fails to sync
+ */
+ private void syncTestData(ITestDevice device) throws DeviceNotAvailableException,
+ TargetSetupError {
+ if (mLocalDataFile == null) {
+ return;
+ }
+
+ if (!mLocalDataFile.exists() || !mLocalDataFile.isDirectory()) {
+ throw new TargetSetupError(String.format(
+ "local-data-path %s is not a directory", mLocalDataFile.getAbsolutePath()));
+ }
+ String fullRemotePath = device.getIDevice().getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
+ if (fullRemotePath == null) {
+ throw new TargetSetupError(String.format(
+ "failed to get external storage path on device %s", device.getSerialNumber()));
+ }
+ if (mRemoteDataPath != null) {
+ fullRemotePath = String.format("%s/%s", fullRemotePath, mRemoteDataPath);
+ }
+ boolean result = device.syncFiles(mLocalDataFile, fullRemotePath);
+ if (!result) {
+ // TODO: get exact error code and respond accordingly
+ throw new TargetSetupError(String.format(
+ "failed to sync test data from local-data-path %s to %s on device %s",
+ mLocalDataFile.getAbsolutePath(), fullRemotePath, device.getSerialNumber()));
+ }
+ }
+
+ /**
+ * Check that device external store has the required space
+ *
+ * @param device The {@link ITestDevice}
+ * @throws DeviceNotAvailableException if the device is not available or if the device does not
+ * have the required space
+ */
+ private void checkExternalStoreSpace(ITestDevice device) throws DeviceNotAvailableException {
+ if (mMinExternalStorageKb <= 0) {
+ return;
+ }
+
+ long freeSpace = device.getExternalStoreFreeSpace();
+ if (freeSpace < mMinExternalStorageKb) {
+ throw new DeviceNotAvailableException(String.format(
+ "External store free space %dK is less than required %dK for device %s",
+ freeSpace , mMinExternalStorageKb, device.getSerialNumber()));
+ }
+ }
+
+ /**
+ * Helper method to add an ON/OFF setting to a setting map.
+ *
+ * @param state The {@link BinaryState}
+ * @param settingsMap The {@link MultiMap} used to store the settings.
+ * @param setting The setting key
+ * @param onValue The value if ON
+ * @param offValue The value if OFF
+ */
+ public static void setSettingForBinaryState(BinaryState state,
+ MultiMap<String, String> settingsMap, String setting, String onValue, String offValue) {
+ switch (state) {
+ case ON:
+ settingsMap.put(setting, onValue);
+ break;
+ case OFF:
+ settingsMap.put(setting, offValue);
+ break;
+ case IGNORE:
+ // Do nothing
+ break;
+ }
+ }
+
+ /**
+ * Helper method to add an ON/OFF run command to be executed on the device.
+ *
+ * @param state The {@link BinaryState}
+ * @param commands The list of commands to add the on or off command to.
+ * @param onCommand The command to run if ON. Ignored if the command is {@code null}
+ * @param offCommand The command to run if OFF. Ignored if the command is {@code null}
+ */
+ public static void setCommandForBinaryState(BinaryState state, List<String> commands,
+ String onCommand, String offCommand) {
+ switch (state) {
+ case ON:
+ if (onCommand != null) {
+ commands.add(onCommand);
+ }
+ break;
+ case OFF:
+ if (offCommand != null) {
+ commands.add(offCommand);
+ }
+ break;
+ case IGNORE:
+ // Do nothing
+ break;
+ }
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setAirplaneMode(BinaryState airplaneMode) {
+ mAirplaneMode = airplaneMode;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setWifi(BinaryState wifi) {
+ mWifi = wifi;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setWifiNetwork(String wifiNetwork) {
+ mWifiSsid = wifiNetwork;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setWifiWatchdog(BinaryState wifiWatchdog) {
+ mWifiWatchdog = wifiWatchdog;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setWifiScanAlwaysEnabled(BinaryState wifiScanAlwaysEnabled) {
+ mWifiScanAlwaysEnabled = wifiScanAlwaysEnabled;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setEthernet(BinaryState ethernet) {
+ mEthernet = ethernet;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setBluetooth(BinaryState bluetooth) {
+ mBluetooth = bluetooth;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setScreenAdaptiveBrightness(BinaryState screenAdaptiveBrightness) {
+ mScreenAdaptiveBrightness = screenAdaptiveBrightness;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setScreenBrightness(Integer screenBrightness) {
+ mScreenBrightness = screenBrightness;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setScreenAlwaysOn(BinaryState screenAlwaysOn) {
+ mScreenAlwaysOn = screenAlwaysOn;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setScreenTimeoutSecs(Long screenTimeoutSecs) {
+ mScreenTimeoutSecs = screenTimeoutSecs;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setScreenAmbientMode(BinaryState screenAmbientMode) {
+ mScreenAmbientMode = screenAmbientMode;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setWakeGesture(BinaryState wakeGesture) {
+ mWakeGesture = wakeGesture;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setScreenSaver(BinaryState screenSaver) {
+ mScreenSaver = screenSaver;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setNotificationLed(BinaryState notificationLed) {
+ mNotificationLed = notificationLed;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setTriggerMediaMounted(boolean triggerMediaMounted) {
+ mTriggerMediaMounted = triggerMediaMounted;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setLocationGps(BinaryState locationGps) {
+ mLocationGps = locationGps;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setLocationNetwork(BinaryState locationNetwork) {
+ mLocationNetwork = locationNetwork;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setAutoRotate(BinaryState autoRotate) {
+ mAutoRotate = autoRotate;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setBatterySaver(BinaryState batterySaver) {
+ mBatterySaver = batterySaver;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setBatterySaverTrigger(Integer batterySaverTrigger) {
+ mBatterySaverTrigger = batterySaverTrigger;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setDisableDoze(boolean disableDoze) {
+ mDisableDoze = disableDoze;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setAutoUpdateTime(BinaryState autoUpdateTime) {
+ mAutoUpdateTime = autoUpdateTime;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setAutoUpdateTimezone(BinaryState autoUpdateTimezone) {
+ mAutoUpdateTimezone = autoUpdateTimezone;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setDisableDialing(boolean disableDialing) {
+ mDisableDialing = disableDialing;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setDefaultSimData(Integer defaultSimData) {
+ mDefaultSimData = defaultSimData;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setDefaultSimVoice(Integer defaultSimVoice) {
+ mDefaultSimVoice = defaultSimVoice;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setDefaultSimSms(Integer defaultSimSms) {
+ mDefaultSimSms = defaultSimSms;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setDisableAudio(boolean disable) {
+ mDisableAudio = disable;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setTestHarness(boolean setTestHarness) {
+ mSetTestHarness = setTestHarness;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setDisableDalvikVerifier(boolean disableDalvikVerifier) {
+ mDisableDalvikVerifier = disableDalvikVerifier;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setLocalDataPath(File path) {
+ mLocalDataFile = path;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setMinExternalStorageKb(long storageKb) {
+ mMinExternalStorageKb = storageKb;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ protected void setProperty(String key, String value) {
+ mSetProps.put(key, value);
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ @Deprecated
+ protected void setDeprecatedMinExternalStoreSpace(long storeSpace) {
+ mDeprecatedMinExternalStoreSpace = storeSpace;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ @Deprecated
+ protected void setDeprecatedAudioSilent(boolean silent) {
+ mDeprecatedSetAudioSilent = silent;
+ }
+
+ /**
+ * Exposed for unit testing
+ */
+ @Deprecated
+ protected void setDeprecatedSetProp(String prop) {
+ mDeprecatedSetProps.add(prop);
}
}
diff --git a/src/com/android/tradefed/targetprep/InstallApkSetup.java b/src/com/android/tradefed/targetprep/InstallApkSetup.java
index 5edc14c..cd05599 100644
--- a/src/com/android/tradefed/targetprep/InstallApkSetup.java
+++ b/src/com/android/tradefed/targetprep/InstallApkSetup.java
@@ -23,14 +23,23 @@
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.AbiFormatter;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
/**
* A {@link ITargetPreparer} that installs one or more apks located on the filesystem.
+ * <p>
+ * This class should only be used for installing apks from the filesystem when all versions of the
+ * test rely on the apk being on the filesystem. For tests which use {@link TestAppInstallSetup}
+ * to install apks from the tests zip file, use {@code --alt-dir} to specify an alternate directory
+ * on the filesystem containing the apk for other test configurations (for example, local runs
+ * where the tests zip file is not present).
+ * </p>
*/
@OptionClass(alias = "install-apk")
public class InstallApkSetup implements ITargetPreparer {
@@ -47,6 +56,15 @@
importance = Importance.IF_UNSET)
private String mForceAbi = null;
+ @Option(name = "install-arg",
+ description = "Additional arguments to be passed to install command, "
+ + "including leading dash, e.g. \"-d\"")
+ private Collection<String> mInstallArgs = new ArrayList<>();
+
+ @Option(name = "post-install-cmd", description =
+ "optional post-install adb shell commands; can be repeated.")
+ private List<String> mPostInstallCmds = new ArrayList<>();
+
/**
* {@inheritDoc}
*/
@@ -60,18 +78,26 @@
}
Log.i(LOG_TAG, String.format("Installing %s on %s", apk.getName(),
device.getSerialNumber()));
- String[] options = {};
if (mForceAbi != null) {
String abi = AbiFormatter.getDefaultAbi(device, mForceAbi);
if (abi != null) {
- options = new String[]{String.format("--abi %s ", abi)};
+ mInstallArgs.add(String.format("--abi %s", abi));
}
}
- String result = device.installPackage(apk, true, options);
+ String result = device.installPackage(apk, true, mInstallArgs.toArray(new String[]{}));
if (result != null) {
Log.e(LOG_TAG, String.format("Failed to install %s on device %s. Reason: %s",
apk.getAbsolutePath(), device.getSerialNumber(), result));
}
}
+
+ if (mPostInstallCmds != null && !mPostInstallCmds.isEmpty()){
+ for (String cmd : mPostInstallCmds) {
+ // If the command had any output, the executeShellCommand method will log it at the
+ // VERBOSE level; so no need to do any logging from here.
+ CLog.d("About to run setup command on device %s: %s", device.getSerialNumber(), cmd);
+ device.executeShellCommand(cmd);
+ }
+ }
}
}
diff --git a/src/com/android/tradefed/targetprep/InstrumentationPreparer.java b/src/com/android/tradefed/targetprep/InstrumentationPreparer.java
index 0edeb48..e9a1960 100644
--- a/src/com/android/tradefed/targetprep/InstrumentationPreparer.java
+++ b/src/com/android/tradefed/targetprep/InstrumentationPreparer.java
@@ -17,6 +17,9 @@
package com.android.tradefed.targetprep;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
@@ -25,9 +28,6 @@
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.CollectingTestListener;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestResult.TestStatus;
-import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.testtype.InstrumentationTest;
import com.android.tradefed.util.RunUtil;
@@ -60,10 +60,23 @@
description="The test method name to run.")
private String mMethodName = null;
+ @Deprecated
@Option(name = "timeout",
- description="Aborts the test run if any test takes longer than the specified number of "
- + "milliseconds. For no timeout, set to 0.")
- private int mTimeout = 10 * 60 * 1000; // default to 10 minutes
+ description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
+ private Integer mTimeout = null;
+
+ @Option(name = "shell-timeout",
+ description="The defined timeout (in milliseconds) is used as a maximum waiting time "
+ + "when expecting the command output from the device. At any time, if the "
+ + "shell command does not output anything for a period longer than defined "
+ + "timeout the TF run terminates. For no timeout, set to 0.")
+ private long mShellTimeout = 10 * 60 * 1000; // default to 10 minutes
+
+ @Option(name = "test-timeout",
+ description="Sets timeout (in milliseconds) that will be applied to each test. In the "
+ + "event of a test timeout it will log the results and proceed with executing "
+ + "the next test. For no timeout, set to 0.")
+ private int mTestTimeout = 10 * 60 * 1000; // default to 10 minutes
@Option(name = "instrumentation-arg",
description = "Instrumentation arguments to provide.")
@@ -109,7 +122,13 @@
test.setRunnerName(mRunnerName);
test.setClassName(mClassName);
test.setMethodName(mMethodName);
- test.setTestTimeout(mTimeout);
+ if (mTimeout != null) {
+ CLog.w("\"timeout\" argument is deprecated and should not be used! \"shell-timeout\""
+ + " argument value is overwritten with %d ms", mTimeout);
+ setShellTimeout(mTimeout);
+ }
+ test.setShellTimeout(mShellTimeout);
+ test.setTestTimeout(mTestTimeout);
for (Map.Entry<String, String> entry : mInstrArgMap.entrySet()) {
test.addInstrumentationArg(entry.getKey(), entry.getValue());
}
@@ -164,8 +183,20 @@
mMethodName = methodName;
}
+ /**
+ * @Deprecated Use {@link #setShellTimeout(long)} or {@link #setTestTimeout(int)}
+ */
+ @Deprecated
void setTimeout(int timeout) {
- mTimeout = timeout;
+ setShellTimeout(timeout);
+ }
+
+ void setShellTimeout(long timeout) {
+ mShellTimeout = timeout;
+ }
+
+ void setTestTimeout(int timeout) {
+ mTestTimeout = timeout;
}
void setAttempts(int attempts) {
diff --git a/src/com/android/tradefed/targetprep/PushFilePreparer.java b/src/com/android/tradefed/targetprep/PushFilePreparer.java
index 1a9338e..7ef7ac4 100644
--- a/src/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/src/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -25,8 +25,8 @@
import com.android.tradefed.device.ITestDevice;
import java.io.File;
+import java.util.ArrayList;
import java.util.Collection;
-import java.util.LinkedList;
/**
* A {@link ITargetPreparer} that attempts to push any number of files from any host path to any
@@ -35,19 +35,19 @@
* Should be performed *after* a new build is flashed, and *after* DeviceSetup is run (if enabled)
*/
@OptionClass(alias = "push-file")
-public class PushFilePreparer implements ITargetPreparer {
+public class PushFilePreparer implements ITargetCleaner {
private static final String LOG_TAG = "PushFilePreparer";
@Option(name="push", description=
"A push-spec, formatted as '/path/to/srcfile.txt->/path/to/destfile.txt' or " +
"'/path/to/srcfile.txt->/path/to/destdir/'. May be repeated.")
- private Collection<String> mPushSpecs = new LinkedList<String>();
+ private Collection<String> mPushSpecs = new ArrayList<>();
@Option(name="post-push", description=
"A command to run on the device (with `adb shell (yourcommand)`) after all pushes " +
"have been attempted. Will not be run if a push fails with abort-on-push-failure " +
"enabled. May be repeated.")
- private Collection<String> mPostPushCommands = new LinkedList<String>();
+ private Collection<String> mPostPushCommands = new ArrayList<>();
@Option(name="abort-on-push-failure", description=
"If false, continue if pushes fail. If true, abort the Invocation on any failure.")
@@ -57,6 +57,17 @@
"After pushing files, trigger a media scan of external storage on device.")
private boolean mTriggerMediaScan = false;
+ @Option(name="cleanup", description = "Whether files pushed onto device should be cleaned up "
+ + "after test. Note that the preparer does not verify that files/directories have "
+ + "been deleted.")
+ private boolean mCleanup = false;
+
+ @Option(name="remount-system", description="Remounts system partition to be writable "
+ + "so that files could be pushed there too")
+ private boolean mRemount = false;
+
+ private Collection<String> mFilesPushed = null;
+
/**
* Set abort on failure. Exposed for testing.
*/
@@ -92,11 +103,25 @@
}
/**
+ * Resolve relative file path via {@link IBuildInfo}
+ * @param buildInfo the build artifact information
+ * @param fileName relative file path to be resolved
+ * @return
+ */
+ public File resolveRelativeFilePath(IBuildInfo buildInfo, String fileName) {
+ return buildInfo.getFile(fileName);
+ }
+
+ /**
* {@inheritDoc}
*/
@Override
public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, BuildError,
DeviceNotAvailableException {
+ mFilesPushed = new ArrayList<>();
+ if (mRemount) {
+ device.remountSystemWritable();
+ }
for (String pushspec : mPushSpecs) {
String[] pair = pushspec.split("->");
if (pair.length != 2) {
@@ -107,24 +132,28 @@
pair[1]));
File src = new File(pair[0]);
- if (!src.exists()) {
- src = buildInfo.getFile(pair[0]);
- if (src == null || !src.exists()) {
- fail(String.format("Local source file '%s' does not exist", pair[0]));
- continue;
- }
+ if (!src.isAbsolute()) {
+ src = resolveRelativeFilePath(buildInfo, pair[0]);
+ }
+ if (src == null || !src.exists()) {
+ fail(String.format("Local source file '%s' does not exist", pair[0]));
+ continue;
}
if (src.isDirectory()) {
if (!device.pushDir(src, pair[1])) {
fail(String.format("Failed to push local '%s' to remote '%s'", pair[0],
pair[1]));
continue;
+ } else {
+ mFilesPushed.add(pair[1]);
}
} else {
if (!device.pushFile(src, pair[1])) {
fail(String.format("Failed to push local '%s' to remote '%s'", pair[0],
pair[1]));
continue;
+ } else {
+ mFilesPushed.add(pair[1]);
}
}
}
@@ -140,4 +169,20 @@
device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE)));
}
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+ if (!(e instanceof DeviceNotAvailableException) && mCleanup && mFilesPushed != null) {
+ if (mRemount) {
+ device.remountSystemWritable();
+ }
+ for (String devicePath : mFilesPushed) {
+ device.executeShellCommand("rm -r " + devicePath);
+ }
+ }
+ }
}
diff --git a/src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java b/src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java
index 34acf2b..abb3c7e 100644
--- a/src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java
+++ b/src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java
@@ -36,23 +36,13 @@
private List<String> mFiles= new ArrayList<String>();
/**
- * @param device The device to remount
- * @throws DeviceNotAvailableException
- */
- private void remount(ITestDevice device) throws DeviceNotAvailableException {
- device.enableAdbRoot();
- device.executeAdbCommand("remount");
- device.waitForDeviceAvailable();
- }
-
- /**
* {@inheritDoc}
*/
@Override
public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
DeviceNotAvailableException {
- remount(device);
+ device.remountSystemWritable();
for (String file : mFiles) {
CLog.d("Removing system app %s from /system/app", file);
device.executeShellCommand(String.format("rm /system/app/%s", file));
diff --git a/src/com/android/tradefed/targetprep/SdkAvdPreparer.java b/src/com/android/tradefed/targetprep/SdkAvdPreparer.java
index 07a658c..aa6b433 100644
--- a/src/com/android/tradefed/targetprep/SdkAvdPreparer.java
+++ b/src/com/android/tradefed/targetprep/SdkAvdPreparer.java
@@ -86,6 +86,11 @@
"If unspecified, will launch generic version")
private String mDevice = null;
+ @Option(name = "display", description = "which display to launch the emulator in. " +
+ "If unspecified, display will not be set. Display values should start with :" +
+ " for example for display 1 use ':1'.")
+ private String mDisplay = null;
+
@Option(name = "abi", description = "abi to select for the avd")
private String mAbi = null;
@@ -110,6 +115,9 @@
description = "Additional argument to launch the emulator with. Can be repeated.")
private Collection<String> mEmulatorArgs = new ArrayList<String>();
+ @Option(name = "verbose", description = "Use verbose for emulator output")
+ private boolean mVerbose = false;
+
private final IRunUtil mRunUtil;
private IDeviceManager mDeviceManager;
@@ -188,6 +196,9 @@
mEmulatorBinary == null ? sdkBuild.getEmulatorToolPath() : mEmulatorBinary;
List<String> emulatorArgs = ArrayUtil.list(emulatorBinary, "-avd", avd);
+ if (mDisplay != null) {
+ emulatorArgs.add(0, "DISPLAY=" + mDisplay);
+ }
// Ensure the emulator will launch on the same port as the allocated emulator device
Integer port = EmulatorConsole.getEmulatorPort(device.getSerialNumber());
if (port == null) {
@@ -207,6 +218,11 @@
emulatorArgs.add("-gpu");
emulatorArgs.add("on");
}
+
+ if (mVerbose) {
+ emulatorArgs.add("-verbose");
+ }
+
for (Map.Entry<String, String> propEntry : mProps.entrySet()) {
emulatorArgs.add("-prop");
emulatorArgs.add(String.format("%s=%s", propEntry.getKey(), propEntry.getValue()));
diff --git a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
index 8581065..3b3ac52 100644
--- a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
@@ -15,7 +15,6 @@
*/
package com.android.tradefed.targetprep;
-import com.android.ddmlib.Log;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.IDeviceBuildInfo;
import com.android.tradefed.config.Option;
@@ -23,25 +22,34 @@
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.util.AaptParser;
import com.android.tradefed.util.AbiFormatter;
import com.android.tradefed.util.FileUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
/**
* A {@link ITargetPreparer} that installs one or more apps from a
* {@link IDeviceBuildInfo#getTestsDir()} folder onto device.
+ * <p>
+ * This preparer will look in alternate directories if the tests zip does not exist or does not
+ * contain the required apk. The search will go in order from the last alternative dir specified to
+ * the first.
+ * </p>
*/
@OptionClass(alias = "tests-zip-app")
-public class TestAppInstallSetup implements ITargetPreparer {
+public class TestAppInstallSetup implements ITargetCleaner, IAbiReceiver {
- private static final String LOG_TAG = "TestAppInstallSetup";
-
- @Option(name = "test-file-name", description =
- "the name of a test zip file to install on device. Can be repeated.",
- importance = Importance.IF_UNSET)
+ @Option(name = "test-file-name",
+ description = "the name of a test zip file to install on device. Can be repeated.",
+ importance = Importance.IF_UNSET)
private Collection<String> mTestFileNames = new ArrayList<String>();
@Option(name = AbiFormatter.FORCE_ABI_STRING,
@@ -49,6 +57,31 @@
importance = Importance.IF_UNSET)
private String mForceAbi = null;
+ @Option(name = "install-arg",
+ description = "Additional arguments to be passed to install command, "
+ + "including leading dash, e.g. \"-d\"")
+ private Collection<String> mInstallArgs = new ArrayList<>();
+
+ @Option(name = "cleanup-apks",
+ description = "Whether apks installed should be uninstalled after test. Note that the "
+ + "preparer does not verify if the apks are successfully removed.")
+ private boolean mCleanup = false;
+
+ @Option(name = "alt-dir",
+ description = "Alternate directory to look for the apk if the apk is not in the tests "
+ + "zip file. For each alternate dir, will look in //, //data/app, //DATA/app, "
+ + "and //DATA/app/apk_name/. Can be repeated. Look for apks in last alt-dir "
+ + "first.")
+ private List<File> mAltDirs = new ArrayList<>();
+
+ @Option(name = "alt-dir-behavior", description = "The order of alternate directory to be used "
+ + "when searching for apks to install")
+ private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK;
+
+ private IAbi mAbi = null;
+
+ private List<String> mPackagesInstalled = null;
+
/**
* Adds a file to the list of apks to install
*
@@ -59,52 +92,138 @@
}
/**
+ * Resolve the actual apk path based on testing artifact information inside build info.
+ *
+ * @param buildInfo build artifact information
+ * @param apkFileName filename of the apk to install
+ * @return a {@link File} representing the physical apk file on host or {@code null} if the
+ * file does not exist.
+ */
+ protected File getLocalPathForFilename(IBuildInfo buildInfo, String apkFileName)
+ throws TargetSetupError {
+ String apkBase = apkFileName.split("\\.")[0];
+
+ List<File> dirs = new ArrayList<>();
+ for (File dir : mAltDirs) {
+ dirs.add(dir);
+ // Files in tests zip file will be in DATA/app/ or DATA/app/apk_name
+ dirs.add(FileUtil.getFileForPath(dir, "DATA", "app"));
+ dirs.add(FileUtil.getFileForPath(dir, "DATA", "app", apkBase));
+ // Files in out dir will bein in uses data/app/apk_name
+ dirs.add(FileUtil.getFileForPath(dir, "data", "app", apkBase));
+ }
+ // reverse the order so ones provided via command line last can be searched first
+ Collections.reverse(dirs);
+
+ List<File> expandedTestDirs = new ArrayList<>();
+ if (buildInfo instanceof IDeviceBuildInfo) {
+ File testsDir = ((IDeviceBuildInfo)buildInfo).getTestsDir();
+ if (testsDir != null && testsDir.exists()) {
+ expandedTestDirs.add(FileUtil.getFileForPath(testsDir, "DATA", "app"));
+ expandedTestDirs.add(FileUtil.getFileForPath(testsDir, "DATA", "app", apkBase));
+ }
+ }
+ if (mAltDirBehavior == AltDirBehavior.FALLBACK) {
+ // alt dirs are appended after build artifact dirs
+ expandedTestDirs.addAll(dirs);
+ dirs = expandedTestDirs;
+ } else if (mAltDirBehavior == AltDirBehavior.OVERRIDE) {
+ dirs.addAll(expandedTestDirs);
+ } else {
+ throw new TargetSetupError("Missing handler for alt-dir-behavior: " + mAltDirBehavior);
+ }
+ if (dirs.isEmpty()) {
+ throw new TargetSetupError(
+ "Provided buildInfo does not contain a valid tests directory and no " +
+ "alternative directories were provided");
+ }
+
+ for (File dir : dirs) {
+ File testAppFile = new File(dir, apkFileName);
+ if (testAppFile.exists()) {
+ return testAppFile;
+ }
+ }
+ return null;
+ }
+
+ /**
* {@inheritDoc}
*/
@Override
public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
DeviceNotAvailableException {
- if (!(buildInfo instanceof IDeviceBuildInfo)) {
- throw new IllegalArgumentException(String.format("Provided buildInfo is not a %s",
- IDeviceBuildInfo.class.getCanonicalName()));
- }
if (mTestFileNames.size() == 0) {
- Log.i(LOG_TAG, "No test apps to install, skipping");
+ CLog.i("No test apps to install, skipping");
return;
}
- File testsDir = ((IDeviceBuildInfo)buildInfo).getTestsDir();
- if (testsDir == null || !testsDir.exists()) {
- throw new TargetSetupError(
- "Provided buildInfo does not contain a valid tests directory");
+ if (mCleanup) {
+ mPackagesInstalled = new ArrayList<>();
}
-
for (String testAppName : mTestFileNames) {
- File testAppFile = FileUtil.getFileForPath(testsDir, "DATA", "app", testAppName);
- if (!testAppFile.exists()) {
- // in addition to /data/app/TestApp.apk
- // also check path like /data/app/TestApp/TestApp.apk
- String[] fields = testAppName.split("\\.");
- testAppFile = FileUtil.getFileForPath(
- testsDir, "DATA", "app", fields[0], testAppName);
- }
- if (!testAppFile.exists()) {
+ File testAppFile = getLocalPathForFilename(buildInfo, testAppName);
+ if (testAppFile == null) {
throw new TargetSetupError(
String.format("Could not find test app %s directory in extracted tests.zip",
- testAppFile));
+ testAppName));
}
- String[] options = {};
- if (mForceAbi != null) {
- String abi = AbiFormatter.getDefaultAbi(device, mForceAbi);
- if (abi != null) {
- options = new String[]{String.format("--abi %s ", abi)};
- }
+ // resolve abi flags
+ if (mAbi != null && mForceAbi != null) {
+ throw new IllegalStateException("cannot specify both abi flags");
}
- String result = device.installPackage(testAppFile, true, options);
+ String abiName = null;
+ if (mAbi != null) {
+ abiName = mAbi.getName();
+ } else if (mForceAbi != null) {
+ abiName = AbiFormatter.getDefaultAbi(device, mForceAbi);
+ }
+ if (abiName != null) {
+ mInstallArgs.add(String.format("--abi %s", abiName));
+ }
+ CLog.d("Installing apk from %s ...", testAppFile.getAbsolutePath());
+ String result = device.installPackage(testAppFile, true,
+ mInstallArgs.toArray(new String[]{}));
if (result != null) {
throw new TargetSetupError(
String.format("Failed to install %s on %s. Reason: '%s'", testAppName,
device.getSerialNumber(), result));
}
+ if (mCleanup) {
+ AaptParser parser = AaptParser.parse(testAppFile);
+ if (parser == null) {
+ throw new TargetSetupError("apk installed but AaptParser failed");
+ }
+ mPackagesInstalled.add(parser.getPackageName());
+ }
}
}
+
+ @Override
+ public void setAbi(IAbi abi) {
+ mAbi = abi;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+ if (mCleanup && mPackagesInstalled != null && !(e instanceof DeviceNotAvailableException)) {
+ for (String packageName : mPackagesInstalled) {
+ String msg = device.uninstallPackage(packageName);
+ if (msg != null) {
+ CLog.w(String.format("error uninstalling package '%s': %s",
+ packageName, msg));
+ }
+ }
+ }
+ }
+
+ /**
+ * Set an alternate directory.
+ */
+ public void setAltDir(File altDir) {
+ mAltDirs.add(altDir);
+ }
}
diff --git a/src/com/android/tradefed/targetprep/TestFilePushSetup.java b/src/com/android/tradefed/targetprep/TestFilePushSetup.java
index 234b748..cbf59ed 100644
--- a/src/com/android/tradefed/targetprep/TestFilePushSetup.java
+++ b/src/com/android/tradefed/targetprep/TestFilePushSetup.java
@@ -30,11 +30,17 @@
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
/**
* A {@link ITargetPreparer} that pushes one or more files/dirs from a
* {@link IDeviceBuildInfo#getTestsDir()} folder onto device.
- *
+ * <p>
+ * This preparer will look in alternate directories if the tests zip does not exist or does not
+ * contain the required apk. The search will go in order from the last alternative dir specified to
+ * the first.
+ * </p>
*/
@OptionClass(alias = "tests-zip-file")
public class TestFilePushSetup implements ITargetPreparer {
@@ -48,6 +54,16 @@
"Throw exception if the specified file is not found.")
private boolean mThrowIfNoFile = true;
+ @Option(name = "alt-dir",
+ description = "Alternate directory to look for the apk if the apk is not in the tests "
+ + "zip file. For each alternate dir, will look in // and //DATA. Can be "
+ + "repeated. Look for apks in last alt-dir first.")
+ private List<File> mAltDirs = new ArrayList<>();
+
+ @Option(name = "alt-dir-behavior", description = "The order of alternate directory to be used "
+ + "when searching for files to push")
+ private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK;
+
/**
* Adds a file to the list of items to push
*
@@ -60,6 +76,54 @@
}
/**
+ * Resolve the host side path based on testing artifact information inside build info.
+ *
+ * @param buildInfo build artifact information
+ * @param fileName filename of artifacts to push
+ * @return a {@link File} representing the physical file/path on host
+ */
+ protected File getLocalPathForFilename(IBuildInfo buildInfo, String fileName)
+ throws TargetSetupError {
+ List<File> dirs = new ArrayList<>();
+ for (File dir : mAltDirs) {
+ dirs.add(dir);
+ dirs.add(FileUtil.getFileForPath(dir, "DATA"));
+ }
+ // reverse the order so ones provided via command line last can be searched first
+ Collections.reverse(dirs);
+
+ List<File> expandedTestDirs = new ArrayList<>();
+ if (buildInfo instanceof IDeviceBuildInfo) {
+ File testsDir = ((IDeviceBuildInfo)buildInfo).getTestsDir();
+ if (testsDir != null && testsDir.exists()) {
+ expandedTestDirs.add(FileUtil.getFileForPath(testsDir, "DATA"));
+ }
+ }
+ if (mAltDirBehavior == AltDirBehavior.FALLBACK) {
+ // alt dirs are appended after build artifact dirs
+ expandedTestDirs.addAll(dirs);
+ dirs = expandedTestDirs;
+ } else if (mAltDirBehavior == AltDirBehavior.OVERRIDE) {
+ dirs.addAll(expandedTestDirs);
+ } else {
+ throw new TargetSetupError("Missing handler for alt-dir-behavior: " + mAltDirBehavior);
+ }
+ if (dirs.isEmpty()) {
+ throw new TargetSetupError(
+ "Provided buildInfo does not contain a valid tests directory and no " +
+ "alternative directories were provided");
+ }
+
+ for (File dir : dirs) {
+ File testAppFile = new File(dir, fileName);
+ if (testAppFile.exists()) {
+ return testAppFile;
+ }
+ }
+ return null;
+ }
+
+ /**
* {@inheritDoc}
*/
@Override
@@ -73,15 +137,10 @@
CLog.d("No test files to push, skipping");
return;
}
- File testsDir = ((IDeviceBuildInfo)buildInfo).getTestsDir();
- if (testsDir == null || !testsDir.exists()) {
- throw new TargetSetupError(
- "Provided buildInfo does not contain a valid tests directory");
- }
int filePushed = 0;
for (String fileName : mTestPaths) {
- File localFile = FileUtil.getFileForPath(testsDir, "DATA", fileName);
- if (!localFile.exists()) {
+ File localFile = getLocalPathForFilename(buildInfo, fileName);
+ if (localFile == null) {
if (mThrowIfNoFile) {
throw new TargetSetupError(String.format(
"Could not find test file %s directory in extracted tests.zip",
@@ -90,15 +149,15 @@
continue;
}
}
- fileName = getDevicePathFromUserData(fileName);
- CLog.d("Pushing file: %s -> %s", localFile.getAbsoluteFile(), fileName);
+ String remoteFileName = getDevicePathFromUserData(fileName);
+ CLog.d("Pushing file: %s -> %s", localFile.getAbsoluteFile(), remoteFileName);
if (localFile.isDirectory()) {
- device.pushDir(localFile, fileName);
+ device.pushDir(localFile, remoteFileName);
} else if (localFile.isFile()) {
- device.pushFile(localFile, fileName);
+ device.pushFile(localFile, remoteFileName);
}
// there's no recursive option for 'chown', best we can do here
- device.executeShellCommand(String.format("chown system.system %s", fileName));
+ device.executeShellCommand(String.format("chown system.system %s", remoteFileName));
filePushed++;
}
if (filePushed == 0) {
@@ -106,6 +165,21 @@
}
}
+ /**
+ * Set an alternate directory.
+ */
+ public void setAltDir(File altDir) {
+ mAltDirs.add(altDir);
+ }
+
+ /**
+ * Set the alternative directory search beahvior
+ * @param behavior
+ */
+ public void setAltDirBehavior(AltDirBehavior behavior) {
+ mAltDirBehavior = behavior;
+ }
+
static String getDevicePathFromUserData(String path) {
return ArrayUtil.join(FileListingService.FILE_SEPARATOR,
"", FileListingService.DIRECTORY_DATA, path);
diff --git a/src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java
index 3acfd93..33cc62e 100644
--- a/src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java
@@ -73,9 +73,8 @@
throw new TargetSetupError(
"Provided buildInfo does not contain a valid tests directory");
}
- device.enableAdbRoot();
+ device.remountSystemWritable();
device.setRecoveryMode(RecoveryMode.ONLINE);
- device.executeAdbCommand("remount");
device.executeShellCommand("stop");
for (String testAppName : mTestFileNames) {
diff --git a/src/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java b/src/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
new file mode 100644
index 0000000..efb8d13
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+
+/**
+ * A {@link ITargetPreparer} that waits for datetime to be set on device
+ * <p>
+ * Optionally this preparer can force a {@link TargetSetupError} if datetime is not set within
+ * timeout, or force host datetime onto device,
+ */
+@OptionClass(alias = "wait-for-datetime")
+public class WaitForDeviceDatetimePreparer implements ITargetPreparer {
+
+ // 30s to wait for device datetime
+ private static final long DATETIME_WAIT_TIMEOUT = 30 * 1000;
+ // poll every 5s when waiting correct device datetime
+ private static final long DATETIME_CHECK_INTERVAL = 5 * 1000;
+ // allow 10s of margin for datetime difference between host/device
+ private static final long DATETIME_MARGIN = 10;
+
+ @Option(name = "force-datetime", description = "Force sync host datetime to device if device "
+ + "fails to set datetime automatically.")
+ private boolean mForceDatetime = false;
+
+ @Option(name = "datetime-wait-timeout",
+ description = "Timeout in ms to wait for correct datetime on device.")
+ private long mDatetimeWaitTimeout = DATETIME_WAIT_TIMEOUT;
+
+ @Option(name = "force-setup-error",
+ description = "Throw an TargetSetupError if correct datetime was not set. "
+ + "Only meaningful if \"force-datetime\" is not used.")
+ private boolean mForceSetupError = false;
+
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ BuildError, DeviceNotAvailableException {
+ if (!waitForDeviceDatetime(device, mForceDatetime)) {
+ if (mForceSetupError) {
+ throw new TargetSetupError("datetime on device is incorrect after wait timeout");
+ } else {
+ CLog.w("datetime on device is incorrect after wait timeout.");
+ }
+ }
+ }
+
+ /**
+ * Sets the timeout for waiting on valid device datetime
+ */
+ public void setDatetimeWaitTimeout(long datetimeWaitTimeout) {
+ mDatetimeWaitTimeout = datetimeWaitTimeout;
+ }
+
+ /**
+ * Sets the if datetime should be forced from host to device
+ */
+ public void setForceDatetime(boolean forceDatetime) {
+ mForceDatetime = forceDatetime;
+ }
+
+ /**
+ * Waits for a correct datetime on device, optionally force host datetime onto device
+ * @param forceDatetime
+ * @return <code>true</code> if datetime is correct or forced, <code>false</code> otherwise
+ */
+ boolean waitForDeviceDatetime(ITestDevice device, boolean forceDatetime)
+ throws DeviceNotAvailableException {
+ return waitForDeviceDatetime(device, forceDatetime,
+ mDatetimeWaitTimeout, DATETIME_CHECK_INTERVAL);
+ }
+
+ /**
+ * Waits for a correct datetime on device, optionally force host datetime onto device
+ * @param forceDatetime
+ * @param datetimeWaitTimeout
+ * @param datetimeCheckInterval
+ * @return <code>true</code> if datetime is correct or forced, <code>false</code> otherwise
+ */
+ boolean waitForDeviceDatetime(ITestDevice device, boolean forceDatetime,
+ long datetimeWaitTimeout, long datetimeCheckInterval)
+ throws DeviceNotAvailableException {
+ long start = System.currentTimeMillis();
+ while ((System.currentTimeMillis() - start) < datetimeWaitTimeout) {
+ long datetime = getDeviceDatetimeEpoch(device);
+ long now = System.currentTimeMillis() / 1000;
+ if (datetime == -1) {
+ if (forceDatetime) {
+ throw new UnsupportedOperationException(
+ "unexpected return from \"date\" command on device");
+ } else {
+ return false;
+ }
+ }
+ if ((Math.abs(now - datetime) < DATETIME_MARGIN)) {
+ return true;
+ }
+ getRunUtil().sleep(datetimeCheckInterval);
+ }
+ if (forceDatetime) {
+ device.setDate(null);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Retrieve device datetime in epoch format
+ * @param device
+ * @return datetime on device in epoch format, -1 if failed
+ */
+ long getDeviceDatetimeEpoch(ITestDevice device) throws DeviceNotAvailableException {
+ String datetime = device.executeShellCommand("date '+%s'").trim();
+ try {
+ return Long.parseLong(datetime);
+ } catch (NumberFormatException nfe) {
+ CLog.v("returned datetime from device is not a number: '%s'", datetime);
+ return -1;
+ }
+ }
+
+ /**
+ * @return the {@link IRunUtil} to use
+ */
+ protected IRunUtil getRunUtil() {
+ return RunUtil.getDefault();
+ }
+}
diff --git a/src/com/android/tradefed/targetprep/WifiPreparer.java b/src/com/android/tradefed/targetprep/WifiPreparer.java
index 99edcd8..1f576c2 100644
--- a/src/com/android/tradefed/targetprep/WifiPreparer.java
+++ b/src/com/android/tradefed/targetprep/WifiPreparer.java
@@ -80,6 +80,11 @@
return;
}
+ if (e instanceof DeviceFailedToBootError) {
+ CLog.d("boot failure: skipping wifi teardown");
+ return;
+ }
+
if (mMonitorNetwork) {
device.disableNetworkMonitor();
}
diff --git a/src/com/android/tradefed/testtype/CodeCoverageTest.java b/src/com/android/tradefed/testtype/CodeCoverageTest.java
index 1736995..6547b91 100644
--- a/src/com/android/tradefed/testtype/CodeCoverageTest.java
+++ b/src/com/android/tradefed/testtype/CodeCoverageTest.java
@@ -16,6 +16,7 @@
package com.android.tradefed.testtype;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
@@ -26,7 +27,6 @@
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.ResultForwarder;
-import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.RunUtil;
diff --git a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
index 15018ba..3917a75 100644
--- a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
+++ b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
@@ -71,6 +71,9 @@
"draining processes and allow the device to charge at its fastest rate.")
private boolean mRebootChargeDevices = false;
+ @Option(name = "stop-runtime", description = "Whether to stop runtime.")
+ private boolean mStopRuntime = false;
+
Integer checkBatteryLevel(ITestDevice device) throws DeviceNotAvailableException {
try {
IDevice idevice = device.getIDevice();
@@ -81,20 +84,8 @@
}
}
- private void turnScreenOffOrStopRuntime(ITestDevice device) throws DeviceNotAvailableException {
- String output = getDevice().executeShellCommand("pm path android");
- if (output == null || !output.contains("package:")) {
- CLog.d("framework does not seem to be running, trying to stop it.");
- // stop framework in case it's running some sort of runtime restart loop, and we can
- // still charge the device
- getDevice().executeShellCommand("stop");
- } else {
- output = getDevice().executeShellCommand("dumpsys power");
- if (output.contains("mScreenOn=true")) {
- // KEYCODE_POWER = 26
- getDevice().executeShellCommand("input keyevent 26");
- }
- }
+ private void stopRuntime(ITestDevice device) throws DeviceNotAvailableException {
+ getDevice().executeShellCommand("stop");
}
/**
@@ -128,7 +119,9 @@
mTestDevice.reboot();
}
- turnScreenOffOrStopRuntime(mTestDevice);
+ if (mStopRuntime) {
+ stopRuntime(mTestDevice);
+ }
// If we're down here, it's time to hold the device until it reaches mResumeLevel
Long lastReportTime = System.currentTimeMillis();
diff --git a/src/com/android/tradefed/testtype/FakeTest.java b/src/com/android/tradefed/testtype/FakeTest.java
index c6ea57a..a3cc8f3 100644
--- a/src/com/android/tradefed/testtype/FakeTest.java
+++ b/src/com/android/tradefed/testtype/FakeTest.java
@@ -16,7 +16,6 @@
package com.android.tradefed.testtype;
import com.android.ddmlib.testrunner.ITestRunListener;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
@@ -40,7 +39,7 @@
@Option(name = "run", description = "Specify a new run to include. " +
"The key should be the unique name of the TestRun (which may be a Java class name). " +
"The value should specify the sequence of test results, using the characters P[ass], " +
- "F[ail], or E[rror]. You may use run-length encoding to specify repeats, and you " +
+ "or F[ail]. You may use run-length encoding to specify repeats, and you " +
"may use parentheses for grouping. So \"(PF)4\" and \"((PF)2)2\" will both expand " +
"to \"PFPFPFPF\".", importance = Importance.IF_UNSET)
private Map<String, String> mRuns = new LinkedHashMap<String, String>();
@@ -172,7 +171,7 @@
listener.testRunStarted(runName, spec.length());
int i = 0;
for (char c : spec.toCharArray()) {
- if (c != 'P' && c != 'F' && c != 'E') {
+ if (c != 'P' && c != 'F') {
throw new IllegalArgumentException(String.format(
"Received unexpected test spec character '%c' in spec \"%s\"", c, spec));
}
@@ -187,13 +186,9 @@
// no-op
break;
case 'F':
- listener.testFailed(TestFailure.FAILURE, test,
+ listener.testFailed(test,
String.format("Test %s had a predictable boo-boo.", testName));
break;
- case 'E':
- listener.testFailed(TestFailure.ERROR, test,
- String.format("Test %s had an unexpected boo-boo. Uh-oh...", testName));
- break;
}
listener.testEnded(test, EMPTY_MAP);
}
diff --git a/src/com/android/tradefed/testtype/GTestResultParser.java b/src/com/android/tradefed/testtype/GTestResultParser.java
index 2856240..0bd06e8 100644
--- a/src/com/android/tradefed/testtype/GTestResultParser.java
+++ b/src/com/android/tradefed/testtype/GTestResultParser.java
@@ -539,16 +539,14 @@
// If the test name of the result changed from what we started with, report that
// the last known test failed, regardless of whether we received a pass or fail tag.
for (ITestRunListener listener : mTestListeners) {
- listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
- mCurrentTestResult.getTrace());
+ listener.testFailed(testId, mCurrentTestResult.getTrace());
}
// Report error as failure.
++mTotalNumberOfTestFailed;
}
else if (!testPassed) { // test failed
for (ITestRunListener listener : mTestListeners) {
- listener.testFailed(ITestRunListener.TestFailure.FAILURE, testId,
- mCurrentTestResult.getTrace());
+ listener.testFailed(testId, mCurrentTestResult.getTrace());
}
++mTotalNumberOfTestFailed;
@@ -624,7 +622,7 @@
testRunStackTrace = mCurrentTestResult.getTrace();
}
for (ITestRunListener listener : mTestListeners) {
- listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
+ listener.testFailed(testId,
"No test results.\r\n" + testRunStackTrace);
listener.testEnded(testId, emptyMap);
}
diff --git a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
index eec9c89..3209e54 100644
--- a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
+++ b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
@@ -30,6 +30,8 @@
import com.android.tradefed.testtype.testdefs.XmlDefsTest;
import com.android.tradefed.util.AbiFormatter;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -41,7 +43,7 @@
* Runs all instrumentation found on current device.
*/
@OptionClass(alias = "installed-instrumentation")
-public class InstalledInstrumentationsTest implements IDeviceTest, IResumableTest {
+public class InstalledInstrumentationsTest implements IDeviceTest, IResumableTest, IShardableTest {
/** the metric key name for the test coverage target value */
// TODO: move this to a more generic location
@@ -51,10 +53,23 @@
private ITestDevice mDevice;
+ @Deprecated
@Option(name = "timeout",
- description = "Fail any test that takes longer than the specified number of "
- + "milliseconds.")
- private int mTestTimeout = 10 * 60 * 1000; // default to 10 minutes
+ description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
+ private Integer mTimeout = null;
+
+ @Option(name = "shell-timeout",
+ description="The defined timeout (in milliseconds) is used as a maximum waiting time "
+ + "when expecting the command output from the device. At any time, if the "
+ + "shell command does not output anything for a period longer than defined "
+ + "timeout the TF run terminates. For no timeout, set to 0.")
+ private long mShellTimeout = 10 * 60 * 1000; // default to 10 minutes
+
+ @Option(name = "test-timeout",
+ description="Sets timeout (in milliseconds) that will be applied to each test. In the "
+ + "event of a test timeout it will log the results and proceed with executing "
+ + "the next test. For no timeout, set to 0.")
+ private int mTestTimeout = 5 * 60 * 1000; // default to 5 minutes
@Option(name = "size",
description = "Restrict tests to a specific test size. " +
@@ -117,6 +132,13 @@
"each remaining test")
private boolean mReRunUsingTestFile = false;
+ @Option(name = "shards", description =
+ "Split test run into this many parallel shards")
+ private int mShards = 0;
+
+ private int mTotalShards = 0;
+ private int mShardIndex = 0;
+
private List<InstrumentationTest> mTests = null;
@Option(name = AbiFormatter.FORCE_ABI_STRING,
@@ -228,6 +250,10 @@
listener.testRunEnded(0, coverageMetric);
}
+ long getShellTimeout() {
+ return mShellTimeout;
+ }
+
int getTestTimeout() {
return mTestTimeout;
}
@@ -292,13 +318,19 @@
if (mRunnerFilter == null || mRunnerFilter.equals(runner)) {
InstrumentationTest t = createInstrumentationTest();
try {
+ // Copies all current argument values to the new runner that will be
+ // used to actually run the tests.
OptionCopier.copyOptions(InstalledInstrumentationsTest.this, t);
} catch (ConfigurationException e) {
- CLog.e("failed to copy instrumentation options", e);
+ CLog.e("failed to copy instrumentation options: %s", e.getMessage());
}
t.setPackageName(m.group(1));
t.setRunnerName(runner);
t.setCoverageTarget(m.group(3));
+ if (mTotalShards > 0) {
+ t.addInstrumentationArg("shardIndex", Integer.toString(mShardIndex));
+ t.addInstrumentationArg("numShards", Integer.toString(mTotalShards));
+ }
mTests.add(t);
}
}
@@ -309,4 +341,28 @@
return mTests;
}
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Collection<IRemoteTest> split() {
+ if (mShards > 1) {
+ Collection<IRemoteTest> shards = new ArrayList<>(mShards);
+ for (int index = 0; index < mShards; index++) {
+ InstalledInstrumentationsTest shard = new InstalledInstrumentationsTest();
+ try {
+ OptionCopier.copyOptions(this, shard);
+ } catch (ConfigurationException e) {
+ CLog.e("failed to copy instrumentation options: %s", e.getMessage());
+ }
+ shard.mShards = 0;
+ shard.mShardIndex = index;
+ shard.mTotalShards = mShards;
+ shards.add(shard);
+ }
+ return shards;
+ }
+ return null;
+ }
}
diff --git a/src/com/android/tradefed/testtype/InstrumentationFileTest.java b/src/com/android/tradefed/testtype/InstrumentationFileTest.java
index e8dcaf2..71062cb 100644
--- a/src/com/android/tradefed/testtype/InstrumentationFileTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationFileTest.java
@@ -100,9 +100,10 @@
*/
private void writeTestsToFileAndRun(Collection<TestIdentifier> tests,
final ITestInvocationListener listener) throws DeviceNotAvailableException {
+ File testFile = null;
try {
// create and populate test file
- File testFile = FileUtil.createTempFile(
+ testFile = FileUtil.createTempFile(
"tf_testFile_" + InstrumentationFileTest.class.getCanonicalName(), ".txt");
try (BufferedWriter bw = new BufferedWriter(new FileWriter(testFile))) {
for (TestIdentifier testToRun : tests) {
@@ -123,8 +124,11 @@
reRunTestsSerially(mInstrumentationTest, listener);
}
} catch (IOException e) {
- CLog.e("Failed to run tests from file, re-running tests serially", e);
+ CLog.e("Failed to run tests from file, re-running tests serially: %s", e.getMessage());
reRunTestsSerially(mInstrumentationTest, listener);
+ } finally {
+ // clean up test file, if it was created
+ FileUtil.deleteFile(testFile);
}
}
diff --git a/src/com/android/tradefed/testtype/InstrumentationSerialTest.java b/src/com/android/tradefed/testtype/InstrumentationSerialTest.java
index 4d5bea4..ea5d913 100644
--- a/src/com/android/tradefed/testtype/InstrumentationSerialTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationSerialTest.java
@@ -20,14 +20,12 @@
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.ResultForwarder;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.Map;
/**
@@ -99,7 +97,7 @@
runTest(runner, listener, testToRun);
}
} catch (ConfigurationException e) {
- CLog.e("Failed to create new InstrumentationTest", e);
+ CLog.e("Failed to create new InstrumentationTest: %s", e.getMessage());
}
}
@@ -164,7 +162,7 @@
public void markTestAsFailed() {
super.testRunStarted(mRunName, 1);
super.testStarted(mExpectedTest);
- super.testFailed(TestFailure.ERROR, mExpectedTest, String.format(
+ super.testFailed(mExpectedTest, String.format(
"Test failed to run. Test run failed due to : %s", mRunErrorMsg));
if (mRunErrorMsg != null) {
super.testRunFailed(mRunErrorMsg);
diff --git a/src/com/android/tradefed/testtype/InstrumentationTest.java b/src/com/android/tradefed/testtype/InstrumentationTest.java
index 567e2d0..b1612a1 100644
--- a/src/com/android/tradefed/testtype/InstrumentationTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationTest.java
@@ -22,11 +22,11 @@
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner.TestSize;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
@@ -36,7 +36,6 @@
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.ResultForwarder;
-import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.util.AbiFormatter;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StringEscapeUtils;
@@ -63,6 +62,8 @@
private static final String TEST_FILE_INST_ARGS_KEY = "testFile";
static final String DELAY_MSEC_ARG = "delay_msec";
+ /** instrumentation test runner argument key used for individual test timeout */
+ static final String TEST_TIMEOUT_INST_ARGS_KEY = "timeout_msec";
@Option(name = "package", shortName = 'p',
description="The manifest package name of the Android test application to run.",
@@ -86,10 +87,23 @@
"Will be ignored if --class is set.")
private String mTestPackageName = null;
+ @Deprecated
@Option(name = "timeout",
- description="Aborts the test run if any test takes longer than the specified number of "
- + "milliseconds. For no timeout, set to 0.")
- private int mTestTimeout = 10 * 60 * 1000; // default to 10 minutes
+ description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
+ private Integer mTimeout = null;
+
+ @Option(name = "shell-timeout",
+ description="The defined timeout (in milliseconds) is used as a maximum waiting time "
+ + "when expecting the command output from the device. At any time, if the "
+ + "shell command does not output anything for a period longer than defined "
+ + "timeout the TF run terminates. For no timeout, set to 0.")
+ private long mShellTimeout = 10 * 60 * 1000; // default to 10 minutes
+
+ @Option(name = "test-timeout",
+ description="Sets timeout (in milliseconds) that will be applied to each test. In the "
+ + "event of a test timeout it will log the results and proceed with executing "
+ + "the next test. For no timeout, set to 0.")
+ private int mTestTimeout = 5 * 60 * 1000; // default to 5 minutes
@Option(name = "size",
description="Restrict test to a specific test size.")
@@ -311,7 +325,14 @@
}
/**
- * Optionally, set the maximum time for each test.
+ * Optionally, set the maximum time (in milliseconds) expecting shell output from the device.
+ */
+ public void setShellTimeout(long timeout) {
+ mShellTimeout = timeout;
+ }
+
+ /**
+ * Optionally, set the maximum time (in milliseconds) for each individual test run.
*/
public void setTestTimeout(int timeout) {
mTestTimeout = timeout;
@@ -369,6 +390,13 @@
}
/**
+ * Get the shell timeout in ms.
+ */
+ long getShellTimeout() {
+ return mShellTimeout;
+ }
+
+ /**
* Get the test timeout in ms.
*/
int getTestTimeout() {
@@ -508,7 +536,7 @@
if (mTestSize != null) {
mRunner.setTestSize(TestSize.getTestSize(mTestSize));
}
- mRunner.setMaxTimeToOutputResponse(mTestTimeout, TimeUnit.MILLISECONDS);
+ addTimeoutsToRunner(mRunner);
if (mRunName != null) {
mRunner.setRunName(mRunName);
}
@@ -526,6 +554,29 @@
}
/**
+ * Helper method to add test-timeout & shell-timeout timeouts to given runner
+ */
+ private void addTimeoutsToRunner(IRemoteAndroidTestRunner runner) {
+ if (mTimeout != null) {
+ CLog.w("\"timeout\" argument is deprecated and should not be used! \"shell-timeout\""
+ + " argument value is overwritten with %d ms", mTimeout);
+ setShellTimeout(mTimeout);
+ }
+ if (mTestTimeout < 0) {
+ throw new IllegalArgumentException(
+ String.format("test-timeout %d cannot be negative", mTestTimeout));
+ }
+ if (mShellTimeout < mTestTimeout) {
+ CLog.w(String.format("shell-timeout %d cannot be smaller then test-timeout %d; "
+ + "NOTE: extending shell-timeout to match test-timeout %d, please "
+ + "consider fixing this!", mShellTimeout, mTestTimeout, mTestTimeout));
+ mShellTimeout = mTestTimeout;
+ }
+ runner.setMaxTimeToOutputResponse(mShellTimeout, TimeUnit.MILLISECONDS);
+ addInstrumentationArg(TEST_TIMEOUT_INST_ARGS_KEY, Long.toString(mTestTimeout));
+ }
+
+ /**
* Execute test run.
*
* @param listener the test result listener
@@ -625,7 +676,7 @@
calculateRemainingTests(mRemainingTests, testTracker);
}
} catch (ConfigurationException e) {
- CLog.e("Failed to create InstrumentationFileTest", e);
+ CLog.e("Failed to create InstrumentationFileTest: %s", e.getMessage());
}
}
@@ -649,7 +700,7 @@
calculateRemainingTests(mRemainingTests, testTracker);
}
} catch (ConfigurationException e) {
- CLog.e("Failed to create InstrumentationSerialTest", e);
+ CLog.e("Failed to create InstrumentationSerialTest: %s", e.getMessage());
}
}
@@ -680,20 +731,13 @@
if (isRerunMode()) {
Log.d(LOG_TAG, String.format("Collecting test info for %s on device %s",
mPackageName, mDevice.getSerialNumber()));
- runner.setLogOnly(true);
- // the collecting test command can fail for large volumes of test bug 1750602. insert a
- // small delay between each test to prevent this
- if (mTestDelay > 0) {
- runner.addInstrumentationArg(DELAY_MSEC_ARG, Integer.toString(mTestDelay));
- }
- // use a shorter timeout when collecting tests
- runner.setMaxTimeToOutputResponse(mCollectTestsShellTimeout, TimeUnit.MILLISECONDS);
+ runner.setTestCollection(true);
// try to collect tests multiple times, in case device is temporarily not available
// on first attempt
Collection<TestIdentifier> tests = collectTestsAndRetry(runner);
- runner.setLogOnly(false);
- runner.setMaxTimeToOutputResponse(mTestTimeout, TimeUnit.MILLISECONDS);
- runner.removeInstrumentationArg(DELAY_MSEC_ARG);
+ // done with "logOnly" mode, restore proper test timeout before real test execution
+ addTimeoutsToRunner(runner);
+ runner.setTestCollection(false);
return tests;
}
return null;
@@ -759,8 +803,8 @@
}
@Override
- public void testFailed(TestFailure status, TestIdentifier test, String trace) {
- super.testFailed(status, test, trace);
+ public void testFailed(TestIdentifier test, String trace) {
+ super.testFailed(test, trace);
try {
InputStreamSource screenSource = mDevice.getScreenshot();
@@ -790,8 +834,18 @@
}
@Override
- public void testFailed(TestFailure status, TestIdentifier test, String trace) {
- super.testFailed(status, test, trace);
+ public void testFailed(TestIdentifier test, String trace) {
+ super.testFailed(test, trace);
+ captureLog(test);
+ }
+
+ @Override
+ public void testAssumptionFailure(TestIdentifier test, String trace) {
+ super.testAssumptionFailure(test, trace);
+ captureLog(test);
+ }
+
+ private void captureLog(TestIdentifier test) {
// sleep a small amount of time to ensure test failure stack trace makes it into logcat
// capture
RunUtil.getDefault().sleep(10);
diff --git a/src/com/android/tradefed/testtype/UiAutomatorRunner.java b/src/com/android/tradefed/testtype/UiAutomatorRunner.java
index 5137a02..f648eb9 100644
--- a/src/com/android/tradefed/testtype/UiAutomatorRunner.java
+++ b/src/com/android/tradefed/testtype/UiAutomatorRunner.java
@@ -254,6 +254,11 @@
throw new UnsupportedOperationException("coverage mode is not supported");
}
+ @Override
+ public void setTestCollection(boolean b) {
+ throw new UnsupportedOperationException("Test Collection mode is not supported");
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/com/android/tradefed/testtype/UiAutomatorTest.java b/src/com/android/tradefed/testtype/UiAutomatorTest.java
index 2d3c10c..ae38299 100644
--- a/src/com/android/tradefed/testtype/UiAutomatorTest.java
+++ b/src/com/android/tradefed/testtype/UiAutomatorTest.java
@@ -210,7 +210,7 @@
if (mJarPaths.isEmpty()) {
String rawFileString =
getDevice().executeShellCommand(String.format("ls %s", SHELL_EXE_BASE));
- String[] rawFiles = rawFileString.split("\r\n");
+ String[] rawFiles = rawFileString.split("\r?\n");
for (String rawFile : rawFiles) {
if (rawFile.endsWith(".jar")) {
mJarPaths.add(rawFile);
@@ -308,7 +308,16 @@
}
@Override
- public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ public void testFailed(TestIdentifier test, String trace) {
+ captureFailureLog(test);
+ }
+
+ @Override
+ public void testAssumptionFailure(TestIdentifier test, String trace) {
+ captureFailureLog(test);
+ }
+
+ private void captureFailureLog(TestIdentifier test) {
if (mLoggingOption == LoggingOption.AFTER_FAILURE) {
onScreenshotAndBugreport(getDevice(), mListener, String.format("%s_%s_failure",
test.getClassName(), test.getTestName()));
diff --git a/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java b/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
index 1a75227..23087a0 100644
--- a/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
+++ b/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
@@ -21,6 +21,7 @@
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
@@ -60,9 +61,22 @@
private ITestDevice mDevice;
+ @Deprecated
@Option(name = "timeout",
- description = "Fail any test that takes longer than the specified number of "
- + "milliseconds.")
+ description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
+ private Integer mTimeout = null;
+
+ @Option(name = "shell-timeout",
+ description="The defined timeout (in milliseconds) is used as a maximum waiting time "
+ + "when expecting the command output from the device. At any time, if the "
+ + "shell command does not output anything for a period longer than defined "
+ + "timeout the TF run terminates. For no timeout, set to 0.")
+ private long mShellTimeout = 10 * 60 * 1000; // default to 10 minutes
+
+ @Option(name = "test-timeout",
+ description="Sets timeout (in milliseconds) that will be applied to each test. In the "
+ + "event of a test timeout it will log the results and proceed with executing "
+ + "the next test. For no timeout, set to 0.")
private int mTestTimeout = 10 * 60 * 1000; // default to 10 minutes
@Option(name = "size",
@@ -215,6 +229,13 @@
test.setRerunMode(mIsRerunMode);
test.setResumeMode(mIsResumeMode);
test.setTestSize(getTestSize());
+ if (mTimeout != null) {
+ LogUtil.CLog
+ .w("\"timeout\" argument is deprecated and should not be used! \"shell-timeout\""
+ + " argument value is overwritten with %d ms", mTimeout);
+ setShellTimeout(mTimeout);
+ }
+ test.setShellTimeout(getShellTimeout());
test.setTestTimeout(getTestTimeout());
test.setCoverageTarget(def.getCoverageTarget());
mTests.add(test);
@@ -306,6 +327,14 @@
return files;
}
+ void setShellTimeout(long timeout) {
+ mShellTimeout = timeout;
+ }
+
+ long getShellTimeout() {
+ return mShellTimeout;
+ }
+
int getTestTimeout() {
return mTestTimeout;
}
diff --git a/src/com/android/tradefed/util/AaptParser.java b/src/com/android/tradefed/util/AaptParser.java
index a3c5c0e..bd598d1 100644
--- a/src/com/android/tradefed/util/AaptParser.java
+++ b/src/com/android/tradefed/util/AaptParser.java
@@ -28,11 +28,17 @@
*/
public class AaptParser {
private static final Pattern PKG_PATTERN = Pattern.compile(
- "package:\\s+name='(.*?)'\\s+versionCode='(\\d+)'\\s+versionName='(.*)'");
+ "^package:\\s+name='(.*?)'\\s+versionCode='(\\d+)'\\s+versionName='(.*?)'.*$",
+ Pattern.MULTILINE);
+ private static final Pattern LABEL_PATTERN = Pattern.compile(
+ "^application-label:'(.+?)'.*$",
+ Pattern.MULTILINE);
+ private static final int AAPT_TIMEOUT_MS = 60000;
private String mPackageName;
private String mVersionCode;
private String mVersionName;
+ private String mLabel;
// @VisibleForTesting
AaptParser() {
@@ -42,8 +48,13 @@
Matcher m = PKG_PATTERN.matcher(aaptOut);
if (m.find()) {
mPackageName = m.group(1);
+ mLabel = mPackageName;
mVersionCode = m.group(2);
mVersionName = m.group(3);
+ m = LABEL_PATTERN.matcher(aaptOut);
+ if (m.find()) {
+ mLabel = m.group(1);
+ }
return true;
}
CLog.e("Failed to parse package and version info from 'aapt dump badging'. stdout: '%s'",
@@ -58,8 +69,8 @@
* @return the {@link AaptParser} or <code>null</code> if failed to extract the information
*/
public static AaptParser parse(File apkFile) {
- CommandResult result = RunUtil.getDefault().runTimedCmd(5000, "aapt", "dump", "badging",
- apkFile.getAbsolutePath());
+ CommandResult result = RunUtil.getDefault().runTimedCmd(AAPT_TIMEOUT_MS,
+ "aapt", "dump", "badging", apkFile.getAbsolutePath());
String stderr = result.getStderr();
if (stderr != null && stderr.length() > 0) {
@@ -87,4 +98,8 @@
public String getVersionName() {
return mVersionName;
}
+
+ public String getLabel() {
+ return mLabel;
+ }
}
diff --git a/src/com/android/tradefed/util/AbiFormatter.java b/src/com/android/tradefed/util/AbiFormatter.java
index 9ebbb31..09e1bcf 100644
--- a/src/com/android/tradefed/util/AbiFormatter.java
+++ b/src/com/android/tradefed/util/AbiFormatter.java
@@ -28,6 +28,7 @@
public class AbiFormatter {
private static final String PRODUCT_CPU_ABILIST_KEY = "ro.product.cpu.abilist";
+ private static final String PRODUCT_CPU_ABI_KEY = "ro.product.cpu.abi";
public static final String FORCE_ABI_STRING = "force-abi";
public static final String FORCE_ABI_DESCRIPTION = "The abi to use, can be either 32 or 64.";
@@ -77,7 +78,7 @@
public static String getDefaultAbi(ITestDevice device, String bitness)
throws DeviceNotAvailableException {
String []abis = getSupportedAbis(device, bitness);
- if (abis.length > 0 && abis[0].length() > 0) {
+ if (abis != null && abis.length > 0 && abis[0] != null && abis[0].length() > 0) {
return abis[0];
}
return null;
@@ -93,10 +94,13 @@
public static String[] getSupportedAbis(ITestDevice device, String bitness)
throws DeviceNotAvailableException {
String abiList = device.getProperty(PRODUCT_CPU_ABILIST_KEY + bitness);
- if (abiList != null) {
+ if (abiList != null && !abiList.isEmpty()) {
String []abis = abiList.split(",");
- return abis;
+ if (abis.length > 0) {
+ return abis;
+ }
}
- return new String[0];
+ // fallback plan for before lmp, the bitness is ignored
+ return new String[]{device.getProperty(PRODUCT_CPU_ABI_KEY)};
}
}
diff --git a/src/com/android/tradefed/util/Alarm.java b/src/com/android/tradefed/util/Alarm.java
new file mode 100644
index 0000000..f5ddc64
--- /dev/null
+++ b/src/com/android/tradefed/util/Alarm.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A thread which waits for a period of time and then interrupts a specific other thread.
+ * Can call {@link Thread#interrupt()} to get a thread out of a blocking wait, or
+ * {@link Socket#close()} to stop a thread from blocking on a socket read or write.
+ * <p/>
+ * All time units are in milliseconds.
+ */
+public class Alarm extends Thread {
+ private final List<Thread> mInterruptThreads = new ArrayList<Thread>();
+ private final List<Socket> mInterruptSockets = new ArrayList<Socket>();
+ private final long mTimeoutTime;
+ private boolean mAlarmFired = false;
+
+ /**
+ * Constructor takes the amount of time to wait, in millis.
+ *
+ * @param timeout The amount of time to wait, in millis
+ * @throws IllegalArgumentException if {@code timeout <= 0}.
+ */
+ public Alarm(long timeout) {
+ super();
+ setDaemon(true);
+
+ if (timeout <= 0) {
+ throw new IllegalArgumentException(String.format(
+ "Alarm timeout time %d <= 0, which is not valid.", timeout));
+ }
+
+ mTimeoutTime = timeout;
+ }
+
+ public void addThread(Thread intThread) {
+ mInterruptThreads.add(intThread);
+ }
+
+ public void addSocket(Socket intSocket) {
+ mInterruptSockets.add(intSocket);
+ }
+
+ public boolean didAlarmFire() {
+ return mAlarmFired;
+ }
+
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(mTimeoutTime);
+ } catch (InterruptedException e) {
+ // Expected; return without interrupt()ing any of our InterruptThreads
+ return;
+ }
+ mAlarmFired = true;
+ for (Socket sock : mInterruptSockets) {
+ try {
+ sock.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ for (Thread thread : mInterruptThreads) {
+ thread.interrupt();
+ }
+ }
+}
+
diff --git a/src/com/android/tradefed/util/BulkEmailer.java b/src/com/android/tradefed/util/BulkEmailer.java
index 681df03..6522b85 100644
--- a/src/com/android/tradefed/util/BulkEmailer.java
+++ b/src/com/android/tradefed/util/BulkEmailer.java
@@ -49,7 +49,7 @@
private int mInitialBurst = 0;
@Option(name = "sender", description = "the sender email.", importance = Importance.NEVER)
- private String mSender = "android.sync.battery.test@gmail.com";
+ private String mSender = "android-power-lab-external@google.com";
private static final String SUBJECT = "No emails to send";
private static final String MESSAGE = "This is a test message!";
diff --git a/src/com/android/tradefed/util/FileUtil.java b/src/com/android/tradefed/util/FileUtil.java
index be3eab3..4e59201 100644
--- a/src/com/android/tradefed/util/FileUtil.java
+++ b/src/com/android/tradefed/util/FileUtil.java
@@ -420,7 +420,7 @@
StreamUtil.copyStreams(origStream, destStream);
} finally {
StreamUtil.close(origStream);
- StreamUtil.close(destStream);
+ StreamUtil.flushAndCloseStream(destStream);
}
}
@@ -793,4 +793,16 @@
public static void gzipFile(File file, File gzipFile) throws IOException {
ZipUtil.gzipFile(file, gzipFile);
}
+
+ /**
+ * Helper method to calculate md5 for a file.
+ *
+ * @param file
+ * @return md5 of the file
+ * @throws IOException
+ */
+ public static String calculateMd5(File file) throws IOException {
+ FileInputStream inputSource = new FileInputStream(file);
+ return StreamUtil.calculateMd5(inputSource);
+ }
}
diff --git a/src/com/android/tradefed/util/IRunUtil.java b/src/com/android/tradefed/util/IRunUtil.java
index f7e837b..ef8d735 100644
--- a/src/com/android/tradefed/util/IRunUtil.java
+++ b/src/com/android/tradefed/util/IRunUtil.java
@@ -18,6 +18,7 @@
import java.io.File;
import java.io.IOException;
+import java.io.OutputStream;
import java.util.List;
/**
@@ -65,6 +66,15 @@
public void setEnvVariable(String key, String value);
/**
+ * Unsets an environment variable, so the system commands run without this environment variable.
+ *
+ * @param key the variable name
+ *
+ * @see {@link ProcessBuilder#environment()}
+ */
+ public void unsetEnvVariable(String key);
+
+ /**
* Helper method to execute a system command, and aborting if it takes longer than a specified
* time.
*
@@ -130,6 +140,17 @@
public Process runCmdInBackground(List<String> command) throws IOException;
/**
+ * Running command with a {@link OutputStream} log the output of the command.
+ * Stdout and stderr are merged together.
+ * @param command the command to run
+ * @param output the OutputStream to save the output
+ * @return the {@link Process} running the command
+ * @throws IOException
+ */
+ public Process runCmdInBackground(List<String> command, OutputStream output)
+ throws IOException;
+
+ /**
* Block and executes an operation, aborting if it takes longer than a specified time.
*
* @param timeout maximum time to wait in ms
@@ -188,4 +209,21 @@
* @param time ms to sleep. values less than or equal to 0 will be ignored
*/
public void sleep(long time);
+
+ /**
+ * Allows/disallows run interrupts on the current thread. If it is allowed, run operations of
+ * the current thread can be interrupted from other threads via {@link #interrupt} method.
+ *
+ * @param allow whether to allow run interrupts on the current thread.
+ */
+ public void allowInterrupt(boolean allow);
+
+ /**
+ * Interrupts the ongoing/forthcoming run operations on the given thread. The run operations on
+ * the given thread will throw {@link RunInterruptedException}.
+ *
+ * @param thread
+ * @param message the message for {@link RunInterruptedException}.
+ */
+ public void interrupt(Thread thread, String message);
}
diff --git a/src/com/android/tradefed/util/JUnitXmlParser.java b/src/com/android/tradefed/util/JUnitXmlParser.java
index 2ea6f58..edecc76 100644
--- a/src/com/android/tradefed/util/JUnitXmlParser.java
+++ b/src/com/android/tradefed/util/JUnitXmlParser.java
@@ -16,7 +16,6 @@
package com.android.tradefed.util;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.util.xml.AbstractXmlParser;
@@ -114,7 +113,7 @@
mTestListener.testEnded(mCurrentTest, Collections.<String, String> emptyMap());
}
if (FAILURE_TAG.equalsIgnoreCase(name)) {
- mTestListener.testFailed(TestFailure.FAILURE, mCurrentTest,
+ mTestListener.testFailed(mCurrentTest,
mFailureContent.toString());
}
mFailureContent = null;
diff --git a/src/com/android/tradefed/util/RunInterruptedException.java b/src/com/android/tradefed/util/RunInterruptedException.java
new file mode 100644
index 0000000..0f40fa3
--- /dev/null
+++ b/src/com/android/tradefed/util/RunInterruptedException.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+
+/**
+ * Thrown when a run operation is interrupted by an external request.
+ */
+@SuppressWarnings("serial")
+public class RunInterruptedException extends RuntimeException {
+ /**
+ * Creates a {@link RunInterruptedException}.
+ */
+ public RunInterruptedException() {
+ super();
+ }
+
+ /**
+ * Creates a {@link RunInterruptedException}.
+ *
+ * @param msg a descriptive message.
+ */
+ public RunInterruptedException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Creates a {@link RunInterruptedException}.
+ *
+ * @param cause the root {@link Throwable} that caused the device to become unavailable.
+ */
+ public RunInterruptedException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Creates a {@link RunInterruptedException}.
+ *
+ * @param msg a descriptive message.
+ * @param cause the root {@link Throwable} that caused the device to become unavailable.
+ */
+ public RunInterruptedException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/src/com/android/tradefed/util/RunUtil.java b/src/com/android/tradefed/util/RunUtil.java
index 2066248..73afb48 100644
--- a/src/com/android/tradefed/util/RunUtil.java
+++ b/src/com/android/tradefed/util/RunUtil.java
@@ -27,8 +27,10 @@
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* A collection of helper methods for executing operations.
@@ -39,6 +41,14 @@
private static IRunUtil sDefaultInstance = null;
private File mWorkingDir = null;
private Map<String, String> mEnvVariables = new HashMap<String, String>();
+ private Set<String> mUnsetEnvVariables = new HashSet<String>();
+ private ThreadLocal<Boolean> mIsInterruptAllowed = new ThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return Boolean.FALSE;
+ }
+ };
+ private Map<Long, String> mInterruptThreads = new HashMap<>();
/**
* Create a new {@link RunUtil} object to use.
@@ -85,6 +95,22 @@
/**
* {@inheritDoc}
+ * Environment variables may inherit from the parent process, so we need to delete
+ * the environment variable from {@link ProcessBuilder#environment()}
+ *
+ * @param key the variable name
+ * @see {@link ProcessBuilder#environment()}
+ */
+ @Override
+ public synchronized void unsetEnvVariable(String key) {
+ if (this.equals(sDefaultInstance)) {
+ throw new UnsupportedOperationException("Cannot unsetEnvVariable on default RunUtil");
+ }
+ mUnsetEnvVariables.add(key);
+ }
+
+ /**
+ * {@inheritDoc}
*/
@Override
public CommandResult runTimedCmd(final long timeout, final String... command) {
@@ -97,14 +123,7 @@
}
private synchronized ProcessBuilder createProcessBuilder(String... command) {
- ProcessBuilder processBuilder = new ProcessBuilder();
- if (mWorkingDir != null) {
- processBuilder.directory(mWorkingDir);
- }
- if (!mEnvVariables.isEmpty()) {
- processBuilder.environment().putAll(mEnvVariables);
- }
- return processBuilder.command(command);
+ return createProcessBuilder(Arrays.asList(command));
}
private synchronized ProcessBuilder createProcessBuilder(List<String> commandList) {
@@ -115,6 +134,10 @@
if (!mEnvVariables.isEmpty()) {
processBuilder.environment().putAll(mEnvVariables);
}
+ if (!mUnsetEnvVariables.isEmpty()) {
+ // in this implementation, the unsetEnv's priority is higher than set.
+ processBuilder.environment().keySet().removeAll(mUnsetEnvVariables);
+ }
return processBuilder.command(commandList);
}
@@ -177,8 +200,23 @@
* {@inheritDoc}
*/
@Override
+ public Process runCmdInBackground(List<String> command, OutputStream output)
+ throws IOException {
+ CLog.v("Running %s", command);
+ Process process = createProcessBuilder(command).start();
+ inheritIO(process.getInputStream(), output);
+ inheritIO(process.getErrorStream(), output);
+ return process;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public CommandStatus runTimed(long timeout, IRunUtil.IRunnableResult runnable,
boolean logErrors) {
+ checkInterrupted();
RunnableNotifier runThread = new RunnableNotifier(runnable, logErrors);
runThread.start();
try {
@@ -190,6 +228,7 @@
|| runThread.getStatus() == CommandStatus.EXCEPTION) {
runThread.interrupt();
}
+ checkInterrupted();
return runThread.getStatus();
}
@@ -273,6 +312,7 @@
*/
@Override
public void sleep(long time) {
+ checkInterrupted();
if (time <= 0) {
return;
}
@@ -282,6 +322,37 @@
// ignore
CLog.d("sleep interrupted");
}
+ checkInterrupted();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void allowInterrupt(boolean allow) {
+ CLog.d("run interrupt allowed: %s", allow);
+ mIsInterruptAllowed.set(allow);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public synchronized void interrupt(Thread thread, String message) {
+ if (message == null) {
+ throw new IllegalArgumentException("message cannot be null.");
+ }
+ mInterruptThreads.put(thread.getId(), message);
+ }
+
+ private synchronized void checkInterrupted() {
+ final long threadId = Thread.currentThread().getId();
+ if (mIsInterruptAllowed.get()) {
+ final String message = mInterruptThreads.remove(threadId);
+ if (message != null) {
+ throw new RunInterruptedException(message);
+ }
+ }
}
/**
@@ -362,16 +433,14 @@
// Wait for process to complete.
int rc = mProcess.waitFor();
synchronized (this) {
- if (mProcess != null) {
- // wait for stdout and stderr to be read
- stdoutThread.join();
- stderrThread.join();
- // Write out the streams to the result.
- mCommandResult.setStdout(stdOut.toString("UTF-8"));
- mCommandResult.setStderr(stdErr.toString("UTF-8"));
- stdOut.close();
- stdErr.close();
- }
+ // wait for stdout and stderr to be read
+ stdoutThread.join();
+ stderrThread.join();
+ // Write out the streams to the result.
+ mCommandResult.setStdout(stdOut.toString("UTF-8"));
+ mCommandResult.setStderr(stdErr.toString("UTF-8"));
+ stdOut.close();
+ stdErr.close();
}
if (rc == 0) {
@@ -391,7 +460,7 @@
}
}
}
- };
+ }
/**
* Helper method to redirect input stream.
diff --git a/src/com/android/tradefed/util/StreamUtil.java b/src/com/android/tradefed/util/StreamUtil.java
index 9e93703..eb80284 100644
--- a/src/com/android/tradefed/util/StreamUtil.java
+++ b/src/com/android/tradefed/util/StreamUtil.java
@@ -28,9 +28,14 @@
import java.io.PrintStream;
import java.io.Reader;
import java.io.Writer;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipOutputStream;
+import javax.xml.bind.DatatypeConverter;
+
/**
* Utility class for managing input streams.
*/
@@ -265,6 +270,31 @@
/** Discards the specified byte array. */
@Override public void write(byte[] b, int off, int len) {
}
- };
+ };
+ }
+
+ /**
+ * Helper method to calculate md5 for a inputStream. The inputStream will be consumed and
+ * closed.
+ *
+ * @param inputSource used to create inputStream
+ * @return md5 of the stream
+ * @throws IOException
+ */
+ static String calculateMd5(InputStream inputSource) throws IOException {
+ MessageDigest md = null;
+ try {
+ md = MessageDigest.getInstance("md5");
+ } catch (NoSuchAlgorithmException e) {
+ // This should not happen
+ throw new RuntimeException(e);
+ }
+ InputStream input = new BufferedInputStream(new DigestInputStream(inputSource, md));
+ while (input.read() >= 0) {
+ // Read through the stream to update digest.
+ }
+ input.close();
+ String md5 = DatatypeConverter.printHexBinary(md.digest()).toLowerCase();
+ return md5;
}
}
diff --git a/src/com/android/tradefed/util/TimeVal.java b/src/com/android/tradefed/util/TimeVal.java
new file mode 100644
index 0000000..c35628a
--- /dev/null
+++ b/src/com/android/tradefed/util/TimeVal.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import com.google.common.math.LongMath;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This is a sentinel type which wraps a {@code Long}. It exists solely as a hint to the
+ * options parsing machinery that a particular value should be parsed as if it were a string
+ * representing a time value.
+ */
+@SuppressWarnings("serial")
+public class TimeVal extends Number implements Comparable<Long> {
+ private static final Pattern TIME_PATTERN =
+ Pattern.compile("(?i)" + // case insensitive
+ "(?:(?<d>\\d+)d)?" + // a number followed by "d"
+ "(?:(?<h>\\d+)h)?" +
+ "(?:(?<m>\\d+)m)?" +
+ "(?:(?<s>\\d+)s)?" +
+ "(?:(?<ms>\\d+)(?:ms)?)?"); // a number followed by "ms"
+
+ private Long mValue = null;
+
+ /**
+ * Constructs a newly allocated TimeVal object that represents the specified Long argument
+ */
+ public TimeVal(Long value) {
+ mValue = value;
+ }
+
+ /**
+ * Constructs a newly allocated TimeVal object that represents the <emph>timestamp</emph>
+ * indicated by the String parameter. The string is converted to a TimeVal in exactly the
+ * manner used by the {@see fromString(String)} method.
+ */
+ public TimeVal(String value) throws NumberFormatException {
+ mValue = fromString(value);
+ }
+
+ /**
+ * @return the wrapped {@code Long} value.
+ */
+ public Long asLong() {
+ return mValue;
+ }
+
+ /**
+ * Parses the string as a hierarchical time value
+ * <p />
+ * The default unit is millis. The parser will accept {@code s} for seconds (1000 millis),
+ * {@code m} for minutes (60 seconds), {@code h} for hours (60 minutes), or {@code d} for days
+ * (24 hours).
+ * <p />
+ * Units may be mixed and matched, so long as each unit appears at most once, and so long as
+ * all units which do appear are listed in decreasing order of scale. So, for instance,
+ * {@code h} may only appear before {@code m}, and may only appear after {@code d}. As a
+ * specific example, "1d2h3m4s5ms" would be a valid time value, as would "4" or "4ms". All
+ * embedded whitespace is discarded.
+ * <p />
+ * Do note that this method rejects overflows. So the output number is guaranteed to be
+ * non-negative, and to fit within the {@code long} type.
+ */
+ public static long fromString(String value) throws NumberFormatException {
+ if (value == null) throw new NumberFormatException("value is null");
+
+ try {
+ value = value.replaceAll("\\s+", "");
+ Matcher m = TIME_PATTERN.matcher(value);
+ if (m.matches()) {
+ // This works by, essentially, modifying the units of timeValue, from the
+ // largest supported unit, until we've dropped down to millis.
+ long timeValue = 0;
+ timeValue = val(m.group("d"));
+
+ // 1 day == 24 hours
+ timeValue = LongMath.checkedMultiply(timeValue, 24);
+ timeValue = LongMath.checkedAdd(timeValue, val(m.group("h")));
+
+ // 1 hour == 60 minutes
+ timeValue = LongMath.checkedMultiply(timeValue, 60);
+ timeValue = LongMath.checkedAdd(timeValue, val(m.group("m")));
+
+ // 1 hour == 60 seconds
+ timeValue = LongMath.checkedMultiply(timeValue, 60);
+ timeValue = LongMath.checkedAdd(timeValue, val(m.group("s")));
+
+ // 1 second == 1000 millis
+ timeValue = LongMath.checkedMultiply(timeValue, 1000);
+ timeValue = LongMath.checkedAdd(timeValue, val(m.group("ms")));
+
+ return timeValue;
+ }
+ } catch (ArithmeticException e) {
+ throw new NumberFormatException(String.format(
+ "Failed to parse value %s as a time value: %s", value, e.getMessage()));
+ }
+
+ throw new NumberFormatException(
+ String.format("Failed to parse value %s as a time value", value));
+ }
+
+ static long val(String str) throws NumberFormatException {
+ if (str == null) return 0;
+
+ Long value = Long.parseLong(str);
+ if (value == null) return 0;
+ return value;
+ }
+
+
+ // implementing interfaces
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public double doubleValue() {
+ return mValue.doubleValue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float floatValue() {
+ return mValue.floatValue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int intValue() {
+ return mValue.intValue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long longValue() {
+ return mValue.longValue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int compareTo(Long other) {
+ return mValue.compareTo(other);
+ }
+}
diff --git a/src/com/android/tradefed/util/VersionParser.java b/src/com/android/tradefed/util/VersionParser.java
index fb2dc77..1f20a21 100644
--- a/src/com/android/tradefed/util/VersionParser.java
+++ b/src/com/android/tradefed/util/VersionParser.java
@@ -15,19 +15,18 @@
*/
package com.android.tradefed.util;
+import com.android.tradefed.log.LogUtil.CLog;
+
import java.io.File;
import java.io.IOException;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.FileUtil;
-
public class VersionParser {
private static final String DEFAULT_VERSION_FILE_NAME = "tf_version.txt";
public static String fetchVersion(File file) {
if (file.exists()) {
try {
- return FileUtil.readStringFromFile(file);
+ return FileUtil.readStringFromFile(file).trim();
} catch (IOException e) {
CLog.e(e.toString());
return null;
diff --git a/src/com/android/tradefed/util/ZipUtil.java b/src/com/android/tradefed/util/ZipUtil.java
index 957a7e6..cc18413 100644
--- a/src/com/android/tradefed/util/ZipUtil.java
+++ b/src/com/android/tradefed/util/ZipUtil.java
@@ -163,7 +163,7 @@
* @param relativePathSegs the relative path of file, including separators
* @throws IOException if failed to add file to zip
*/
- private static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
+ public static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
throws IOException {
relativePathSegs.add(file.getName());
if (file.isDirectory()) {
diff --git a/src/com/android/tradefed/util/net/HttpHelper.java b/src/com/android/tradefed/util/net/HttpHelper.java
index f7098ee..e4854df 100644
--- a/src/com/android/tradefed/util/net/HttpHelper.java
+++ b/src/com/android/tradefed/util/net/HttpHelper.java
@@ -124,6 +124,21 @@
* {@inheritDoc}
*/
@Override
+ public void doGet(String url, OutputStream outputStream) throws IOException {
+ CLog.d("Performing GET download request for %s", url);
+ InputStream remote = null;
+ try {
+ remote = getRemoteUrlStream(new URL(url));
+ StreamUtil.copyStreams(remote, outputStream);
+ } finally {
+ StreamUtil.close(remote);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public void doGetIgnore(String url) throws IOException {
CLog.d("Performing GET request for %s. Ignoring result.", url);
InputStream remote = null;
@@ -240,7 +255,7 @@
* Runnable for making requests with
* {@link IRunUtil#runEscalatingTimedRetry(long, long, long, long, IRunnableResult)}.
*/
- private abstract class RequestRunnable implements IRunnableResult {
+ public abstract class RequestRunnable implements IRunnableResult {
private String mResponse = null;
private Exception mException = null;
private final String mUrl;
@@ -499,7 +514,7 @@
/**
* Get {@link IRunUtil} to use. Exposed so unit tests can mock.
*/
- IRunUtil getRunUtil() {
+ public IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
}
diff --git a/src/com/android/tradefed/util/net/IHttpHelper.java b/src/com/android/tradefed/util/net/IHttpHelper.java
index c1bde53..49c3fb7 100644
--- a/src/com/android/tradefed/util/net/IHttpHelper.java
+++ b/src/com/android/tradefed/util/net/IHttpHelper.java
@@ -20,6 +20,7 @@
import com.android.tradefed.util.MultiMap;
import java.io.IOException;
+import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
@@ -83,6 +84,17 @@
public String doGet(String url) throws IOException, DataSizeException;
/**
+ * Performs a GET HTTP request method for a given URL and streams result to a
+ * {@link OutputStream}.
+ *
+ * @see #doGet(String)
+ * @param url the URL
+ * @param outputStream stream of the response data
+ * @throws IOException if failed to retrieve data
+ */
+ public void doGet(String url, OutputStream outputStream) throws IOException;
+
+ /**
* Performs {{@link #doGet(String)} retrying upon failure.
*
* @see IRunUtil#runEscalatingTimedRetry(long, long, long, long,
diff --git a/tests/Android.mk b/tests/Android.mk
index 31f1748..df45f09 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -26,7 +26,7 @@
LOCAL_MODULE := tradefed-tests
LOCAL_MODULE_TAGS := optional
LOCAL_STATIC_JAVA_LIBRARIES := easymock
-LOCAL_JAVA_LIBRARIES := tradefed ddmlib-prebuilt
+LOCAL_JAVA_LIBRARIES := tradefed
include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/tests/res/config/tf/acceptance.xml b/tests/res/config/tf/acceptance.xml
new file mode 100644
index 0000000..ae396a4
--- /dev/null
+++ b/tests/res/config/tf/acceptance.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration
+ description="Run the tradefed stress tests on a userdebug build with single iteration">
+
+ <test class="com.android.tradefed.device.TestDeviceStressTest">
+ <option name="iterations" value="1" />
+ </test>
+</configuration>
diff --git a/tests/res/testCmdFiles/basic.txt b/tests/res/testCmdFiles/basic.txt
new file mode 100644
index 0000000..39c3afd
--- /dev/null
+++ b/tests/res/testCmdFiles/basic.txt
@@ -0,0 +1,13 @@
+MACRO f0 = 0
+MACRO f1 = 1
+MACRO f2 = 2
+MACRO f3 = 3
+
+LONG MACRO f
+f0()
+f1()
+f2()
+f3()
+END MACRO
+
+recharge --max-battery f() --noisy-dry-run
diff --git a/tests/res/testCmdFiles/missing-begin-macro.txt b/tests/res/testCmdFiles/missing-begin-macro.txt
new file mode 100644
index 0000000..b5ad227
--- /dev/null
+++ b/tests/res/testCmdFiles/missing-begin-macro.txt
@@ -0,0 +1,15 @@
+MACRO f0 = 0
+MACRO f1 = 1
+MACRO f2 = 2
+MACRO f3 = 3
+
+#LONG MACRO f
+f0()
+f1()
+f2()
+f3()
+# Note that "END MACRO" by itself is potentially a completely valid command.
+# This will actually fail because the macro f() is now undefined.
+END MACRO
+
+recharge --max-battery f() --noisy-dry-run
diff --git a/tests/res/testCmdFiles/missing-end-macro.txt b/tests/res/testCmdFiles/missing-end-macro.txt
new file mode 100644
index 0000000..7f24bac
--- /dev/null
+++ b/tests/res/testCmdFiles/missing-end-macro.txt
@@ -0,0 +1,13 @@
+MACRO f0 = 0
+MACRO f1 = 1
+MACRO f2 = 2
+MACRO f3 = 3
+
+LONG MACRO f
+f0()
+f1()
+f2()
+f3()
+#END MACRO
+
+recharge --max-battery f() --noisy-dry-run
diff --git a/tests/res/testCmdFiles/missing-macro-def.txt b/tests/res/testCmdFiles/missing-macro-def.txt
new file mode 100644
index 0000000..defb8d2
--- /dev/null
+++ b/tests/res/testCmdFiles/missing-macro-def.txt
@@ -0,0 +1,13 @@
+MACRO f0 = 0
+MACRO f1 = 1
+MACRO f2 = 2
+#MACRO f3 = 3
+
+LONG MACRO f
+f0()
+f1()
+f2()
+f3()
+END MACRO
+
+recharge --max-battery f() --noisy-dry-run
diff --git a/tests/res/testconfigs/depend-template-include-config.xml b/tests/res/testconfigs/depend-template-include-config.xml
new file mode 100644
index 0000000..028b9c2
--- /dev/null
+++ b/tests/res/testconfigs/depend-template-include-config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration
+ description="test template-based config inclusion with templated targets">
+
+ <template-include name="dep-target" />
+
+ <test class="com.android.tradefed.config.StubOptionTest" >
+ <option name="option" value="valueFromDependTemplateIncludeConfig" />
+ </test>
+
+</configuration>
diff --git a/tests/res/testconfigs/include-template-config-with-default.xml b/tests/res/testconfigs/include-template-config-with-default.xml
new file mode 100644
index 0000000..ed4c674
--- /dev/null
+++ b/tests/res/testconfigs/include-template-config-with-default.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration
+ description="Make sure that we can include a template-include-containing config if it has a default resolution set.">
+
+ <include name="template-include-config-with-default" />
+
+ <test class="com.android.tradefed.config.StubOptionTest" >
+ <option name="option" value="valueFromIncludeTemplateConfigWithDefault" />
+ </test>
+
+</configuration>
diff --git a/tests/res/testconfigs/include-template-config.xml b/tests/res/testconfigs/include-template-config.xml
new file mode 100644
index 0000000..5654045
--- /dev/null
+++ b/tests/res/testconfigs/include-template-config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration
+ description="test to demonstrate that including a template-include-containing config doesn't work">
+
+ <include name="template-include-config" />
+
+ <test class="com.android.tradefed.config.StubOptionTest" >
+ <option name="option" value="valueFromIncludeTemplateConfig" />
+ </test>
+
+</configuration>
diff --git a/tests/res/testconfigs/template-include-config-with-default.xml b/tests/res/testconfigs/template-include-config-with-default.xml
new file mode 100644
index 0000000..4af14e5
--- /dev/null
+++ b/tests/res/testconfigs/template-include-config-with-default.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration
+ description="test template-based config inclusion with default values">
+
+ <template-include name="target" default="test-config" />
+
+ <test class="com.android.tradefed.config.StubOptionTest" >
+ <option name="option" value="valueFromTemplateIncludeWithDefaultConfig" />
+ </test>
+
+</configuration>
diff --git a/tests/res/testconfigs/template-include-config.xml b/tests/res/testconfigs/template-include-config.xml
new file mode 100644
index 0000000..d8b3f7f
--- /dev/null
+++ b/tests/res/testconfigs/template-include-config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration
+ description="test template-based config inclusion">
+
+ <template-include name="target" />
+
+ <test class="com.android.tradefed.config.StubOptionTest" >
+ <option name="option" value="valueFromTemplateIncludeConfig" />
+ </test>
+
+</configuration>
diff --git a/tests/src/com/android/tradefed/FuncTests.java b/tests/src/com/android/tradefed/FuncTests.java
index 422d84f..4435f22 100644
--- a/tests/src/com/android/tradefed/FuncTests.java
+++ b/tests/src/com/android/tradefed/FuncTests.java
@@ -17,6 +17,7 @@
import com.android.tradefed.build.FileDownloadCacheFuncTest;
import com.android.tradefed.command.CommandSchedulerFuncTest;
+import com.android.tradefed.command.remote.RemoteManagerFuncTest;
import com.android.tradefed.device.TestDeviceFuncTest;
import com.android.tradefed.targetprep.DeviceSetupFuncTest;
import com.android.tradefed.testtype.DeviceTestSuite;
@@ -41,6 +42,8 @@
this.addTestSuite(FileDownloadCacheFuncTest.class);
// command
this.addTestSuite(CommandSchedulerFuncTest.class);
+ // command.remote
+ this.addTestSuite(RemoteManagerFuncTest.class);
// device
this.addTestSuite(TestDeviceFuncTest.class);
// targetprep
diff --git a/tests/src/com/android/tradefed/TfTestLauncher.java b/tests/src/com/android/tradefed/TfTestLauncher.java
index 2946883..cdd2b00 100644
--- a/tests/src/com/android/tradefed/TfTestLauncher.java
+++ b/tests/src/com/android/tradefed/TfTestLauncher.java
@@ -15,12 +15,11 @@
*/
package com.android.tradefed;
-import com.android.ddmlib.Log;
-import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.IFolderBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.testtype.IRemoteTest;
@@ -56,6 +55,7 @@
@Option(name = "config-name", description = "the config that runs the TF tests")
private String mConfigName;
+ private static final String TF_GLOBAL_CONFIG = "TF_GLOBAL_CONFIG";
/**
* {@inheritDoc}
*/
@@ -90,19 +90,18 @@
args.add(mBuildInfo.getBuildFlavor());
}
- CommandResult result = getRunUtil().runTimedCmd(mMaxTfRunTimeMin * 60 * 1000,
+ IRunUtil runUtil = new RunUtil();
+ // clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file
+ runUtil.unsetEnvVariable(TF_GLOBAL_CONFIG);
+ CommandResult result = runUtil.runTimedCmd(mMaxTfRunTimeMin * 60 * 1000,
args.toArray(new String[0]));
if (result.getStatus().equals(CommandStatus.SUCCESS)) {
- Log.logAndDisplay(LogLevel.INFO, "TfTestLauncher",
- String.format("Successfully ran TF tests for build %s. stdout: %s\n, stderr: %s",
- mBuildInfo.getBuildId(), result.getStdout(), result.getStderr()));
-
+ CLog.d("Successfully ran TF tests for build %s", mBuildInfo.getBuildId());
} else {
- Log.logAndDisplay(LogLevel.INFO, "TfTestLauncher",
- String.format("Failed to run TF tests for build %s. stdout: %s\n, stderr: %s",
- mBuildInfo.getBuildId(), result.getStdout(),
- result.getStderr()));
+ CLog.w("Failed ran TF tests for build %s, status %s",
+ mBuildInfo.getBuildId(), result.getStatus());
}
+ CLog.v("TF tests output:\nstdout: %s\nstderror\n", result.getStdout(), result.getStderr());
}
IRunUtil getRunUtil() {
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 3e3fc4c..ca85966 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -26,7 +26,7 @@
import com.android.tradefed.command.CommandFileParserTest;
import com.android.tradefed.command.CommandSchedulerTest;
import com.android.tradefed.command.ConsoleTest;
-import com.android.tradefed.command.remote.RemoteManagerTest;
+import com.android.tradefed.command.VerifyTest;
import com.android.tradefed.command.remote.RemoteOperationTest;
import com.android.tradefed.config.ArgsOptionParserTest;
import com.android.tradefed.config.ConfigurationDefTest;
@@ -42,6 +42,7 @@
import com.android.tradefed.device.DeviceStateMonitorTest;
import com.android.tradefed.device.DeviceUtilStatsMonitorTest;
import com.android.tradefed.device.DumpsysPackageReceiverTest;
+import com.android.tradefed.device.FastbootHelperTest;
import com.android.tradefed.device.ManagedDeviceListTest;
import com.android.tradefed.device.ReconnectingRecoveryTest;
import com.android.tradefed.device.TestDeviceTest;
@@ -53,6 +54,8 @@
import com.android.tradefed.log.TerribleFailureEmailHandlerTest;
import com.android.tradefed.result.BugreportCollectorTest;
import com.android.tradefed.result.CollectingTestListenerTest;
+import com.android.tradefed.result.ConsoleResultReporterTest;
+import com.android.tradefed.result.DeviceFileReporterTest;
import com.android.tradefed.result.EmailResultReporterTest;
import com.android.tradefed.result.FailureEmailResultReporterTest;
import com.android.tradefed.result.FileSystemLogSaverTest;
@@ -104,6 +107,7 @@
import com.android.tradefed.util.RegexTrieTest;
import com.android.tradefed.util.RunUtilTest;
import com.android.tradefed.util.SizeLimitedOutputStreamTest;
+import com.android.tradefed.util.net.HttpHelperTest;
import com.android.tradefed.util.net.HttpMultipartPostTest;
import com.android.tradefed.util.xml.AndroidManifestWriterTest;
@@ -132,9 +136,9 @@
addTestSuite(CommandFileParserTest.class);
addTestSuite(CommandSchedulerTest.class);
addTestSuite(ConsoleTest.class);
+ addTestSuite(VerifyTest.class);
// command.remote
- addTestSuite(RemoteManagerTest.class);
addTestSuite(RemoteOperationTest.class);
// config
@@ -150,11 +154,12 @@
// device
addTestSuite(CpuStatsCollectorTest.class);
addTestSuite(DeviceManagerTest.class);
- addTestSuite(ManagedDeviceListTest.class);
addTestSuite(DeviceSelectionOptionsTest.class);
addTestSuite(DeviceStateMonitorTest.class);
addTestSuite(DeviceUtilStatsMonitorTest.class);
addTestSuite(DumpsysPackageReceiverTest.class);
+ addTestSuite(FastbootHelperTest.class);
+ addTestSuite(ManagedDeviceListTest.class);
addTestSuite(ReconnectingRecoveryTest.class);
addTestSuite(TestDeviceTest.class);
addTestSuite(WaitDeviceRecoveryTest.class);
@@ -170,7 +175,9 @@
// result
addTestSuite(BugreportCollectorTest.class);
+ addTestSuite(ConsoleResultReporterTest.class);
addTestSuite(CollectingTestListenerTest.class);
+ addTestSuite(DeviceFileReporterTest.class);
addTestSuite(EmailResultReporterTest.class);
addTestSuite(FailureEmailResultReporterTest.class);
addTestSuite(FileSystemLogSaverTest.class);
@@ -221,6 +228,7 @@
addTestSuite(ConditionPriorityBlockingQueueTest.class);
addTestSuite(EmailTest.class);
addTestSuite(FileUtilTest.class);
+ addTestSuite(HttpHelperTest.class);
addTestSuite(HttpMultipartPostTest.class);
addTestSuite(JUnitXmlParserTest.class);
addTestSuite(MultiMapTest.class);
diff --git a/tests/src/com/android/tradefed/command/CommandFileParserTest.java b/tests/src/com/android/tradefed/command/CommandFileParserTest.java
index dbb5d0a..a9ffd15 100644
--- a/tests/src/com/android/tradefed/command/CommandFileParserTest.java
+++ b/tests/src/com/android/tradefed/command/CommandFileParserTest.java
@@ -61,20 +61,20 @@
assertParsedData(expectedArgs);
}
- @SuppressWarnings("unchecked")
- private void assertParsedData(List<String>... expectedCommands) throws IOException,
+ @SafeVarargs
+ private final void assertParsedData(List<String>... expectedCommands) throws IOException,
ConfigurationException {
assertParsedData(mCommandFile, mMockFile, expectedCommands);
}
- @SuppressWarnings("unchecked")
- private void assertParsedData(CommandFileParser parser, List<String>... expectedCommands)
+ @SafeVarargs
+ private final void assertParsedData(CommandFileParser parser, List<String>... expectedCommands)
throws IOException, ConfigurationException {
assertParsedData(parser, mMockFile, expectedCommands);
}
- @SuppressWarnings("unchecked")
- private void assertParsedData(CommandFileParser parser, File file,
+ @SafeVarargs
+ private final void assertParsedData(CommandFileParser parser, File file,
List<String>... expectedCommands) throws IOException, ConfigurationException {
List<CommandLine> data = parser.parseFile(file);
assertEquals(expectedCommands.length, data.size());
diff --git a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
index 3446206..9faf93e 100644
--- a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
@@ -39,6 +39,8 @@
import org.easymock.EasyMock;
import org.easymock.IAnswer;
+import org.json.JSONArray;
+import org.json.JSONException;
import org.junit.Assert;
import java.io.File;
@@ -104,6 +106,11 @@
}
@Override
+ void checkInvocations() {
+ // ignore
+ }
+
+ @Override
CommandFileParser createCommandFileParser() {
return mMockCmdFileParser;
}
@@ -171,6 +178,20 @@
}
/**
+ * Test {@link CommandScheduler#addCommand(String[])} when json help mode is specified
+ */
+ public void testAddConfig_configJsonHelp() throws ConfigurationException, JSONException {
+ String[] args = new String[] {};
+ mCommandOptions.setJsonHelpMode(true);
+ setCreateConfigExpectations(args, 1);
+ // expect
+ EasyMock.expect(mMockConfiguration.getJsonCommandUsage()).andReturn(new JSONArray());
+ replayMocks();
+ mScheduler.addCommand(args);
+ verifyMocks();
+ }
+
+ /**
* Test {@link CommandScheduler#run()} when one config has been added
*/
public void testRun_oneConfig() throws Throwable {
@@ -551,14 +572,13 @@
mMockManager.setNumDevices(0);
String[] cmdFile1Args = new String[] {"fromFile1"};
setCreateConfigExpectations(cmdFile1Args, 1);
-
+ setCreateConfigExpectations(cmdFile1Args, 1);
mMockConfiguration.validateOptions();
- EasyMock.expectLastCall().times(1);
+ EasyMock.expectLastCall().times(2);
final List<CommandLine> cmdFileContent1 = Arrays.asList(new CommandLine(
Arrays.asList("fromFile1")));
mMockCmdFileParser = new CommandFileParser() {
- boolean firstCall = true;
@Override
public List<CommandLine> parseFile(File cmdFile) {
return cmdFileContent1;
@@ -575,7 +595,8 @@
// now attempt to add the same command file
mScheduler.addCommandFile("mycmd.txt", Collections.<String>emptyList());
- // ensure no effect
+ // expect reload
+ // ensure same state as before
cmds = mScheduler.getCommandTrackers();
assertEquals(1, cmds.size());
Assert.assertArrayEquals(cmdFile1Args, cmds.get(0).getArgs());
diff --git a/tests/src/com/android/tradefed/command/VerifyTest.java b/tests/src/com/android/tradefed/command/VerifyTest.java
new file mode 100644
index 0000000..a0b4e82
--- /dev/null
+++ b/tests/src/com/android/tradefed/command/VerifyTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.command;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.util.FileUtil;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link Verify}
+ */
+public class VerifyTest extends TestCase {
+ private static final String TEST_CMD_FILE_PATH = "/testCmdFiles";
+ private final Verify mVerify;
+
+ public VerifyTest() throws ConfigurationException {
+ mVerify = new Verify();
+
+ OptionSetter option = new OptionSetter(mVerify);
+ option.setOptionValue("quiet", "true");
+ }
+
+ /**
+ * Extract an embedded command file into a temporary file, which we can feed to the
+ * CommandFileParser
+ */
+ private File extractTestCmdFile(String name) throws IOException {
+ final InputStream cmdFileStream = getClass().getResourceAsStream(
+ String.format("%s/%s.txt", TEST_CMD_FILE_PATH, name));
+ final String tmpFileName = String.format("VerifyTest_%s_", name);
+ File tmpFile = FileUtil.createTempFile(tmpFileName, ".txt");
+ try {
+ FileUtil.writeToFile(cmdFileStream, tmpFile);
+ } catch (Throwable t) {
+ // Clean up tmpFile, if it was created.
+ FileUtil.deleteFile(tmpFile);
+ throw t;
+ }
+
+ return tmpFile;
+ }
+
+ /**
+ * Assert that the specified command file parses correctly, and clean up any temporary files
+ */
+ private void assertGoodCmdFile(String name) throws IOException {
+ File cmdFile = extractTestCmdFile(name);
+ try {
+ assertTrue(mVerify.runVerify(cmdFile));
+ } finally {
+ FileUtil.deleteFile(cmdFile);
+ }
+ }
+
+ /**
+ * Assert that the specified command file does not parse correctly, and clean up any temporary
+ * files
+ */
+ private void assertBadCmdFile(String name) throws IOException {
+ File cmdFile = extractTestCmdFile(name);
+ try {
+ assertFalse(mVerify.runVerify(cmdFile));
+ } finally {
+ FileUtil.deleteFile(cmdFile);
+ }
+ }
+
+ public void testBasic() throws IOException {
+ assertGoodCmdFile("basic");
+ }
+
+ public void testMissingMacroDef() throws IOException {
+ assertBadCmdFile("missing-macro-def");
+ }
+
+ public void testMissingBeginMacro() throws IOException {
+ assertBadCmdFile("missing-begin-macro");
+ }
+
+ public void testMissingEndMacro() throws IOException {
+ assertBadCmdFile("missing-end-macro");
+ }
+}
diff --git a/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java b/tests/src/com/android/tradefed/command/remote/RemoteManagerFuncTest.java
similarity index 99%
rename from tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
rename to tests/src/com/android/tradefed/command/remote/RemoteManagerFuncTest.java
index b8e1f50..bef004d 100644
--- a/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
+++ b/tests/src/com/android/tradefed/command/remote/RemoteManagerFuncTest.java
@@ -37,7 +37,7 @@
/**
* Unit tests for {@link RemoteManager}.
*/
-public class RemoteManagerTest extends TestCase {
+public class RemoteManagerFuncTest extends TestCase {
private IDeviceManager mMockDeviceManager;
private RemoteManager mRemoteMgr;
diff --git a/tests/src/com/android/tradefed/config/ArgsOptionParserTest.java b/tests/src/com/android/tradefed/config/ArgsOptionParserTest.java
index af38b77..c53808a 100644
--- a/tests/src/com/android/tradefed/config/ArgsOptionParserTest.java
+++ b/tests/src/com/android/tradefed/config/ArgsOptionParserTest.java
@@ -195,6 +195,8 @@
private String mNullImmutableOption = null;
}
+
+ // SECTION: option update rule validation
/**
* Verify that {@link OptionUpdateRule}s work properly when the update compares to greater-than
* the default value.
@@ -314,32 +316,34 @@
}
}
+
+ // SECTION: tests for #parse(...)
/**
* Test passing an empty argument list for an object that has one option specified.
* <p/>
* Expected that the option field should retain its default value.
*/
- public void testParse_noArg() throws ConfigurationException {
- OneOptionSource object = new OneOptionSource();
- ArgsOptionParser parser = new ArgsOptionParser(object);
- parser.parse(new String[] {});
- assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
- }
+ public void testParse_noArg() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ parser.parse(new String[] {});
+ assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
+ }
- /**
- * Test passing an single argument for an object that has one option specified.
- */
- public void testParse_oneArg() throws ConfigurationException {
- OneOptionSource object = new OneOptionSource();
- ArgsOptionParser parser = new ArgsOptionParser(object);
- final String expectedValue = "set";
- parser.parse(new String[] {"--my_option", expectedValue});
- assertEquals(expectedValue, object.mMyOption);
- }
+ /**
+ * Test passing an single argument for an object that has one option specified.
+ */
+ public void testParse_oneArg() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String expectedValue = "set";
+ parser.parse(new String[] {"--my_option", expectedValue});
+ assertEquals(expectedValue, object.mMyOption);
+ }
- /**
- * Test passing an single argument for an object that has one option specified.
- */
+ /**
+ * Test passing an single argument for an object that has one option specified.
+ */
public void testParse_oneMapArg() throws ConfigurationException {
MapOptionSource object = new MapOptionSource();
ArgsOptionParser parser = new ArgsOptionParser(object);
@@ -546,6 +550,245 @@
}
}
+
+ // SECTION: tests for #parseBestEffort(...)
+ /**
+ * Test passing an single argument for an object that has one option specified.
+ */
+ public void testParseBestEffort_oneArg() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String option = "--my_option";
+ final String value = "set";
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {option, value});
+ assertEquals(value, object.mMyOption);
+ assertEquals(0, leftovers.size());
+ }
+
+ /**
+ * Make sure that overwriting arguments works as expected.
+ */
+ public void testParseBestEffort_oneArg_overwrite() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String option = "--my_option";
+ final String value1 = "set";
+ final String value2 = "game";
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {option, value1, option, value2});
+ assertEquals(value2, object.mMyOption);
+ assertEquals(0, leftovers.size());
+ }
+
+ /**
+ * Test passing a usable argument followed by an unusable one.
+ */
+ public void testParseBestEffort_oneArg_oneLeftover() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String expectedValue = "set";
+ final String leftoverOption = "--no_exist";
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {"--my_option", expectedValue, leftoverOption});
+ assertEquals(expectedValue, object.mMyOption);
+ assertEquals(1, leftovers.size());
+ assertEquals(leftoverOption, leftovers.get(0));
+ }
+
+ /**
+ * Test passing an unusable argument followed by a usable one. Basically verifies that the
+ * parse attempt stops wholesale, and doesn't merely skip the unusable arg.
+ */
+ public void testParseBestEffort_oneLeftover_oneArg() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String goodOption = "--my_option";
+ final String value = "set";
+ final String badOption = "--no_exist";
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {badOption, goodOption, value});
+ assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
+ assertEquals(3, leftovers.size());
+ assertEquals(badOption, leftovers.get(0));
+ assertEquals(goodOption, leftovers.get(1));
+ assertEquals(value, leftovers.get(2));
+ }
+
+ /**
+ * Make sure that parsing stops when a bare option prefix, "--", is encountered. That prefix
+ * should _not_ be returned as one of the leftover args.
+ */
+ public void testParseBestEffort_manualStop() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String goodOption = "--my_option";
+ final String value = "set";
+ final String badOption = "--no_exist";
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {"--", badOption, goodOption, value});
+ assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
+ assertEquals(3, leftovers.size());
+ assertEquals(badOption, leftovers.get(0));
+ assertEquals(goodOption, leftovers.get(1));
+ assertEquals(value, leftovers.get(2));
+ }
+
+ /**
+ * Make sure that parsing stops when a bare word is encountered. Unlike a bare option prefix,
+ * the bare word _should_ be returned as one of the leftover args.
+ */
+ public void testParseBestEffort_bareWord() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String goodOption = "--my_option";
+ final String value = "set";
+ final String badOption = "--no_exist";
+ final String bareWord = "configName";
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {bareWord, badOption, goodOption, value});
+ assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
+ assertEquals(4, leftovers.size());
+ assertEquals(bareWord, leftovers.get(0));
+ assertEquals(badOption, leftovers.get(1));
+ assertEquals(goodOption, leftovers.get(2));
+ assertEquals(value, leftovers.get(3));
+ }
+
+ /**
+ * Make sure that parsing stops when a bare option prefix, "--", is encountered. That prefix
+ * should _not_ be returned as one of the leftover args.
+ */
+ public void testParseBestEffort_oneArg_manualStop() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String option = "--my_option";
+ final String value1 = "set";
+ final String value2 = "game";
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {option, value1, "--", option, value2});
+ assertEquals(value1, object.mMyOption);
+ assertEquals(2, leftovers.size());
+ assertEquals(option, leftovers.get(0));
+ assertEquals(value2, leftovers.get(1));
+ }
+
+ /**
+ * Make sure that parsing stops when a bare word is encountered. Unlike a bare option prefix,
+ * the bare word _should_ be returned as one of the leftover args.
+ */
+ public void testParseBestEffort_oneArg_bareWord() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String option = "--my_option";
+ final String value1 = "set";
+ final String value2 = "game";
+ final String bareWord = "configName";
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {option, value1, bareWord, option, value2});
+ assertEquals(value1, object.mMyOption);
+ assertEquals(3, leftovers.size());
+ assertEquals(bareWord, leftovers.get(0));
+ assertEquals(option, leftovers.get(1));
+ assertEquals(value2, leftovers.get(2));
+ }
+
+ /**
+ * Test passing an single argument for an object that has one option specified.
+ */
+ public void testParseBestEffort_oneArg_twoLeftovers() throws ConfigurationException {
+ OneOptionSource object = new OneOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String expectedValue = "set";
+ final String leftover1 = "--no_exist";
+ final String leftover2 = "--me_neither";
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {"--my_option", expectedValue, leftover1, leftover2});
+ assertEquals(expectedValue, object.mMyOption);
+ assertEquals(2, leftovers.size());
+ assertEquals(leftover1, leftovers.get(0));
+ assertEquals(leftover2, leftovers.get(1));
+ }
+
+ /**
+ * Make sure that map option parsing works as expected.
+ */
+ public void testParseBestEffort_mapOption() throws ConfigurationException {
+ MapOptionSource object = new MapOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String option = "--my_option";
+ final String key = "123"; // Integer is the key type
+ final String value = "true"; // Boolean is the value type
+ final Integer expKey = 123;
+ final Boolean expValue = Boolean.TRUE;
+
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {option, key, value});
+
+ assertEquals(0, leftovers.size());
+ assertNotNull(object.mMyOption);
+ assertEquals(1, object.mMyOption.size());
+ assertTrue(object.mMyOption.containsKey(expKey));
+ assertEquals(expValue, object.mMyOption.get(expKey));
+ }
+
+ /**
+ * Make sure that we backtrack the appropriate amount when a Map option parse fails in the
+ * middle
+ */
+ public void testParseBestEffort_mapOption_missingValue() throws ConfigurationException {
+ MapOptionSource object = new MapOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String option = "--my_option";
+ final String key = "123"; // Integer is the key type
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {option, key});
+ assertTrue(object.mMyOption.isEmpty());
+ assertEquals(2, leftovers.size());
+ assertEquals(option, leftovers.get(0));
+ assertEquals(key, leftovers.get(1));
+ }
+
+ /**
+ * Make sure that we backtrack the appropriate amount when a Map option parse fails in the
+ * middle
+ */
+ public void testParseBestEffort_mapOption_badValue() throws ConfigurationException {
+ MapOptionSource object = new MapOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String option = "--my_option";
+ final String key = "123"; // Integer is the key type
+ final String value = "notBoolean"; // Boolean is the value type
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {option, key, value});
+ assertTrue(object.mMyOption.isEmpty());
+ assertEquals(3, leftovers.size());
+ assertEquals(option, leftovers.get(0));
+ assertEquals(key, leftovers.get(1));
+ assertEquals(value, leftovers.get(2));
+ }
+
+ /**
+ * Make sure that we backtrack the appropriate amount when a Map option parse fails in the
+ * middle
+ */
+ public void testParseBestEffort_mapOption_badKey() throws ConfigurationException {
+ MapOptionSource object = new MapOptionSource();
+ ArgsOptionParser parser = new ArgsOptionParser(object);
+ final String option = "--my_option";
+ final String key = "NotANumber"; // Integer is the key type
+ final String value = "true"; // Boolean is the value type
+ final List<String> leftovers = parser.parseBestEffort(
+ new String[] {option, key, value});
+ assertTrue(object.mMyOption.isEmpty());
+ assertEquals(3, leftovers.size());
+ assertEquals(option, leftovers.get(0));
+ assertEquals(key, leftovers.get(1));
+ assertEquals(value, leftovers.get(2));
+ }
+
+
+ // SECTION: help-related tests
/**
* Test that help text is displayed for all fields
*/
@@ -578,6 +821,8 @@
assertFalse(help.contains(ImportantOptionSource.UNIMPORTANT_OPTION_NAME));
}
+
+ // SECTION: mandatory option tests
public void testMandatoryOption_noDefault() throws Exception {
MandatoryOptionSourceNoDefault object = new MandatoryOptionSourceNoDefault();
ArgsOptionParser parser = new ArgsOptionParser(object);
diff --git a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
index fb37036..5bb5e07 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
@@ -16,6 +16,7 @@
package com.android.tradefed.config;
import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.ConfigurationFactory.ConfigId;
import com.android.tradefed.log.ILeveledLogOutput;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.FileUtil;
@@ -28,7 +29,9 @@
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* Unit tests for {@link ConfigurationFactory}
@@ -82,6 +85,104 @@
assertConfigValid(TEST_CONFIG);
}
+ private Map<String, String> buildMap(String... args) {
+ if ((args.length % 2) != 0) {
+ throw new IllegalArgumentException(String.format(
+ "Expected an even number of args; got %d", args.length));
+ }
+
+ final Map<String, String> map = new HashMap<String, String>(args.length / 2);
+ for (int i = 0; i < args.length; i += 2) {
+ map.put(args[i], args[i + 1]);
+ }
+
+ return map;
+ }
+
+ /**
+ * Make sure that ConfigId behaves in the right way to serve as a hash key
+ */
+ public void testConfigId_equals() {
+ final ConfigId config1a = new ConfigId("one");
+ final ConfigId config1b = new ConfigId("one");
+ final ConfigId config2 = new ConfigId("two");
+ final ConfigId config3a = new ConfigId("one", buildMap("target", "foo"));
+ final ConfigId config3b = new ConfigId("one", buildMap("target", "foo"));
+ final ConfigId config4 = new ConfigId("two", buildMap("target", "bar"));
+
+ assertEquals(config1a, config1b);
+ assertEquals(config3a, config3b);
+
+ // Check for false equivalences, and don't depend on #equals being commutative
+ assertFalse(config1a.equals(config2));
+ assertFalse(config1a.equals(config3a));
+ assertFalse(config1a.equals(config4));
+
+ assertFalse(config2.equals(config1a));
+ assertFalse(config2.equals(config3a));
+ assertFalse(config2.equals(config4));
+
+ assertFalse(config3a.equals(config1a));
+ assertFalse(config3a.equals(config2));
+ assertFalse(config3a.equals(config4));
+
+ assertFalse(config4.equals(config1a));
+ assertFalse(config4.equals(config2));
+ assertFalse(config4.equals(config3a));
+ }
+
+ /**
+ * Make sure that ConfigId behaves in the right way to serve as a hash key
+ */
+ public void testConfigId_hashKey() {
+ final Map<ConfigId, String> map = new HashMap<>();
+ final ConfigId config1a = new ConfigId("one");
+ final ConfigId config1b = new ConfigId("one");
+ final ConfigId config2 = new ConfigId("two");
+ final ConfigId config3a = new ConfigId("one", buildMap("target", "foo"));
+ final ConfigId config3b = new ConfigId("one", buildMap("target", "foo"));
+ final ConfigId config4 = new ConfigId("two", buildMap("target", "bar"));
+
+ // Make sure that keys config1a and config1b behave identically
+ map.put(config1a, "1a");
+ assertEquals("1a", map.get(config1a));
+ assertEquals("1a", map.get(config1b));
+
+ map.put(config1b, "1b");
+ assertEquals("1b", map.get(config1a));
+ assertEquals("1b", map.get(config1b));
+
+ assertFalse(map.containsKey(config2));
+ assertFalse(map.containsKey(config3a));
+ assertFalse(map.containsKey(config4));
+
+ // Make sure that keys config3a and config3b behave identically
+ map.put(config3a, "3a");
+ assertEquals("3a", map.get(config3a));
+ assertEquals("3a", map.get(config3b));
+
+ map.put(config3b, "3b");
+ assertEquals("3b", map.get(config3a));
+ assertEquals("3b", map.get(config3b));
+
+ assertEquals(2, map.size());
+ assertFalse(map.containsKey(config2));
+ assertFalse(map.containsKey(config4));
+
+ // It's unlikely for these to fail if the above tests all passed, but just fill everything
+ // out for completeness
+ map.put(config2, "2");
+ map.put(config4, "4");
+
+ assertEquals(4, map.size());
+ assertEquals("1b", map.get(config1a));
+ assertEquals("1b", map.get(config1b));
+ assertEquals("2", map.get(config2));
+ assertEquals("3b", map.get(config3a));
+ assertEquals("3b", map.get(config3b));
+ assertEquals("4", map.get(config4));
+ }
+
/**
* Test specifying a config xml by file path
*/
@@ -238,7 +339,6 @@
/**
* Test loading a config that includes another config.
- * Also ensure options are set correctly.
*/
public void testCreateConfigurationFromArgs_includeConfig() throws Exception {
IConfiguration config = mFactory.createConfigurationFromArgs(
@@ -252,6 +352,181 @@
}
/**
+ * Test loading a config that uses the "default" attribute of a template-include tag to include
+ * another config.
+ */
+ public void testCreateConfigurationFromArgs_defaultTemplateInclude_default() throws Exception {
+ // The default behavior is to include test-config directly. Nesting is such that innermost
+ // elements come first.
+ IConfiguration config = mFactory.createConfigurationFromArgs(
+ new String[]{"template-include-config-with-default"});
+ assertEquals(2, config.getTests().size());
+ assertTrue(config.getTests().get(0) instanceof StubOptionTest);
+ assertTrue(config.getTests().get(1) instanceof StubOptionTest);
+ StubOptionTest innerConfig = (StubOptionTest) config.getTests().get(0);
+ StubOptionTest outerConfig = (StubOptionTest) config.getTests().get(1);
+ assertEquals("valueFromTestConfig", innerConfig.mOption);
+ assertEquals("valueFromTemplateIncludeWithDefaultConfig", outerConfig.mOption);
+ }
+
+ /**
+ * Test using {@code <include>} to load a config that uses the "default" attribute of a
+ * template-include tag to include a third config.
+ */
+ public void testCreateConfigurationFromArgs_includeTemplateIncludeWithDefault() throws Exception {
+ // The default behavior is to include test-config directly. Nesting is such that innermost
+ // elements come first.
+ IConfiguration config = mFactory.createConfigurationFromArgs(
+ new String[]{"include-template-config-with-default"});
+ assertEquals(3, config.getTests().size());
+ assertTrue(config.getTests().get(0) instanceof StubOptionTest);
+ assertTrue(config.getTests().get(1) instanceof StubOptionTest);
+ assertTrue(config.getTests().get(2) instanceof StubOptionTest);
+ StubOptionTest innerConfig = (StubOptionTest) config.getTests().get(0);
+ StubOptionTest middleConfig = (StubOptionTest) config.getTests().get(1);
+ StubOptionTest outerConfig = (StubOptionTest) config.getTests().get(2);
+ assertEquals("valueFromTestConfig", innerConfig.mOption);
+ assertEquals("valueFromTemplateIncludeWithDefaultConfig", middleConfig.mOption);
+ assertEquals("valueFromIncludeTemplateConfigWithDefault", outerConfig.mOption);
+ }
+
+ /**
+ * Test loading a config that uses the "default" attribute of a template-include tag to include
+ * another config. In this case, we override the default attribute on the commandline.
+ */
+ public void testCreateConfigurationFromArgs_defaultTemplateInclude_alternate() throws Exception {
+ IConfiguration config = mFactory.createConfigurationFromArgs(
+ new String[]{"template-include-config-with-default", "--template:map", "target",
+ "include-config"});
+ assertEquals(3, config.getTests().size());
+ assertTrue(config.getTests().get(0) instanceof StubOptionTest);
+ assertTrue(config.getTests().get(1) instanceof StubOptionTest);
+ assertTrue(config.getTests().get(2) instanceof StubOptionTest);
+
+ StubOptionTest innerConfig = (StubOptionTest) config.getTests().get(0);
+ StubOptionTest middleConfig = (StubOptionTest) config.getTests().get(1);
+ StubOptionTest outerConfig = (StubOptionTest) config.getTests().get(2);
+
+ assertEquals("valueFromTestConfig", innerConfig.mOption);
+ assertEquals("valueFromIncludeConfig", middleConfig.mOption);
+ assertEquals("valueFromTemplateIncludeWithDefaultConfig", outerConfig.mOption);
+ }
+
+ /**
+ * Test loading a config that uses template-include to include another config.
+ */
+ public void testCreateConfigurationFromArgs_templateInclude() throws Exception {
+ IConfiguration config = mFactory.createConfigurationFromArgs(
+ new String[]{"template-include-config", "--template:map", "target",
+ "test-config"});
+ assertTrue(config.getTests().get(0) instanceof StubOptionTest);
+ assertTrue(config.getTests().get(1) instanceof StubOptionTest);
+ StubOptionTest fromTestConfig = (StubOptionTest) config.getTests().get(0);
+ StubOptionTest fromTemplateIncludeConfig = (StubOptionTest) config.getTests().get(1);
+ assertEquals("valueFromTestConfig", fromTestConfig.mOption);
+ assertEquals("valueFromTemplateIncludeConfig", fromTemplateIncludeConfig.mOption);
+ }
+
+ /**
+ * Make sure that we throw a useful error when template-include usage is underspecified.
+ */
+ public void testCreateConfigurationFromArgs_templateInclude_unspecified() throws Exception {
+ final String configName = "template-include-config";
+ try {
+ mFactory.createConfigurationFromArgs(new String[]{configName});
+ fail ("ConfigurationException not thrown");
+ } catch (ConfigurationException e) {
+ // Make sure that we get the expected error message
+ final String msg = e.getMessage();
+ assertNotNull(msg);
+
+ assertTrue(String.format("Error message does not mention the name of the broken " +
+ "config. msg was: %s", msg), msg.contains(configName));
+
+ // Error message should help people to resolve the problem
+ assertTrue(String.format("Error message should help user to resolve the " +
+ "template-include. msg was: %s", msg),
+ msg.contains(String.format("--template:map %s", "target")));
+ assertTrue(String.format("Error message should mention the ability to specify a " +
+ "default resolution. msg was: %s", msg),
+ msg.contains(String.format("'default'", configName)));
+ }
+ }
+
+ /**
+ * Make sure that we throw a useful error when template-include mentions a target configuration
+ * that doesn't exist.
+ */
+ public void testCreateConfigurationFromArgs_templateInclude_missing() throws Exception {
+ final String configName = "template-include-config";
+ final String includeName = "no-exist";
+
+ try {
+ mFactory.createConfigurationFromArgs(
+ new String[]{configName, "--template:map", "target", includeName});
+ fail ("ConfigurationException not thrown");
+ } catch (ConfigurationException e) {
+ // Make sure that we get the expected error message
+ final String msg = e.getMessage();
+ assertNotNull(msg);
+
+ assertTrue(String.format("Error message does not mention the name of the broken " +
+ "config. msg was: %s", msg), msg.contains(configName));
+ assertTrue(String.format("Error message does not mention the name of the missing " +
+ "include target. msg was: %s", msg), msg.contains(includeName));
+ }
+ }
+
+ /**
+ * A limitation of the current implementation is that template args are only passed to the
+ * outermost configuration. This unit test codifies the expectation that an inner
+ * {@code <template-include>} tag that doesn't have a default resolution set will fail.
+ */
+ public void testCreateConfigurationFromArgs_templateInclude_dependent() throws Exception {
+ final String configName = "depend-template-include-config";
+ final String depTargetName = "template-include-config";
+ final String targetName = "test-config";
+ final String expError = String.format(
+ "Failed to parse config xml '%s'. Reason: " +
+ ConfigurationXmlParser.ConfigHandler.INNER_TEMPLATE_INCLUDE_ERROR,
+ configName, configName, depTargetName);
+
+ try {
+ mFactory.createConfigurationFromArgs(new String[]{configName,
+ "--template:map", "dep-target", depTargetName,
+ "--template:map", "target", targetName});
+ fail ("ConfigurationException not thrown");
+ } catch (ConfigurationException e) {
+ // Make sure that we get the expected error message
+ assertEquals(expError, e.getMessage());
+ }
+ }
+
+ /**
+ * A limitation of the current implementation is that template args are only passed to the
+ * outermost configuration. This unit test codifies the expectation that an inner
+ * {@code <template-include>} tag that doesn't have a default resolution set will fail.
+ */
+ public void testCreateConfigurationFromArgs_include_dependent() throws Exception {
+ final String configName = "include-template-config";
+ final String targetName = "test-config";
+ final String failedTargetName = "template-include-config";
+ final String expError = String.format(
+ "Failed to parse config xml '%s'. Reason: " +
+ ConfigurationXmlParser.ConfigHandler.INNER_TEMPLATE_INCLUDE_ERROR,
+ configName, configName, failedTargetName);
+
+ try {
+ mFactory.createConfigurationFromArgs(new String[]{configName,
+ "--template:map", "target", targetName});
+ fail ("ConfigurationException not thrown");
+ } catch (ConfigurationException e) {
+ // Make sure that we get the expected error message
+ assertEquals(expError, e.getMessage());
+ }
+ }
+
+ /**
* Test loading a config that tries to include itself
*/
public void testCreateConfigurationFromArgs_recursiveInclude() throws Exception {
@@ -264,6 +539,18 @@
}
/**
+ * Test loading a config that tries to include a non-bundled config
+ */
+ public void testCreateConfigurationFromArgs_nonBundledInclude() throws Exception {
+ try {
+ mFactory.createConfigurationFromArgs(new String[] {"non-bundled-config"});
+ fail("ConfigurationException not thrown");
+ } catch (ConfigurationException e) {
+ // expected
+ }
+ }
+
+ /**
* Test loading a config that has a circular include
*/
public void testCreateConfigurationFromArgs_circularInclude() throws Exception {
diff --git a/tests/src/com/android/tradefed/config/ConfigurationTest.java b/tests/src/com/android/tradefed/config/ConfigurationTest.java
index bd0f3c9..ad7c3f0 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationTest.java
@@ -32,6 +32,9 @@
import junit.framework.TestCase;
import org.easymock.EasyMock;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
@@ -352,4 +355,74 @@
usageString.contains("serial"));
}
+
+ /**
+ * Basic test for {@link Configuration#getJsonCommandUsage()}.
+ */
+ public void testGetJsonCommandUsage() throws ConfigurationException, JSONException {
+ TestConfigObject testConfigObject = new TestConfigObject();
+ mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
+
+ // General validation of usage elements
+ JSONArray usage = mConfig.getJsonCommandUsage();
+ JSONObject jsonConfigObject = null;
+ for (int i = 0; i < usage.length(); i++) {
+ JSONObject optionClass = usage.getJSONObject(i);
+
+ // Each element should contain 'name', 'class', and 'fields' values
+ assertTrue("Usage element does not contain a 'name' value", optionClass.has("name"));
+ assertTrue("Usage element does not contain a 'class' value", optionClass.has("class"));
+ assertTrue("Usage element does not contain a 'fields' value",
+ optionClass.has("fields"));
+
+ // General validation of each field
+ JSONArray fields = optionClass.getJSONArray("fields");
+ for (int j = 0; j < fields.length(); j++) {
+ JSONObject field = fields.getJSONObject(j);
+
+ // Each field should at least have 'name', 'description', 'mandatory', and
+ // 'javaClass' values
+ assertTrue("Option field does not have a 'name' value", field.has("name"));
+ assertTrue("Option field does not have a 'description' value",
+ field.has("description"));
+ assertTrue("Option field does not have a 'mandatory' value",
+ field.has("mandatory"));
+ assertTrue("Option field does not have a 'javaClass' value",
+ field.has("javaClass"));
+ }
+
+ // The only elements should either be built-in types, or the configuration object we
+ // added.
+ String name = optionClass.getString("name");
+ if (name.equals(CONFIG_OBJECT_TYPE_NAME)) {
+ // The object we added should only appear once
+ assertNull("Duplicate JSON usage element", jsonConfigObject);
+ jsonConfigObject = optionClass;
+ } else {
+ assertTrue(String.format("Unexpected JSON usage element: %s", name),
+ Configuration.isBuiltInObjType(name));
+ }
+ }
+
+ // Verify that the configuration element we added has the expected values
+ assertNotNull("Missing JSON usage element", jsonConfigObject);
+ JSONArray fields = jsonConfigObject.getJSONArray("fields");
+ JSONObject jsonOptionField = null;
+ JSONObject jsonAltOptionField = null;
+ for (int i = 0; i < fields.length(); i++) {
+ JSONObject field = fields.getJSONObject(i);
+
+ if (OPTION_NAME.equals(field.getString("name"))) {
+ assertNull("Duplicate option field", jsonOptionField);
+ jsonOptionField = field;
+ } else if (ALT_OPTION_NAME.equals(field.getString("name"))) {
+ assertNull("Duplication option field", jsonAltOptionField);
+ jsonAltOptionField = field;
+ }
+ }
+ assertNotNull(jsonOptionField);
+ assertEquals(OPTION_DESCRIPTION, jsonOptionField.getString("description"));
+ assertNotNull(jsonAltOptionField);
+ assertEquals(OPTION_DESCRIPTION, jsonAltOptionField.getString("description"));
+ }
}
diff --git a/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java b/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java
index 7614008..5ba3f9e 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java
@@ -49,7 +49,7 @@
"</configuration>";
final String configName = "config";
ConfigurationDef configDef = new ConfigurationDef(configName);
- xmlParser.parse(configDef, configName, getStringAsStream(normalConfig));
+ xmlParser.parse(configDef, configName, getStringAsStream(normalConfig), null);
assertEquals(configName, configDef.getName());
assertEquals("desc", configDef.getDescription());
assertEquals("junit.framework.TestCase", configDef.getObjectClassMap().get("test").get(0));
@@ -69,7 +69,7 @@
"</configuration>";
final String configName = "config";
ConfigurationDef configDef = new ConfigurationDef(configName);
- xmlParser.parse(configDef, configName, getStringAsStream(normalConfig));
+ xmlParser.parse(configDef, configName, getStringAsStream(normalConfig), null);
assertEquals(configName, configDef.getName());
assertEquals("desc", configDef.getDescription());
assertEquals("junit.framework.TestCase", configDef.getObjectClassMap().get("test").get(0));
@@ -93,7 +93,7 @@
"</configuration>";
final String configName = "config";
ConfigurationDef configDef = new ConfigurationDef(configName);
- xmlParser.parse(configDef, configName, getStringAsStream(normalConfig));
+ xmlParser.parse(configDef, configName, getStringAsStream(normalConfig), null);
assertEquals(configName, configDef.getName());
assertEquals("desc", configDef.getDescription());
@@ -114,7 +114,7 @@
final String config =
"<object name=\"foo\" />";
try {
- xmlParser.parse(new ConfigurationDef("foo"), "foo", getStringAsStream(config));
+ xmlParser.parse(new ConfigurationDef("foo"), "foo", getStringAsStream(config), null);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
@@ -128,7 +128,7 @@
final String config =
"<option name=\"foo\" />";
try {
- xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config));
+ xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config), null);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
@@ -142,7 +142,7 @@
final String config =
"<object type=\"foo\" class=\"junit.framework.TestCase\" />";
ConfigurationDef configDef = new ConfigurationDef("name");
- xmlParser.parse(configDef, "name", getStringAsStream(config));
+ xmlParser.parse(configDef, "name", getStringAsStream(config), null);
assertEquals("junit.framework.TestCase", configDef.getObjectClassMap().get("foo").get(0));
}
@@ -151,11 +151,11 @@
*/
public void testParse_include() throws ConfigurationException {
String includedName = "includeme";
- ConfigurationDef configDef = new ConfigurationDef("name");
- mMockLoader.loadIncludedConfiguration(EasyMock.eq(configDef), EasyMock.eq(includedName));
+ ConfigurationDef configDef = new ConfigurationDef("foo");
+ mMockLoader.loadIncludedConfiguration(EasyMock.eq(configDef), EasyMock.eq("foo"), EasyMock.eq(includedName));
EasyMock.replay(mMockLoader);
final String config = "<include name=\"includeme\" />";
- xmlParser.parse(configDef, "name", getStringAsStream(config));
+ xmlParser.parse(configDef, "foo", getStringAsStream(config), null);
}
/**
@@ -165,12 +165,12 @@
String includedName = "non-existent";
ConfigurationDef parent = new ConfigurationDef("name");
ConfigurationException exception = new ConfigurationException("I don't exist");
- mMockLoader.loadIncludedConfiguration(parent, includedName);
+ mMockLoader.loadIncludedConfiguration(parent, "name", includedName);
EasyMock.expectLastCall().andThrow(exception);
EasyMock.replay(mMockLoader);
final String config = String.format("<include name=\"%s\" />", includedName);
try {
- xmlParser.parse(parent, "name", getStringAsStream(config));
+ xmlParser.parse(parent, "name", getStringAsStream(config), null);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
@@ -183,7 +183,7 @@
public void testParse_badTag() throws ConfigurationException {
final String config = "<blah name=\"foo\" />";
try {
- xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config));
+ xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config), null);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
@@ -196,7 +196,7 @@
public void testParse_xml() throws ConfigurationException {
final String config = "blah";
try {
- xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config));
+ xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config), null);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
diff --git a/tests/src/com/android/tradefed/config/OptionSetterTest.java b/tests/src/com/android/tradefed/config/OptionSetterTest.java
index 623ed61..e9edcf9 100644
--- a/tests/src/com/android/tradefed/config/OptionSetterTest.java
+++ b/tests/src/com/android/tradefed/config/OptionSetterTest.java
@@ -17,6 +17,8 @@
package com.android.tradefed.config;
import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.TimeVal;
import junit.framework.TestCase;
@@ -110,6 +112,9 @@
@Option(name = "string_string_map")
private Map<String, String> mStringMap = new HashMap<String, String>();
+ @Option(name = "string_string_multimap")
+ private MultiMap<String, String> mStringMultiMap = new MultiMap<String, String>();
+
@Option(name = "string")
private String mString = null;
@@ -143,6 +148,15 @@
@Option(name = "longObj")
private Long mLongObj = null;
+ @Option(name = "timeValLong", isTimeVal = true)
+ private long mTimeValLong = 0;
+
+ @Option(name = "timeValLongObj", isTimeVal = true)
+ private Long mTimeValLongObj = null;
+
+ @Option(name = "timeVal")
+ private TimeVal mTimeVal = null;
+
@Option(name = "float")
private float mFloat = 0;
@@ -511,6 +525,29 @@
}
/**
+ * Test {@link OptionSetter#setOptionValue(String, String)} for a MultiMap.
+ */
+ public void testSetOptionValue_multimap() throws ConfigurationException, IOException {
+ AllTypesOptionSource optionSource = new AllTypesOptionSource();
+ final String expectedKey = "stringkey";
+ final String expectedValue1 = "stringvalue1";
+ final String expectedValue2 = "stringvalue2";
+
+ // Actually set the key/value pair
+ OptionSetter parser = new OptionSetter(optionSource);
+ parser.setOptionMapValue("string_string_multimap", expectedKey, expectedValue1);
+ parser.setOptionMapValue("string_string_multimap", expectedKey, expectedValue2);
+
+ assertEquals(1, optionSource.mStringMultiMap.size());
+ assertNotNull(optionSource.mStringMultiMap.get(expectedKey));
+
+ Collection<String> values = optionSource.mStringMultiMap.get(expectedKey);
+ assertEquals(2, values.size());
+ assertTrue(values.contains(expectedValue1));
+ assertTrue(values.contains(expectedValue2));
+ }
+
+ /**
* Test {@link OptionSetter#setOptionValue(String, String)} for a boolean.
*/
public void testSetOptionValue_boolean() throws ConfigurationException {
@@ -642,6 +679,41 @@
}
/**
+ * Test {@link OptionSetter#setOptionValue(String, String)} for a long that represents a time
+ * value.
+ */
+ public void testSetOptionValue_timeValLong() throws ConfigurationException {
+ AllTypesOptionSource optionSource = new AllTypesOptionSource();
+ assertSetOptionValue(optionSource, "timeValLong", "2H 45s");
+ assertTrue(1000 * (45 + 60 * 60 * 2) == optionSource.mTimeValLong);
+ assertSetOptionValue(optionSource, "timeValLong", "12345");
+ assertTrue(12345 == optionSource.mTimeValLong);
+ }
+
+ /**
+ * Test {@link OptionSetter#setOptionValue(String, String)} for a Long that represents a time
+ * value.
+ */
+ public void testSetOptionValue_timeValLongObj() throws ConfigurationException {
+ AllTypesOptionSource optionSource = new AllTypesOptionSource();
+ assertSetOptionValue(optionSource, "timeValLongObj", "2H 45s");
+ assertTrue(1000 * (45 + 60 * 60 * 2) == optionSource.mTimeValLongObj);
+ assertSetOptionValue(optionSource, "timeValLongObj", "12345");
+ assertTrue(12345 == optionSource.mTimeValLongObj);
+ }
+
+ /**
+ * Test {@link OptionSetter#setOptionValue(String, String)} for a TimeVal.
+ */
+ public void testSetOptionValue_timeVal() throws ConfigurationException {
+ AllTypesOptionSource optionSource = new AllTypesOptionSource();
+ assertSetOptionValue(optionSource, "timeVal", "2H 45s");
+ assertTrue(1000 * (45 + 60 * 60 * 2) == optionSource.mTimeVal.asLong());
+ assertSetOptionValue(optionSource, "timeVal", "12345");
+ assertTrue(12345 == optionSource.mTimeVal.asLong());
+ }
+
+ /**
* Test {@link OptionSetter#setOptionValue(String, String)} for a float.
*/
public void testSetOptionValue_float() throws ConfigurationException {
diff --git a/tests/src/com/android/tradefed/device/DeviceManagerTest.java b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
index 47b01d1..0c4dfc4 100644
--- a/tests/src/com/android/tradefed/device/DeviceManagerTest.java
+++ b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
@@ -36,7 +36,6 @@
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.Collection;
import java.util.List;
/**
@@ -214,6 +213,10 @@
}
@Override
+ void startDeviceRecoverer() {
+ }
+
+ @Override
IDeviceStateMonitor createStateMonitor(IDevice device) {
return mMockStateMonitor;
}
@@ -305,6 +308,7 @@
.andReturn(new DeviceEventResponse(DeviceAllocationState.Available, true));
EasyMock.expect(mMockTestDevice.handleAllocationEvent(DeviceEvent.ALLOCATE_REQUEST))
.andReturn(new DeviceEventResponse(DeviceAllocationState.Allocated, true));
+ mMockTestDevice.stopEmulatorOutput();
replayMocks();
DeviceManager manager = createDeviceManagerNoInit();
manager.setMaxEmulators(1);
@@ -552,27 +556,6 @@
// TODO: add test for fastboot state changes
/**
- * Verify the 'fastboot devices' output parsing
- */
- public void testParseDevicesOnFastboot() {
- Collection<String> deviceSerials = DeviceManager.parseDevicesOnFastboot(
- "04035EEB0B01F01C fastboot\n" +
- "HT99PP800024 fastboot\n" +
- "???????????? fastboot");
- assertEquals(2, deviceSerials.size());
- assertTrue(deviceSerials.contains("04035EEB0B01F01C"));
- assertTrue(deviceSerials.contains("HT99PP800024"));
- }
-
- /**
- * Verify the 'fastboot devices' output parsing when empty
- */
- public void testParseDevicesOnFastboot_empty() {
- Collection<String> deviceSerials = DeviceManager.parseDevicesOnFastboot("");
- assertEquals(0, deviceSerials.size());
- }
-
- /**
* Test normal success case for {@link DeviceManager#connectToTcpDevice(String)}
*/
public void testConnectToTcpDevice() throws Exception {
diff --git a/tests/src/com/android/tradefed/device/FastbootHelperTest.java b/tests/src/com/android/tradefed/device/FastbootHelperTest.java
new file mode 100644
index 0000000..f61bad5
--- /dev/null
+++ b/tests/src/com/android/tradefed/device/FastbootHelperTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.device;
+
+import com.android.tradefed.util.IRunUtil;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+
+import java.util.Collection;
+
+/**
+ * Unit tests for {@link FastBootHelper}.
+ */
+public class FastbootHelperTest extends TestCase {
+
+ private IRunUtil mMockRunUtil;
+ private FastbootHelper mFastbootHelper;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mMockRunUtil = EasyMock.createMock(IRunUtil.class);
+ mFastbootHelper = new FastbootHelper(mMockRunUtil);
+ }
+
+ /**
+ * Verify the 'fastboot devices' output parsing
+ */
+ public void testParseDevicesOnFastboot() {
+ Collection<String> deviceSerials = mFastbootHelper.parseDevices(
+ "04035EEB0B01F01C fastboot\n" +
+ "HT99PP800024 fastboot\n" +
+ "???????????? fastboot");
+ assertEquals(2, deviceSerials.size());
+ assertTrue(deviceSerials.contains("04035EEB0B01F01C"));
+ assertTrue(deviceSerials.contains("HT99PP800024"));
+ }
+
+ /**
+ * Verify the 'fastboot devices' output parsing when empty
+ */
+ public void testParseDevicesOnFastboot_empty() {
+ Collection<String> deviceSerials = mFastbootHelper.parseDevices("");
+ assertEquals(0, deviceSerials.size());
+ }
+
+}
diff --git a/tests/src/com/android/tradefed/device/StubTestDevice.java b/tests/src/com/android/tradefed/device/StubTestDevice.java
index 28838d7..76f9d2c 100644
--- a/tests/src/com/android/tradefed/device/StubTestDevice.java
+++ b/tests/src/com/android/tradefed/device/StubTestDevice.java
@@ -21,14 +21,12 @@
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.ITestRunListener;
import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.IWifiHelper;
-import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.util.CommandResult;
import java.io.File;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@@ -77,6 +75,13 @@
}
@Override
+ public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
+ Collection<ITestRunListener> listeners) throws DeviceNotAvailableException {
+ // ignore
+ return true;
+ }
+
+ @Override
public boolean runInstrumentationTests(IRemoteAndroidTestRunner runner,
ITestRunListener... listeners) throws DeviceNotAvailableException {
// ignore
@@ -84,6 +89,13 @@
}
@Override
+ public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
+ ITestRunListener... listeners) throws DeviceNotAvailableException {
+ // ignore
+ return true;
+ }
+
+ @Override
public boolean pullFile(String remoteFilePath, File localFile)
throws DeviceNotAvailableException {
return false;
@@ -313,8 +325,8 @@
*/
@Override
public boolean waitForBootComplete(long timeOut) throws DeviceNotAvailableException {
- // ignore
- return false;
+ // ignore
+ return false;
}
/**
@@ -412,6 +424,11 @@
}
@Override
+ public void clearLastConnectedWifiNetwork() {
+ // ignore
+ }
+
+ @Override
public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk)
throws DeviceNotAvailableException {
// ignore
@@ -451,6 +468,7 @@
// ignore
return false;
}
+
/**
* {@inheritDoc}
*/
@@ -464,6 +482,7 @@
/**
* {@inheritDoc}
*/
+ @Override
public boolean checkConnectivity() throws DeviceNotAvailableException {
return false;
}
@@ -500,6 +519,16 @@
* {@inheritDoc}
*/
@Override
+ public String installPackageForUser(File packageFile, boolean reinstall, int userId,
+ String... extraArgs) throws DeviceNotAvailableException {
+ // ignore
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public String uninstallPackage(String packageName) throws DeviceNotAvailableException {
// ignore
return null;
@@ -743,6 +772,7 @@
* {@inheritDoc}
*/
@Override
+ @Deprecated
public String getPropertySync(String name) throws DeviceNotAvailableException {
// ignore
return null;
@@ -860,4 +890,102 @@
@Override
public void setDate(Date date) throws DeviceNotAvailableException {
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isMultiUserSupported() throws DeviceNotAvailableException {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int createUser(String name) throws DeviceNotAvailableException {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean removeUser(int userId) throws DeviceNotAvailableException {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean startUser(int userId) throws DeviceNotAvailableException {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stopUser(int userId) throws DeviceNotAvailableException {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStreamSource getEmulatorOutput() {
+ return new ByteArrayInputStreamSource(new byte[0]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stopEmulatorOutput() {
+ // ignore
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
+ String... extraArgs) throws DeviceNotAvailableException {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String installPackageForUser(File packageFile, boolean reinstall,
+ boolean grantPermissions, int userId, String... extraArgs)
+ throws DeviceNotAvailableException {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void remountSystemWritable() {
+ // no-op
+ }
}
diff --git a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
index 781ab23..a579c2d 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
@@ -18,6 +18,7 @@
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.tradefed.TestAppConstants;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.CollectingTestListener;
@@ -698,6 +699,6 @@
TestAppConstants.UITESTAPP_PACKAGE, getDevice().getIDevice());
CollectingTestListener uilistener = new CollectingTestListener();
getDevice().runInstrumentationTests(uirunner, uilistener);
- return TestAppConstants.UI_TOTAL_TESTS == uilistener.getNumPassedTests();
+ return TestAppConstants.UI_TOTAL_TESTS == uilistener.getNumTestsInState(TestStatus.PASSED);
}
}
diff --git a/tests/src/com/android/tradefed/device/TestDeviceStressTest.java b/tests/src/com/android/tradefed/device/TestDeviceStressTest.java
index 2231635..f54035b 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceStressTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceStressTest.java
@@ -18,6 +18,7 @@
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.tradefed.TestAppConstants;
import com.android.tradefed.config.Option;
import com.android.tradefed.result.CollectingTestListener;
@@ -143,7 +144,7 @@
TestAppConstants.UITESTAPP_PACKAGE, getDevice().getIDevice());
CollectingTestListener uilistener = new CollectingTestListener();
getDevice().runInstrumentationTests(uirunner, uilistener);
- return TestAppConstants.UI_TOTAL_TESTS == uilistener.getNumPassedTests();
+ return TestAppConstants.UI_TOTAL_TESTS == uilistener.getNumTestsInState(TestStatus.PASSED);
}
/**
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index 524874f..7ca4e32 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -22,6 +22,7 @@
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.tradefed.device.ITestDevice.MountPointInfo;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.log.LogUtil.CLog;
@@ -36,6 +37,7 @@
import org.easymock.EasyMock;
import org.easymock.IAnswer;
+import org.easymock.IExpectationSetters;
import java.io.File;
import java.io.IOException;
@@ -44,6 +46,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
@@ -51,6 +54,7 @@
*/
public class TestDeviceTest extends TestCase {
+ private static final String MOCK_DEVICE_SERIAL = "serial";
private IDevice mMockIDevice;
private IShellOutputReceiver mMockReceiver;
private TestDevice mTestDevice;
@@ -92,7 +96,7 @@
protected void setUp() throws Exception {
super.setUp();
mMockIDevice = EasyMock.createMock(IDevice.class);
- EasyMock.expect(mMockIDevice.getSerialNumber()).andReturn("serial").anyTimes();
+ EasyMock.expect(mMockIDevice.getSerialNumber()).andReturn(MOCK_DEVICE_SERIAL).anyTimes();
mMockReceiver = EasyMock.createMock(IShellOutputReceiver.class);
mMockRecovery = EasyMock.createMock(IDeviceRecovery.class);
mMockStateMonitor = EasyMock.createMock(IDeviceStateMonitor.class);
@@ -157,10 +161,7 @@
CommandResult adbResult = new CommandResult();
adbResult.setStatus(CommandStatus.SUCCESS);
adbResult.setStdout("restarting adbd as root");
- EasyMock.expect(
- mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("adb"),
- EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("root"))).andReturn(
- adbResult);
+ setExecuteAdbCommandExpectations(adbResult, "root");
EasyMock.expect(mMockStateMonitor.waitForDeviceNotAvailable(EasyMock.anyLong())).andReturn(
Boolean.TRUE);
EasyMock.expect(mMockStateMonitor.waitForDeviceOnline()).andReturn(
@@ -168,6 +169,19 @@
}
/**
+ * COnfigure EasMock expectations for a successful adb command call
+ * @param command the adb command to execute
+ * @param result the {@link CommandResult} expected from the adb command execution
+ * @throws Exception
+ */
+ private void setExecuteAdbCommandExpectations(CommandResult result, String command)
+ throws Exception {
+ EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(),
+ EasyMock.eq("adb"), EasyMock.eq("-s"), EasyMock.eq(MOCK_DEVICE_SERIAL),
+ EasyMock.eq(command))).andReturn(result);
+ }
+
+ /**
* Test that {@link TestDevice#enableAdbRoot()} reattempts adb root
*/
public void testEnableAdbRoot_rootRetry() throws Exception {
@@ -176,16 +190,10 @@
injectShellResponse("id", "uid=0(root) gid=0(root)");
CommandResult adbBadResult = new CommandResult(CommandStatus.SUCCESS);
adbBadResult.setStdout("");
- EasyMock.expect(
- mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("adb"),
- EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("root"))).andReturn(
- adbBadResult);
+ setExecuteAdbCommandExpectations(adbBadResult, "root");
CommandResult adbResult = new CommandResult(CommandStatus.SUCCESS);
adbResult.setStdout("restarting adbd as root");
- EasyMock.expect(
- mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("adb"),
- EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("root"))).andReturn(
- adbResult);
+ setExecuteAdbCommandExpectations(adbResult, "root");
EasyMock.expect(mMockStateMonitor.waitForDeviceNotAvailable(EasyMock.anyLong())).andReturn(
Boolean.TRUE).times(2);
EasyMock.expect(mMockStateMonitor.waitForDeviceOnline()).andReturn(
@@ -289,9 +297,7 @@
public void testGetProductType_adb() throws Exception {
EasyMock.expect(mMockIDevice.getProperty("ro.hardware")).andReturn(null);
final String expectedOutput = "nexusone";
- SettableFuture<String> f = SettableFuture.create();
- f.set(expectedOutput);
- EasyMock.expect(mMockIDevice.getSystemProperty("ro.hardware")).andReturn(f);
+ injectSystemProperty("ro.hardware", expectedOutput);
EasyMock.replay(mMockIDevice);
assertEquals(expectedOutput, mTestDevice.getProductType());
}
@@ -302,11 +308,7 @@
*/
public void testGetProductType_adbFail() throws Exception {
EasyMock.expect(mMockIDevice.getProperty(EasyMock.<String>anyObject())).andStubReturn(null);
- SettableFuture<String> f = SettableFuture.create();
- f.set(null);
- EasyMock.expect(mMockIDevice.getSystemProperty("ro.hardware"))
- .andReturn(f)
- .times(3);
+ injectSystemProperty("ro.hardware", null).times(3);
EasyMock.replay(mMockIDevice);
try {
mTestDevice.getProductType();
@@ -544,6 +546,18 @@
/**
* Unit test for {@link TestDevice#getExternalStoreFreeSpace()}.
* <p/>
+ * Verify that the coreutils-like output of 'adb shell df' command is parsed correctly.
+ */
+ public void testGetExternalStoreFreeSpace_toybox() throws Exception {
+ final String dfOutput =
+ "Filesystem 1K-blocks Used Available Use% Mounted on\n" +
+ "/dev/fuse 11585536 1316348 10269188 12% /mnt/sdcard";
+ assertGetExternalStoreFreeSpace(dfOutput, 10269188);
+ }
+
+ /**
+ * Unit test for {@link TestDevice#getExternalStoreFreeSpace()}.
+ * <p/>
* Verify behavior when 'df' command returns unexpected content
*/
public void testGetExternalStoreFreeSpace_badOutput() throws Exception {
@@ -705,7 +719,7 @@
}
};
EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
- EasyMock.eq("-s"),EasyMock.eq("serial"), EasyMock.eq("foo"))).andAnswer(
+ EasyMock.eq("-s"),EasyMock.eq(MOCK_DEVICE_SERIAL), EasyMock.eq("foo"))).andAnswer(
blockResult);
// expect
@@ -751,15 +765,16 @@
public void testExecuteFastbootCommand_recovery() throws UnsupportedOperationException,
DeviceNotAvailableException {
CommandResult result = new CommandResult(CommandStatus.EXCEPTION);
- EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
- EasyMock.eq("-s"),EasyMock.eq("serial"), EasyMock.eq("foo"))).andReturn(result);
+ EasyMock.expect(mMockRunUtil.runTimedCmd(
+ EasyMock.anyLong(), EasyMock.eq("fastboot"), EasyMock.eq("-s"),
+ EasyMock.eq(MOCK_DEVICE_SERIAL), EasyMock.eq("foo"))).andReturn(result);
mMockRecovery.recoverDeviceBootloader((IDeviceStateMonitor)EasyMock.anyObject());
CommandResult successResult = new CommandResult(CommandStatus.SUCCESS);
successResult.setStderr("");
successResult.setStdout("");
// now expect a successful retry
EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
- EasyMock.eq("-s"),EasyMock.eq("serial"), EasyMock.eq("foo"))).andReturn(
+ EasyMock.eq("-s"),EasyMock.eq(MOCK_DEVICE_SERIAL), EasyMock.eq("foo"))).andReturn(
successResult);
replayMocks();
mTestDevice.executeFastbootCommand("foo");
@@ -814,9 +829,7 @@
* Simple test for {@link TestDevice#switchToAdbUsb()}
*/
public void testSwitchToAdbUsb() throws Exception {
- EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("adb"),
- EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("usb"))).andReturn(
- new CommandResult(CommandStatus.SUCCESS));
+ setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "usb");
replayMocks();
mTestDevice.switchToAdbUsb();
verifyMocks();
@@ -870,6 +883,259 @@
}
/**
+ * Test that isRuntimePermissionSupported returns correct result for device reporting LRX22F
+ * build attributes
+ * @throws Exception
+ */
+ public void testRuntimePermissionSupportedLmpRelease() throws Exception {
+ injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "REL");
+ injectSystemProperty(TestDevice.BUILD_ID_PROP, "1642709");
+ replayMocks();
+ assertFalse(mTestDevice.isRuntimePermissionSupported());
+ }
+
+ /**
+ * Test that isRuntimePermissionSupported returns correct result for device reporting LMP MR1
+ * dev build attributes
+ * @throws Exception
+ */
+ public void testRuntimePermissionSupportedLmpMr1Dev() throws Exception {
+ injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "REL");
+ injectSystemProperty(TestDevice.BUILD_ID_PROP, "1844090");
+ replayMocks();
+ assertFalse(mTestDevice.isRuntimePermissionSupported());
+ }
+
+ /**
+ * Test that isRuntimePermissionSupported returns correct result for device reporting random
+ * dev build attributes
+ * @throws Exception
+ */
+ public void testRuntimePermissionSupportedRandom1() throws Exception {
+ injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "YADDA");
+ injectSystemProperty(TestDevice.BUILD_ID_PROP, "XYZ");
+ replayMocks();
+ assertFalse(mTestDevice.isRuntimePermissionSupported());
+ }
+
+ /**
+ * Test that isRuntimePermissionSupported returns correct result for device reporting random
+ * mnc dev build attributes
+ * @throws Exception
+ */
+ public void testRuntimePermissionSupportedMncLocal() throws Exception {
+ injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "MNC");
+ injectSystemProperty(TestDevice.BUILD_ID_PROP, "eng.foo.20150414.190304");
+ replayMocks();
+ assertTrue(mTestDevice.isRuntimePermissionSupported());
+ }
+
+ /**
+ * Test that isRuntimePermissionSupported returns correct result for device reporting random
+ * dev build attributes
+ * @throws Exception
+ */
+ public void testRuntimePermissionSupportedNonMncLocal() throws Exception {
+ injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "LMP");
+ injectSystemProperty(TestDevice.BUILD_ID_PROP, "eng.foo.20150414.190304");
+ replayMocks();
+ assertFalse(mTestDevice.isRuntimePermissionSupported());
+ }
+
+ /**
+ * Test that isRuntimePermissionSupported returns correct result for device reporting early MNC
+ * dev build attributes
+ * @throws Exception
+ */
+ public void testRuntimePermissionSupportedEarlyMnc() throws Exception {
+ setMockIDeviceRuntimePermissionNotSupported();
+ replayMocks();
+ assertFalse(mTestDevice.isRuntimePermissionSupported());
+ }
+
+ /**
+ * Test that isRuntimePermissionSupported returns correct result for device reporting early MNC
+ * dev build attributes
+ * @throws Exception
+ */
+ public void testRuntimePermissionSupportedMncPostSwitch() throws Exception {
+ setMockIDeviceRuntimePermissionSupported();
+ replayMocks();
+ assertTrue(mTestDevice.isRuntimePermissionSupported());
+ }
+
+ /**
+ * Convenience method for setting up mMockIDevice to not support runtime permission
+ */
+ private void setMockIDeviceRuntimePermissionNotSupported() {
+ injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "MNC");
+ injectSystemProperty(TestDevice.BUILD_ID_PROP, "1816412");
+ }
+
+ /**
+ * Convenience method for setting up mMockIDevice to support runtime permission
+ */
+ private void setMockIDeviceRuntimePermissionSupported() {
+ injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "MNC");
+ injectSystemProperty(TestDevice.BUILD_ID_PROP, "1844452");
+ }
+
+ /**
+ * Test default installPackage on device not supporting runtime permission has expected
+ * list of args
+ * @throws Exception
+ */
+ public void testInstallPackage_default_runtimePermissionNotSupported() throws Exception {
+ final String apkFile = "foo.apk";
+ setMockIDeviceRuntimePermissionNotSupported();
+ EasyMock.expect(mMockIDevice.installPackage(
+ EasyMock.contains(apkFile), EasyMock.eq(true))).andReturn(null);
+ replayMocks();
+ assertNull(mTestDevice.installPackage(new File(apkFile), true));
+ }
+
+ /**
+ * Test default installPackage on device supporting runtime permission has expected list of args
+ * @throws Exception
+ */
+ public void testInstallPackage_default_runtimePermissionSupported() throws Exception {
+ final String apkFile = "foo.apk";
+ setMockIDeviceRuntimePermissionSupported();
+ EasyMock.expect(mMockIDevice.installPackage(
+ EasyMock.contains(apkFile), EasyMock.eq(true), EasyMock.eq("-g"))).andReturn(null);
+ replayMocks();
+ assertNull(mTestDevice.installPackage(new File(apkFile), true));
+ }
+
+ /**
+ * Test default installPackageForUser on device not supporting runtime permission has expected
+ * list of args
+ * @throws Exception
+ */
+ public void testinstallPackageForUser_default_runtimePermissionNotSupported() throws Exception {
+ final String apkFile = "foo.apk";
+ int uid = 123;
+ setMockIDeviceRuntimePermissionNotSupported();
+ EasyMock.expect(mMockIDevice.installPackage(
+ EasyMock.contains(apkFile), EasyMock.eq(true),
+ EasyMock.eq("--user"), EasyMock.eq(Integer.toString(uid)))).andReturn(null);
+ replayMocks();
+ assertNull(mTestDevice.installPackageForUser(new File(apkFile), true, uid));
+ }
+
+ /**
+ * Test default installPackageForUser on device supporting runtime permission has expected
+ * list of args
+ * @throws Exception
+ */
+ public void testinstallPackageForUser_default_runtimePermissionSupported() throws Exception {
+ final String apkFile = "foo.apk";
+ int uid = 123;
+ setMockIDeviceRuntimePermissionSupported();
+ EasyMock.expect(mMockIDevice.installPackage(
+ EasyMock.contains(apkFile), EasyMock.eq(true), EasyMock.eq("-g"),
+ EasyMock.eq("--user"), EasyMock.eq(Integer.toString(uid)))).andReturn(null);
+ replayMocks();
+ assertNull(mTestDevice.installPackageForUser(new File(apkFile), true, uid));
+ }
+
+ /**
+ * Test runtime permission variant of installPackage throws exception on unsupported device
+ * platform
+ * @throws Exception
+ */
+ public void testInstallPackage_throw() throws Exception {
+ final String apkFile = "foo.apk";
+ setMockIDeviceRuntimePermissionNotSupported();
+ replayMocks();
+ try {
+ mTestDevice.installPackage(new File(apkFile), true, true);
+ } catch (UnsupportedOperationException uoe) {
+ // ignore, exception thrown here is expected
+ return;
+ }
+ fail("installPackage did not throw IllegalArgumentException");
+ }
+
+ /**
+ * Test runtime permission variant of installPackage has expected list of args on a supported
+ * device when granting
+ * @throws Exception
+ */
+ public void testInstallPackage_grant_runtimePermissionSupported() throws Exception {
+ final String apkFile = "foo.apk";
+ setMockIDeviceRuntimePermissionSupported();
+ EasyMock.expect(mMockIDevice.installPackage(
+ EasyMock.contains(apkFile), EasyMock.eq(true), EasyMock.eq("-g"))).andReturn(null);
+ replayMocks();
+ assertNull(mTestDevice.installPackage(new File(apkFile), true, true));
+ }
+
+ /**
+ * Test runtime permission variant of installPackage has expected list of args on a supported
+ * device when not granting
+ * @throws Exception
+ */
+ public void testInstallPackage_noGrant_runtimePermissionSupported() throws Exception {
+ final String apkFile = "foo.apk";
+ setMockIDeviceRuntimePermissionSupported();
+ EasyMock.expect(mMockIDevice.installPackage(
+ EasyMock.contains(apkFile), EasyMock.eq(true))).andReturn(null);
+ replayMocks();
+ assertNull(mTestDevice.installPackage(new File(apkFile), true, false));
+ }
+
+ /**
+ * Test grant permission variant of installPackageForUser throws exception on unsupported
+ * device platform
+ * @throws Exception
+ */
+ public void testInstallPackageForUser_throw() throws Exception {
+ final String apkFile = "foo.apk";
+ setMockIDeviceRuntimePermissionNotSupported();
+ replayMocks();
+ try {
+ mTestDevice.installPackageForUser(new File(apkFile), true, true, 123);
+ } catch (UnsupportedOperationException uoe) {
+ // ignore, exception thrown here is expected
+ return;
+ }
+ fail("installPackage did not throw IllegalArgumentException");
+ }
+
+ /**
+ * Test grant permission variant of installPackageForUser has expected list of args on a
+ * supported device when granting
+ * @throws Exception
+ */
+ public void testInstallPackageForUser_grant_runtimePermissionSupported() throws Exception {
+ final String apkFile = "foo.apk";
+ int uid = 123;
+ setMockIDeviceRuntimePermissionSupported();
+ EasyMock.expect(mMockIDevice.installPackage(
+ EasyMock.contains(apkFile), EasyMock.eq(true), EasyMock.eq("-g"),
+ EasyMock.eq("--user"), EasyMock.eq(Integer.toString(uid)))).andReturn(null);
+ replayMocks();
+ assertNull(mTestDevice.installPackageForUser(new File(apkFile), true, true, uid));
+ }
+
+ /**
+ * Test grant permission variant of installPackageForUser has expected list of args on a
+ * supported device when not granting
+ * @throws Exception
+ */
+ public void testInstallPackageForUser_noGrant_runtimePermissionSupported() throws Exception {
+ final String apkFile = "foo.apk";
+ int uid = 123;
+ setMockIDeviceRuntimePermissionSupported();
+ EasyMock.expect(mMockIDevice.installPackage(
+ EasyMock.contains(apkFile), EasyMock.eq(true),
+ EasyMock.eq("--user"), EasyMock.eq(Integer.toString(uid)))).andReturn(null);
+ replayMocks();
+ assertNull(mTestDevice.installPackageForUser(new File(apkFile), true, false, uid));
+ }
+
+ /**
* Helper method to build a response to a executeShellCommand call
*
* @param expectedCommand the shell command to expect or null to skip verification of command
@@ -887,7 +1153,6 @@
* @param response the response to simulate
* @param asStub whether to set a single expectation or a stub expectation
*/
- @SuppressWarnings("unchecked")
private void injectShellResponse(final String expectedCommand, final String response,
boolean asStub) throws Exception {
IAnswer<Object> shellAnswer = new IAnswer<Object>() {
@@ -918,15 +1183,37 @@
}
/**
- * Test normal success case for {@link TestDevice#reboot()}
+ * Helper method to inject a response to {@link TestDevice#getProperty(String)} calls
+ * @param property property name
+ * @param value property value
+ * @return preset {@link IExpectationSetters} returned by {@link EasyMock} where further
+ * expectations can be added
*/
- public void testReboot() throws Exception {
+ private IExpectationSetters<Future<String>> injectSystemProperty(
+ final String property, final String value) {
+ SettableFuture<String> valueResponse = SettableFuture.create();
+ valueResponse.set(value);
+ return EasyMock.expect(mMockIDevice.getSystemProperty(property)).andReturn(valueResponse);
+ }
+
+ /**
+ * Helper method to build response to a reboot call
+ * @throws Exception
+ */
+ private void setRebootExpectations() throws Exception {
EasyMock.expect(mMockStateMonitor.waitForDeviceOnline()).andReturn(
mMockIDevice);
setEnableAdbRootExpectations();
setEncryptedUnsupportedExpectations();
EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable(EasyMock.anyLong())).andReturn(
mMockIDevice);
+ }
+
+ /**
+ * Test normal success case for {@link TestDevice#reboot()}
+ */
+ public void testReboot() throws Exception {
+ setRebootExpectations();
replayMocks();
mTestDevice.reboot();
verifyMocks();
@@ -1143,7 +1430,7 @@
* Simple test for {@link TestDevice#handleAllocationEvent(DeviceEvent)}
*/
public void testHandleAllocationEvent() {
- EasyMock.expect(mMockIDevice.getSerialNumber()).andStubReturn("serial");
+ EasyMock.expect(mMockIDevice.getSerialNumber()).andStubReturn(MOCK_DEVICE_SERIAL);
EasyMock.replay(mMockIDevice);
assertEquals(DeviceAllocationState.Unknown, mTestDevice.getAllocationState());
@@ -1169,5 +1456,283 @@
assertNotNull(mTestDevice.handleAllocationEvent(DeviceEvent.FREE_UNKNOWN));
assertEquals(DeviceAllocationState.Unknown, mTestDevice.getAllocationState());
}
+
+ public void testGetPingLoss() throws Exception {
+ final String pingCommand = "ping -c 1 -w 5 -s 1024 www.google.com";
+ injectShellResponse(pingCommand, ArrayUtil.join("\r\n",
+ "PING www.google.com (0.0.0.0) 1024(1052) bytes of data.",
+ "1032 bytes from www.google.com (0.0.0.0):" +
+ "icmp_seq=1 ttl=52 time=74.1 ms",
+ "",
+ "--- www.google.com ping statistics ---",
+ "2 packets transmitted, 1 received, 50% packet loss, time 0ms"
+ ));
+ replayMocks();
+ assertEquals(50, mTestDevice.getPingLoss());
+ }
+
+ public void testCheckConnectivity() throws Exception {
+ final String pingCommand = "ping -c 1 -w 5 -s 1024 www.google.com";
+ injectShellResponse(pingCommand, ArrayUtil.join("\r\n",
+ "PING www.google.com (0.0.0.0) 1024(1052) bytes of data.",
+ "1032 bytes from www.google.com (0.0.0.0):" +
+ "icmp_seq=1 ttl=52 time=74.1 ms",
+ "",
+ "--- www.google.com ping statistics ---",
+ "1 packets transmitted, 1 received, 0% packet loss, time 0ms"
+ ));
+ replayMocks();
+ assertTrue(mTestDevice.checkConnectivity());
+ }
+
+ public void testCheckConnectivity_NoConnectivity() throws Exception {
+ final String pingCommand = "ping -c 1 -w 5 -s 1024 www.google.com";
+ injectShellResponse(pingCommand, ArrayUtil.join("\r\n",
+ "PING www.google.com (0.0.0.0) 1024(1052) bytes of data.",
+ "",
+ "--- www.google.com ping statistics ---",
+ "1 packets transmitted, 0 received, 100% packet loss, time 0ms"
+ ));
+ replayMocks();
+ assertFalse(mTestDevice.checkConnectivity());
+ }
+
+ /**
+ * Test that a single user is handled by {@link TestDevice#listUsers()}.
+ */
+ public void testListUsers_oneUser() throws Exception {
+ final String listUsersCommand = "pm list users";
+ injectShellResponse(listUsersCommand, ArrayUtil.join("\r\n",
+ "Users:",
+ "UserInfo{0:Foo:13} running"));
+ replayMocks();
+ ArrayList<Integer> actual = mTestDevice.listUsers();
+ assertNotNull(actual);
+ assertEquals(1, actual.size());
+ assertEquals(0, actual.get(0).intValue());
+ }
+
+ /**
+ * Test that invalid output is handled by {@link TestDevice#listUsers()}.
+ */
+ public void testListUsers_invalidOutput() throws Exception {
+ final String listUsersCommand = "pm list users";
+ injectShellResponse(listUsersCommand, "not really what we are looking for");
+ replayMocks();
+ ArrayList<Integer> actual = mTestDevice.listUsers();
+ assertNull(actual);
+ }
+
+ /**
+ * Test that multiple user is handled by {@link TestDevice#listUsers()}.
+ */
+ public void testListUsers_multiUsers() throws Exception {
+ final String listUsersCommand = "pm list users";
+ injectShellResponse(listUsersCommand, ArrayUtil.join("\r\n",
+ "Users:",
+ "UserInfo{0:Foo:13} running",
+ "UserInfo{3:FooBar:14}"));
+ replayMocks();
+ ArrayList<Integer> actual = mTestDevice.listUsers();
+ assertNotNull(actual);
+ assertEquals(2, actual.size());
+ assertEquals(0, actual.get(0).intValue());
+ assertEquals(3, actual.get(1).intValue());
+ }
+
+ /**
+ * Test that multi user output is handled by {@link TestDevice#getMaxNumberOfUsersSupported()}.
+ */
+ public void testMaxNumberOfUsersSupported() throws Exception {
+ final String getMaxUsersCommand = "pm get-max-users";
+ injectShellResponse(getMaxUsersCommand, "Maximum supported users: 4");
+ replayMocks();
+ assertEquals(4, mTestDevice.getMaxNumberOfUsersSupported());
+ }
+
+ /**
+ * Test that invalid output is handled by {@link TestDevice#getMaxNumberOfUsersSupported()}.
+ */
+ public void testMaxNumberOfUsersSupported_invalid() throws Exception {
+ final String getMaxUsersCommand = "pm get-max-users";
+ injectShellResponse(getMaxUsersCommand, "not the output we expect");
+ replayMocks();
+ assertEquals(0, mTestDevice.getMaxNumberOfUsersSupported());
+ }
+
+ /**
+ * Test that single user output is handled by {@link TestDevice#getMaxNumberOfUsersSupported()}.
+ */
+ public void testIsMultiUserSupported_singleUser() throws Exception {
+ final String getMaxUsersCommand = "pm get-max-users";
+ injectShellResponse(getMaxUsersCommand, "Maximum supported users: 1");
+ replayMocks();
+ assertFalse(mTestDevice.isMultiUserSupported());
+ }
+
+ /**
+ * Test that {@link TestDevice#isMultiUserSupported()} works.
+ */
+ public void testIsMultiUserSupported() throws Exception {
+ final String getMaxUsersCommand = "pm get-max-users";
+ injectShellResponse(getMaxUsersCommand, "Maximum supported users: 4");
+ replayMocks();
+ assertTrue(mTestDevice.isMultiUserSupported());
+ }
+
+ /**
+ * Test that invalid output is handled by {@link TestDevice#isMultiUserSupported()}.
+ */
+ public void testIsMultiUserSupported_invalidOutput() throws Exception {
+ final String getMaxUsersCommand = "pm get-max-users";
+ injectShellResponse(getMaxUsersCommand, "not the output we expect");
+ replayMocks();
+ assertFalse(mTestDevice.isMultiUserSupported());
+ }
+
+ /**
+ * Test that successful user creation is handled by {@link TestDevice#createUser()}.
+ */
+ public void testCreateUser() throws Exception {
+ final String createUserCommand = "pm create-user foo";
+ injectShellResponse(createUserCommand, "Success: created user id 10");
+ replayMocks();
+ assertEquals(10, mTestDevice.createUser("foo"));
+ }
+
+ /**
+ * Test that a failure to create a user is handled by {@link TestDevice#createUser()}.
+ */
+ public void testCreateUser_failed() throws Exception {
+ final String createUserCommand = "pm create-user foo";
+ injectShellResponse(createUserCommand, "Error");
+ replayMocks();
+ try {
+ mTestDevice.createUser("foo");
+ fail("IllegalStateException not thrown");
+ } catch (IllegalStateException e) {
+ // Expected
+ }
+ }
+
+ /**
+ * Test that successful user removal is handled by {@link TestDevice#removeUser()}.
+ */
+ public void testRemoveUser() throws Exception {
+ final String removeUserCommand = "pm remove-user 10";
+ injectShellResponse(removeUserCommand, "Success: removed user\n");
+ replayMocks();
+ assertTrue(mTestDevice.removeUser(10));
+ }
+
+ /**
+ * Test that a failure to remove a user is handled by {@link TestDevice#removeUser()}.
+ */
+ public void testRemoveUser_failed() throws Exception {
+ final String removeUserCommand = "pm remove-user 10";
+ injectShellResponse(removeUserCommand, "Error: couldn't remove user id 10");
+ replayMocks();
+ assertFalse(mTestDevice.removeUser(10));
+ }
+
+ /**
+ * Test that trying to run a test with a user with
+ * {@link TestDevice#runInstrumentationTestsAsUser(IRemoteAndroidTestRunner, int, Collection)}
+ * fails if the {@link IRemoteAndroidTestRunner} is not an instance of
+ * {@link RemoteAndroidTestRunner}.
+ */
+ public void testrunInstrumentationTestsAsUser_failed() throws Exception {
+ IRemoteAndroidTestRunner mockRunner = EasyMock.createMock(IRemoteAndroidTestRunner.class);
+ EasyMock.expect(mockRunner.getPackageName()).andStubReturn("com.example");
+ Collection<ITestRunListener> listeners = new ArrayList<ITestRunListener>();
+ EasyMock.replay(mockRunner);
+ try {
+ mTestDevice.runInstrumentationTestsAsUser(mockRunner, 12, listeners);
+ fail("IllegalStateException not thrown.");
+ } catch (IllegalStateException e) {
+ //expected
+ }
+ }
+
+ /**
+ * Test that successful user start is handled by {@link TestDevice#startUser()}.
+ */
+ public void testStartUser() throws Exception {
+ final String startUserCommand = "am start-user 10";
+ injectShellResponse(startUserCommand, "Success: user started\n");
+ replayMocks();
+ assertTrue(mTestDevice.startUser(10));
+ }
+
+ /**
+ * Test that a failure to start user is handled by {@link TestDevice#startUser()}.
+ */
+ public void testStartUser_failed() throws Exception {
+ final String startUserCommand = "am start-user 10";
+ injectShellResponse(startUserCommand, "Error: could not start user\n");
+ replayMocks();
+ assertFalse(mTestDevice.startUser(10));
+ }
+
+ /**
+ * Test that remount works as expected on a device not supporting dm verity
+ * @throws Exception
+ */
+ public void testRemount_verityUnsupported() throws Exception {
+ injectSystemProperty("partition.system.verified", "");
+ setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+ EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+ replayMocks();
+ mTestDevice.remountSystemWritable();
+ verifyMocks();
+ }
+
+ /**
+ * Test that remount works as expected on a device supporting dm verity v1
+ * @throws Exception
+ */
+ public void testRemount_veritySupportedV1() throws Exception {
+ injectSystemProperty("partition.system.verified", "1");
+ setExecuteAdbCommandExpectations(
+ new CommandResult(CommandStatus.SUCCESS), "disable-verity");
+ setRebootExpectations();
+ setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+ EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+ replayMocks();
+ mTestDevice.remountSystemWritable();
+ verifyMocks();
+ }
+
+ /**
+ * Test that remount works as expected on a device supporting dm verity v2
+ * @throws Exception
+ */
+ public void testRemount_veritySupportedV2() throws Exception {
+ injectSystemProperty("partition.system.verified", "2");
+ setExecuteAdbCommandExpectations(
+ new CommandResult(CommandStatus.SUCCESS), "disable-verity");
+ setRebootExpectations();
+ setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+ EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+ replayMocks();
+ mTestDevice.remountSystemWritable();
+ verifyMocks();
+ }
+
+ /**
+ * Test that remount works as expected on a device supporting dm verity but with unknown version
+ * @throws Exception
+ */
+ public void testRemount_veritySupportedNonNumerical() throws Exception {
+ injectSystemProperty("partition.system.verified", "foo");
+ setExecuteAdbCommandExpectations(
+ new CommandResult(CommandStatus.SUCCESS), "disable-verity");
+ setRebootExpectations();
+ setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+ EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+ replayMocks();
+ mTestDevice.remountSystemWritable();
+ verifyMocks();
+ }
}
diff --git a/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java b/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java
index c4fa088..b44e052 100644
--- a/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java
+++ b/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java
@@ -65,6 +65,7 @@
EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
EasyMock.expect(mMockMonitor.waitForDeviceAvailable(EasyMock.anyLong())).andReturn(
mMockDevice);
+ EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(mMockDevice);
replayMocks();
mRecovery.recoverDevice(mMockMonitor, false);
verifyMocks();
@@ -133,6 +134,7 @@
EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
EasyMock.expect(mMockMonitor.waitForDeviceAvailable(EasyMock.anyLong())).andReturn(
mMockDevice);
+ EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(mMockDevice);
replayMocks();
mRecovery.recoverDevice(mMockMonitor, false);
verifyMocks();
@@ -152,6 +154,10 @@
Boolean.TRUE);
EasyMock.expect(mMockMonitor.waitForDeviceBootloader(EasyMock.anyLong())).andReturn(
Boolean.TRUE).times(2);
+ EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
+ EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("getvar"),
+ EasyMock.eq("product"))).
+ andReturn(new CommandResult(CommandStatus.SUCCESS));
replayMocks();
mRecovery.recoverDeviceBootloader(mMockMonitor);
verifyMocks();
@@ -174,6 +180,10 @@
Boolean.TRUE);
EasyMock.expect(mMockMonitor.waitForDeviceBootloader(EasyMock.anyLong())).andReturn(
Boolean.TRUE).times(2);
+ EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
+ EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("getvar"),
+ EasyMock.eq("product"))).
+ andReturn(new CommandResult(CommandStatus.SUCCESS));
replayMocks();
mRecovery.recoverDeviceBootloader(mMockMonitor);
verifyMocks();
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index ecfe742..3deeb0e 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -341,9 +341,8 @@
mMockPreparer.setUp(mMockDevice, mMockBuildInfo);
EasyMock.expectLastCall().andThrow(exception);
setupMockFailureListeners(exception);
-
EasyMock.expect(mMockDevice.getBugreport())
- .andReturn(new ByteArrayInputStreamSource(new byte[0]));
+ .andReturn(new ByteArrayInputStreamSource(new byte[0]));
setupInvokeWithBuild();
replayMocks(test);
mTestInvocation.invoke(mMockDevice, mStubConfiguration, new StubRescheduler());
@@ -365,6 +364,7 @@
EasyMock.expect(mMockBuildProvider.getBuild()).andReturn(mMockBuildInfo);
resumeListener.invocationStarted(mMockBuildInfo);
+ mMockDevice.clearLastConnectedWifiNetwork();
mMockDevice.setOptions((TestDeviceOptions)EasyMock.anyObject());
mMockBuildInfo.setDeviceSerial(SERIAL);
mMockDevice.startLogcat();
@@ -397,11 +397,13 @@
EasyMock.expect(mockRescheduler.scheduleConfig(EasyMock.capture(capturedConfig)))
.andReturn(Boolean.TRUE);
mMockBuildProvider.cleanUp(mMockBuildInfo);
+ mMockDevice.clearLastConnectedWifiNetwork();
mMockDevice.stopLogcat();
mMockLogger.init();
mMockLogSaver.invocationStarted(mMockBuildInfo);
// now set resumed invocation expectations
+ mMockDevice.clearLastConnectedWifiNetwork();
mMockDevice.setOptions((TestDeviceOptions)EasyMock.anyObject());
mMockBuildInfo.setDeviceSerial(SERIAL);
mMockDevice.startLogcat();
@@ -428,6 +430,7 @@
EasyMock.expect(resumeListener.getSummary()).andReturn(null);
mMockBuildInfo.cleanUp();
mMockLogger.closeLog();
+ mMockDevice.clearLastConnectedWifiNetwork();
mMockDevice.stopLogcat();
EasyMock.replay(mockRescheduler, resumeListener, resumableTest, mMockPreparer,
@@ -499,13 +502,14 @@
EasyMock.expectLastCall().andThrow(exception);
ITargetCleaner mockCleaner = EasyMock.createMock(ITargetCleaner.class);
mockCleaner.setUp(mMockDevice, mMockBuildInfo);
- // tearDown should NOT be called
- // mockCleaner.tearDown(mMockDevice, mMockBuildInfo, null);
+ EasyMock.expectLastCall();
+ mockCleaner.tearDown(mMockDevice, mMockBuildInfo, exception);
+ EasyMock.expectLastCall();
+ EasyMock.replay(mockCleaner);
mStubConfiguration.getTargetPreparers().add(mockCleaner);
setupMockFailureListeners(exception);
mMockBuildProvider.buildNotTested(mMockBuildInfo);
setupNormalInvoke(test);
- EasyMock.replay(mockCleaner);
try {
mTestInvocation.invoke(mMockDevice, mStubConfiguration, new StubRescheduler());
fail("DeviceNotAvailableException not thrown");
@@ -601,8 +605,10 @@
* Set up expected calls that occur on every invoke, regardless of result
*/
private void setupInvoke() {
+ mMockDevice.clearLastConnectedWifiNetwork();
mMockDevice.setOptions((TestDeviceOptions)EasyMock.anyObject());
mMockDevice.startLogcat();
+ mMockDevice.clearLastConnectedWifiNetwork();
mMockDevice.stopLogcat();
}
@@ -643,6 +649,12 @@
mMockTestListener.invocationStarted(mMockBuildInfo);
mMockSummaryListener.invocationStarted(mMockBuildInfo);
+ // invocationFailed
+ if (!status.equals(InvocationStatus.SUCCESS)) {
+ mMockTestListener.invocationFailed(EasyMock.eq(throwable));
+ mMockSummaryListener.invocationFailed(EasyMock.eq(throwable));
+ }
+
if (throwable instanceof BuildError) {
EasyMock.expect(mMockLogSaver.saveLogData(
EasyMock.eq(TestInvocation.BUILD_ERROR_BUGREPORT_NAME),
@@ -654,12 +666,6 @@
EasyMock.eq(LogDataType.BUGREPORT), (InputStreamSource)EasyMock.anyObject());
}
- // invocationFailed
- if (!status.equals(InvocationStatus.SUCCESS)) {
- mMockTestListener.invocationFailed(EasyMock.eq(throwable));
- mMockSummaryListener.invocationFailed(EasyMock.eq(throwable));
- }
-
// saveAndZipLogData (mMockLogFileSaver)
EasyMock.expect(mMockLogSaver.saveLogData( EasyMock.eq(TestInvocation.DEVICE_LOG_NAME),
EasyMock.eq(LogDataType.LOGCAT), (InputStream)EasyMock.anyObject())
diff --git a/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java b/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java
index 5c6034f..777e498 100644
--- a/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java
+++ b/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java
@@ -33,6 +33,7 @@
private IEmail mMockEmail;
private TerribleFailureEmailHandler mWtfEmailHandler;
private final static String MOCK_HOST_NAME = "myhostname.mydomain.com";
+ private long mCurrentTimeMillis;
@Override
protected void setUp() throws Exception {
@@ -43,7 +44,13 @@
protected String getLocalHostName() {
return MOCK_HOST_NAME;
}
+
+ @Override
+ protected long getCurrentTimeMillis() {
+ return mCurrentTimeMillis;
+ }
};
+ mCurrentTimeMillis = System.currentTimeMillis();
}
/**
@@ -103,6 +110,44 @@
}
/**
+ * Test that no email is attempted to be sent if it is too adjacent to the previous failure.
+ */
+ public void testOnTerribleFailure_adjacentFailures() throws IllegalArgumentException,
+ IOException {
+ mMockEmail.send(EasyMock.<Message>anyObject());
+ mWtfEmailHandler.setMinEmailInterval(60000);
+
+ EasyMock.replay(mMockEmail);
+ mWtfEmailHandler.addDestination("user@domain.com");
+ boolean retValue = mWtfEmailHandler.onTerribleFailure("something terrible happened", null);
+ assertTrue(retValue);
+ mCurrentTimeMillis += 30000;
+ retValue = mWtfEmailHandler.onTerribleFailure("something terrible happened again", null);
+ assertFalse(retValue);
+ EasyMock.verify(mMockEmail);
+ }
+
+ /**
+ * Test that the second email is attempted to be sent if it is not adjacent to the previous
+ * failure.
+ */
+ public void testOnTerribleFailure_notAdjacentFailures() throws IllegalArgumentException,
+ IOException {
+ mMockEmail.send(EasyMock.<Message>anyObject());
+ mMockEmail.send(EasyMock.<Message>anyObject());
+ mWtfEmailHandler.setMinEmailInterval(60000);
+
+ EasyMock.replay(mMockEmail);
+ mWtfEmailHandler.addDestination("user@domain.com");
+ boolean retValue = mWtfEmailHandler.onTerribleFailure("something terrible happened", null);
+ assertTrue(retValue);
+ mCurrentTimeMillis += 90000;
+ retValue = mWtfEmailHandler.onTerribleFailure("something terrible happened again", null);
+ assertTrue(retValue);
+ EasyMock.verify(mMockEmail);
+ }
+
+ /**
* Test that the generated email message actually contains the sender and
* destination email addresses.
*/
diff --git a/tests/src/com/android/tradefed/result/BugreportCollectorTest.java b/tests/src/com/android/tradefed/result/BugreportCollectorTest.java
index 9b87328..c807d3b 100644
--- a/tests/src/com/android/tradefed/result/BugreportCollectorTest.java
+++ b/tests/src/com/android/tradefed/result/BugreportCollectorTest.java
@@ -15,7 +15,6 @@
*/
package com.android.tradefed.result;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.result.BugreportCollector.Filter;
@@ -294,7 +293,7 @@
final TestIdentifier test = new TestIdentifier("FooTest", testName);
mCollector.testStarted(test);
if (shouldFail) {
- mCollector.testFailed(TestFailure.FAILURE, test, STACK_TRACE);
+ mCollector.testFailed(test, STACK_TRACE);
}
mCollector.testEnded(test, testMetrics);
mCollector.testRunEnded(0, runMetrics);
@@ -314,8 +313,7 @@
final TestIdentifier test = new TestIdentifier("FooTest", testName);
listener.testStarted(EasyMock.eq(test));
if (shouldFail) {
- listener.testFailed((TestFailure)EasyMock.anyObject(), EasyMock.eq(test),
- EasyMock.eq(STACK_TRACE));
+ listener.testFailed(EasyMock.eq(test), EasyMock.eq(STACK_TRACE));
}
listener.testEnded(EasyMock.eq(test), (Map<String, String>)EasyMock.anyObject());
listener.testRunEnded(EasyMock.anyInt(), (Map<String, String>)EasyMock.anyObject());
diff --git a/tests/src/com/android/tradefed/result/CollectingTestListenerTest.java b/tests/src/com/android/tradefed/result/CollectingTestListenerTest.java
index cfdfa69..c9063a4 100644
--- a/tests/src/com/android/tradefed/result/CollectingTestListenerTest.java
+++ b/tests/src/com/android/tradefed/result/CollectingTestListenerTest.java
@@ -15,10 +15,10 @@
*/
package com.android.tradefed.result;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.result.TestResult.TestStatus;
import junit.framework.TestCase;
@@ -87,7 +87,7 @@
final TestIdentifier test1 = injectTestRun("run1", "testFoo1", METRIC_VALUE);
final TestIdentifier test2 = injectTestRun("run2", "testFoo2", METRIC_VALUE2);
assertEquals(2, mCollectingTestListener.getNumTotalTests());
- assertEquals(2, mCollectingTestListener.getNumPassedTests());
+ assertEquals(2, mCollectingTestListener.getNumTestsInState(TestStatus.PASSED));
assertEquals(2, mCollectingTestListener.getRunResults().size());
Iterator<TestRunResult> runIter = mCollectingTestListener.getRunResults().iterator();
final TestRunResult runResult1 = runIter.next();
@@ -114,10 +114,10 @@
final TestIdentifier test1 = injectTestRun("run", "testFoo1", METRIC_VALUE);
final TestIdentifier test2 = injectTestRun("run", "testFoo2", METRIC_VALUE2);
assertEquals(2, mCollectingTestListener.getNumTotalTests());
- assertEquals(2, mCollectingTestListener.getNumPassedTests());
+ assertEquals(2, mCollectingTestListener.getNumTestsInState(TestStatus.PASSED));
assertEquals(1, mCollectingTestListener.getRunResults().size());
TestRunResult runResult = mCollectingTestListener.getCurrentRunResults();
- assertEquals(2, runResult.getNumPassedTests());
+ assertEquals(2, runResult.getNumTestsInState(TestStatus.PASSED));
assertTrue(runResult.getCompletedTests().contains(test1));
assertTrue(runResult.getCompletedTests().contains(test2));
}
@@ -130,12 +130,12 @@
injectTestRun("run", "testFoo1", METRIC_VALUE);
injectTestRun("run", "testFoo1", METRIC_VALUE2, true);
assertEquals(1, mCollectingTestListener.getNumTotalTests());
- assertEquals(0, mCollectingTestListener.getNumPassedTests());
- assertEquals(1, mCollectingTestListener.getNumFailedTests());
+ assertEquals(0, mCollectingTestListener.getNumTestsInState(TestStatus.PASSED));
+ assertEquals(1, mCollectingTestListener.getNumTestsInState(TestStatus.FAILURE));
assertEquals(1, mCollectingTestListener.getRunResults().size());
TestRunResult runResult = mCollectingTestListener.getCurrentRunResults();
- assertEquals(0, runResult.getNumPassedTests());
- assertEquals(1, runResult.getNumFailedTests());
+ assertEquals(0, runResult.getNumTestsInState(TestStatus.PASSED));
+ assertEquals(1, runResult.getNumTestsInState(TestStatus.FAILURE));
assertEquals(1, runResult.getNumTests());
}
@@ -147,7 +147,7 @@
mCollectingTestListener.testRunStarted("run", 1);
mCollectingTestListener.testStarted(new TestIdentifier("FooTest", "incomplete"));
mCollectingTestListener.testRunEnded(0, Collections.EMPTY_MAP);
- assertEquals(1, mCollectingTestListener.getNumIncompleteTests());
+ assertEquals(1, mCollectingTestListener.getNumTestsInState(TestStatus.INCOMPLETE));
}
/**
@@ -233,7 +233,7 @@
final TestIdentifier test = new TestIdentifier("FooTest", testName);
mCollectingTestListener.testStarted(test);
if (failtest) {
- mCollectingTestListener.testFailed(TestFailure.FAILURE, test, "trace");
+ mCollectingTestListener.testFailed(test, "trace");
}
mCollectingTestListener.testEnded(test, testMetrics);
mCollectingTestListener.testRunEnded(0, runMetrics);
diff --git a/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java b/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
new file mode 100644
index 0000000..9a7cce9
--- /dev/null
+++ b/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import junit.framework.TestCase;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link ConsoleResultReporter}
+ */
+public class ConsoleResultReporterTest extends TestCase {
+ private static final Map<String, String> EMPTY_MAP = Collections.emptyMap();
+
+ /**
+ * Test the results printed for an empty invocation
+ */
+ public void testGetInvocationSummary_empty() {
+ ConsoleResultReporter reporter = new ConsoleResultReporter();
+ reporter.invocationStarted(null);
+ reporter.invocationEnded(0);
+ assertEquals("No test results\n", reporter.getInvocationSummary());
+ }
+
+ /**
+ * Test that run metrics are sorted by key
+ */
+ public void testGetInvocationSummary_test_run_metrics() {
+ ConsoleResultReporter reporter = new ConsoleResultReporter();
+ reporter.invocationStarted(null);
+ reporter.testRunStarted("Test Run", 0);
+ Map<String, String> metrics = new HashMap<>();
+ metrics.put("key2", "value2");
+ metrics.put("key1", "value1");
+ reporter.testRunEnded(0, metrics);
+ reporter.invocationEnded(0);
+ assertEquals(
+ "Test results:\n" +
+ "Test Run:\n" +
+ " key1: value1\n" +
+ " key2: value2\n",
+ reporter.getInvocationSummary());
+ }
+
+ /**
+ * Test that test metrics are sorted by key
+ */
+ public void testGetInvocationSummary_test_metrics() {
+ ConsoleResultReporter reporter = new ConsoleResultReporter();
+ reporter.invocationStarted(null);
+ reporter.testRunStarted("Test Run", 1);
+ TestIdentifier testId = new TestIdentifier("class", "method");
+ reporter.testStarted(testId);
+ Map<String, String> metrics = new HashMap<>();
+ metrics.put("key2", "value2");
+ metrics.put("key1", "value1");
+ reporter.testEnded(testId, metrics);
+ reporter.testRunEnded(0, EMPTY_MAP);
+ reporter.invocationEnded(0);
+ assertEquals(
+ "Test results:\n" +
+ "Test Run: 1 Test, 1 Passed, 0 Failed, 0 Ignored\n" +
+ " class#method: PASSED\n" +
+ " key1: value1\n" +
+ " key2: value2\n",
+ reporter.getInvocationSummary());
+ }
+
+ /**
+ * Test that logs are printed, favoring url.
+ */
+ public void testGetInvocationSummary_logs() {
+ ConsoleResultReporter reporter = new ConsoleResultReporter();
+ reporter.invocationStarted(null);
+ reporter.testLogSaved(null, null, null, new LogFile("/path/to/log1", "http://log1"));
+ reporter.testLogSaved(null, null, null, new LogFile("/path/to/log2", null));
+ reporter.invocationEnded(0);
+ assertEquals(
+ "Test results:\n" +
+ "Log Files:\n" +
+ " http://log1\n" +
+ " /path/to/log2\n",
+ reporter.getInvocationSummary());
+ }
+
+ /**
+ * Inclusive test to test that all test runs are printed, and metrics and logs are printed with
+ * it.
+ */
+ public void testGetInvocationSummary_all() {
+ ConsoleResultReporter reporter = new ConsoleResultReporter();
+ reporter.invocationStarted(null);
+
+ reporter.testRunStarted("Test Run 1", 3);
+
+ TestIdentifier run1test1Id = new TestIdentifier("class1", "method1");
+ reporter.testStarted(run1test1Id);
+ reporter.testFailed(run1test1Id, "trace");
+ Map<String, String> run1Test1Metrics = new HashMap<>();
+ run1Test1Metrics.put("run1_test1_key1", "run1_test1_value1");
+ run1Test1Metrics.put("run1_test1_key2", "run1_test1_value2");
+ reporter.testEnded(run1test1Id, run1Test1Metrics);
+
+ TestIdentifier run1test2Id = new TestIdentifier("class1", "method2");
+ reporter.testStarted(run1test2Id);
+ Map<String, String> run1Test2Metrics = new HashMap<>();
+ run1Test2Metrics.put("run1_test2_key1", "run1_test2_value1");
+ run1Test2Metrics.put("run1_test2_key2", "run1_test2_value2");
+ reporter.testEnded(run1test2Id, run1Test2Metrics);
+
+ TestIdentifier run1test3Id = new TestIdentifier("class1", "method3");
+ reporter.testStarted(run1test3Id);
+ reporter.testAssumptionFailure(run1test3Id, "trace");
+ Map<String, String> run1Test3Metrics = new HashMap<>();
+ run1Test3Metrics.put("run1_test3_key1", "run1_test3_value1");
+ run1Test3Metrics.put("run1_test3_key2", "run1_test3_value2");
+ reporter.testEnded(run1test3Id, run1Test3Metrics);
+
+ TestIdentifier run1test4Id = new TestIdentifier("class1", "method4");
+ reporter.testStarted(run1test4Id);
+ reporter.testIgnored(run1test4Id);
+ reporter.testEnded(run1test4Id, EMPTY_MAP);
+
+ Map<String, String> run1Metrics = new HashMap<>();
+ run1Metrics.put("run1_key1", "run1_value2");
+ run1Metrics.put("run1_key2", "run1_value1");
+ reporter.testRunEnded(0, run1Metrics);
+
+ reporter.testRunStarted("Test Run 2", 4);
+ TestIdentifier run2test1Id = new TestIdentifier("class2", "method1");
+ reporter.testStarted(run2test1Id);
+ reporter.testFailed(run2test1Id, "trace");
+ reporter.testEnded(run2test1Id, EMPTY_MAP);
+
+ TestIdentifier run2test2Id = new TestIdentifier("class2", "method2");
+ reporter.testStarted(run2test2Id);
+ reporter.testEnded(run2test2Id, EMPTY_MAP);
+
+ TestIdentifier run2test3Id = new TestIdentifier("class2", "method3");
+ reporter.testStarted(run2test3Id);
+ reporter.testAssumptionFailure(run2test3Id, "trace");
+ reporter.testEnded(run2test3Id, EMPTY_MAP);
+
+ TestIdentifier run2test4Id = new TestIdentifier("class2", "method4");
+ reporter.testStarted(run2test4Id);
+ reporter.testIgnored(run2test4Id);
+ reporter.testEnded(run2test4Id, EMPTY_MAP);
+ reporter.testRunEnded(0, EMPTY_MAP);
+
+ reporter.testRunStarted("Test Run 3", 0);
+ Map<String, String> run3Metrics = new HashMap<>();
+ run3Metrics.put("run3_key1", "run3_value1");
+ run3Metrics.put("run3_key2", "run3_value2");
+ reporter.testRunEnded(0, run3Metrics);
+
+ reporter.testRunStarted("Test Run 4", 0);
+ reporter.testRunEnded(0, EMPTY_MAP);
+
+ reporter.testLogSaved(null, null, null, new LogFile("/path/to/log1", "http://log1"));
+ reporter.testLogSaved(null, null, null, new LogFile("/path/to/log2", null));
+ reporter.invocationEnded(0);
+ assertEquals(
+ "Test results:\n" +
+ "Test Run 1: 4 Tests, 1 Passed, 2 Failed, 1 Ignored\n" +
+ " class1#method1: FAILURE\n" +
+ " run1_test1_key1: run1_test1_value1\n" +
+ " run1_test1_key2: run1_test1_value2\n" +
+ " class1#method2: PASSED\n" +
+ " run1_test2_key1: run1_test2_value1\n" +
+ " run1_test2_key2: run1_test2_value2\n" +
+ " class1#method3: ASSUMPTION_FAILURE\n" +
+ " run1_test3_key1: run1_test3_value1\n" +
+ " run1_test3_key2: run1_test3_value2\n" +
+ " class1#method4: IGNORED\n" +
+ " run1_key1: run1_value2\n" +
+ " run1_key2: run1_value1\n" +
+ "\n" +
+ "Test Run 2: 4 Tests, 1 Passed, 2 Failed, 1 Ignored\n" +
+ " class2#method1: FAILURE\n" +
+ " class2#method2: PASSED\n" +
+ " class2#method3: ASSUMPTION_FAILURE\n" +
+ " class2#method4: IGNORED\n" +
+ "\n" +
+ "Test Run 3:\n" +
+ " run3_key1: run3_value1\n" +
+ " run3_key2: run3_value2\n" +
+ "\n" +
+ "Test Run 4: No results\n" +
+ "\n" +
+ "Log Files:\n" +
+ " http://log1\n" +
+ " /path/to/log2\n",
+ reporter.getInvocationSummary());
+ }
+}
diff --git a/tests/src/com/android/tradefed/result/DeviceFileReporterTest.java b/tests/src/com/android/tradefed/result/DeviceFileReporterTest.java
index 67a683e..0ddd95d 100644
--- a/tests/src/com/android/tradefed/result/DeviceFileReporterTest.java
+++ b/tests/src/com/android/tradefed/result/DeviceFileReporterTest.java
@@ -16,6 +16,7 @@
package com.android.tradefed.result;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.ArrayUtil;
import junit.framework.TestCase;
@@ -23,6 +24,8 @@
import java.io.File;
import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
/**
* Unit tests for {@link DeviceFileReporter}
@@ -35,9 +38,31 @@
// Used to control what ISS is returned
InputStreamSource mDfrIss = null;
+ @SuppressWarnings("serial")
+ private static class FakeFile extends File {
+ private final String mName;
+ private final long mSize;
+
+ FakeFile(String name, long size) {
+ super(name);
+ mName = name;
+ mSize = size;
+ }
+ @Override
+ public String toString() {
+ return mName;
+ }
+ @Override
+ public long length() {
+ return mSize;
+ }
+ }
+
@Override
public void setUp() throws Exception {
mDevice = EasyMock.createMock(ITestDevice.class);
+ EasyMock.expect(mDevice.getSerialNumber()).andStubReturn("serial");
+
mListener = EasyMock.createMock(ITestInvocationListener.class);
dfr = new DeviceFileReporter(mDevice, mListener) {
@Override
@@ -50,14 +75,15 @@
public void testSimple() throws Exception {
final String result = "/data/tombstones/tombstone_00\r\n";
final String filename = "/data/tombstones/tombstone_00";
+ final String tombstone = "What do you want on your tombstone?";
dfr.addPatterns("/data/tombstones/*");
EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
.andReturn(result);
// This gets passed verbatim to createIssForFile above
- EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename))).andReturn(null);
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename)))
+ .andReturn(new FakeFile(filename, tombstone.length()));
- final String tombstone = "What do you want on your tombstone?";
mDfrIss = new ByteArrayInputStreamSource(tombstone.getBytes());
// FIXME: use captures here to make sure we get the string back out
mListener.testLog(EasyMock.eq(filename), EasyMock.eq(LogDataType.UNKNOWN),
@@ -68,6 +94,175 @@
verifyMocks();
}
+ public void testLineEnding_LF() throws Exception {
+ final String[] filenames = {"/data/tombstones/tombstone_00",
+ "/data/tombstones/tombstone_01",
+ "/data/tombstones/tombstone_02",
+ "/data/tombstones/tombstone_03",
+ "/data/tombstones/tombstone_04"};
+ String result = ArrayUtil.join("\n", (Object[])filenames);
+ final String tombstone = "What do you want on your tombstone?";
+ dfr.addPatterns("/data/tombstones/*");
+
+ EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+ .andReturn(result);
+ mDfrIss = new ByteArrayInputStreamSource(tombstone.getBytes());
+ // This gets passed verbatim to createIssForFile above
+ for (String filename : filenames) {
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename))).andReturn(
+ new FakeFile(filename, tombstone.length()));
+
+ // FIXME: use captures here to make sure we get the string back out
+ mListener.testLog(EasyMock.eq(filename), EasyMock.eq(LogDataType.UNKNOWN),
+ EasyMock.eq(mDfrIss));
+ }
+ replayMocks();
+ dfr.run();
+ verifyMocks();
+ }
+
+ public void testLineEnding_CRLF() throws Exception {
+ final String[] filenames = {"/data/tombstones/tombstone_00",
+ "/data/tombstones/tombstone_01",
+ "/data/tombstones/tombstone_02",
+ "/data/tombstones/tombstone_03",
+ "/data/tombstones/tombstone_04"};
+ String result = ArrayUtil.join("\r\n", (Object[])filenames);
+ final String tombstone = "What do you want on your tombstone?";
+ dfr.addPatterns("/data/tombstones/*");
+
+ EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+ .andReturn(result);
+ mDfrIss = new ByteArrayInputStreamSource(tombstone.getBytes());
+ // This gets passed verbatim to createIssForFile above
+ for (String filename : filenames) {
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename))).andReturn(
+ new FakeFile(filename, tombstone.length()));
+
+ // FIXME: use captures here to make sure we get the string back out
+ mListener.testLog(EasyMock.eq(filename), EasyMock.eq(LogDataType.UNKNOWN),
+ EasyMock.eq(mDfrIss));
+ }
+ replayMocks();
+ dfr.run();
+ verifyMocks();
+ }
+
+ /**
+ * Make sure that the Reporter behaves as expected when a file is matched by multiple patterns
+ */
+ public void testRepeat_skip() throws Exception {
+ final String result1 = "/data/files/file.png\r\n";
+ final String result2 = "/data/files/file.png\r\n/data/files/file.xml\r\n";
+ final String pngFilename = "/data/files/file.png";
+ final String xmlFilename = "/data/files/file.xml";
+ final Map<String, LogDataType> patMap = new HashMap<>(2);
+ patMap.put("/data/files/*.png", LogDataType.PNG);
+ patMap.put("/data/files/*", LogDataType.UNKNOWN);
+
+ final String pngContents = "This is PNG data";
+ final String xmlContents = "<!-- This is XML data -->";
+ final InputStreamSource pngIss = new ByteArrayInputStreamSource(pngContents.getBytes());
+ final InputStreamSource xmlIss = new ByteArrayInputStreamSource(xmlContents.getBytes());
+
+ dfr = new DeviceFileReporter(mDevice, mListener) {
+ @Override
+ InputStreamSource createIssForFile(File file) throws IOException {
+ if (file.toString().endsWith(".png")) {
+ return pngIss;
+ } else if (file.toString().endsWith(".xml")) {
+ return xmlIss;
+ }
+ throw new IOException ("unknown fake file");
+ }
+ };
+ dfr.addPatterns(patMap);
+ dfr.setInferUnknownDataTypes(false);
+
+ // Set file listing pulling, and reporting expectations
+ // Expect that we go through the entire process for the PNG file, and then go through
+ // the entire process again for the XML file
+ EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+ .andReturn(result1);
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(pngFilename)))
+ .andReturn(new FakeFile(pngFilename, pngContents.length()));
+ mListener.testLog(EasyMock.eq(pngFilename), EasyMock.eq(LogDataType.PNG),
+ EasyMock.eq(pngIss));
+
+ EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+ .andReturn(result2);
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(xmlFilename)))
+ .andReturn(new FakeFile(xmlFilename, xmlContents.length()));
+ mListener.testLog(EasyMock.eq(xmlFilename), EasyMock.eq(LogDataType.UNKNOWN),
+ EasyMock.eq(xmlIss));
+
+ replayMocks();
+ dfr.run();
+ verifyMocks();
+ // FIXME: use captures here to make sure we get the string back out
+ }
+
+ /**
+ * Make sure that the Reporter behaves as expected when a file is matched by multiple patterns
+ */
+ public void testRepeat_noSkip() throws Exception {
+ final String result1 = "/data/files/file.png\r\n";
+ final String result2 = "/data/files/file.png\r\n/data/files/file.xml\r\n";
+ final String pngFilename = "/data/files/file.png";
+ final String xmlFilename = "/data/files/file.xml";
+ final Map<String, LogDataType> patMap = new HashMap<>(2);
+ patMap.put("/data/files/*.png", LogDataType.PNG);
+ patMap.put("/data/files/*", LogDataType.UNKNOWN);
+
+ final String pngContents = "This is PNG data";
+ final String xmlContents = "<!-- This is XML data -->";
+ final InputStreamSource pngIss = new ByteArrayInputStreamSource(pngContents.getBytes());
+ final InputStreamSource xmlIss = new ByteArrayInputStreamSource(xmlContents.getBytes());
+
+ dfr = new DeviceFileReporter(mDevice, mListener) {
+ @Override
+ InputStreamSource createIssForFile(File file) throws IOException {
+ if (file.toString().endsWith(".png")) {
+ return pngIss;
+ } else if (file.toString().endsWith(".xml")) {
+ return xmlIss;
+ }
+ throw new IOException ("unknown fake file");
+ }
+ };
+ dfr.addPatterns(patMap);
+ dfr.setInferUnknownDataTypes(false);
+ // this should cause us to see three pulls instead of two
+ dfr.setSkipRepeatFiles(false);
+
+ // Set file listing pulling, and reporting expectations
+ // Expect that we go through the entire process for the PNG file, and then go through
+ // the entire process again for the PNG file (again) and the XML file
+ EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+ .andReturn(result1);
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(pngFilename)))
+ .andReturn(new FakeFile(pngFilename, pngContents.length()));
+ mListener.testLog(EasyMock.eq(pngFilename), EasyMock.eq(LogDataType.PNG),
+ EasyMock.eq(pngIss));
+
+ // Note that the PNG file is picked up with the UNKNOWN data type this time
+ EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+ .andReturn(result2);
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(pngFilename)))
+ .andReturn(new FakeFile(pngFilename, pngContents.length()));
+ mListener.testLog(EasyMock.eq(pngFilename), EasyMock.eq(LogDataType.UNKNOWN),
+ EasyMock.eq(pngIss));
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(xmlFilename)))
+ .andReturn(new FakeFile(xmlFilename, xmlContents.length()));
+ mListener.testLog(EasyMock.eq(xmlFilename), EasyMock.eq(LogDataType.UNKNOWN),
+ EasyMock.eq(xmlIss));
+
+ replayMocks();
+ dfr.run();
+ verifyMocks();
+ // FIXME: use captures here to make sure we get the string back out
+ }
+
/**
* Make sure that we correctly handle the case where a file doesn't exist while matching the
* exact name.
@@ -94,6 +289,7 @@
final String result = "/data/tombstones/tombstone_00\r\n/data/tombstones/tombstone_01\r\n";
final String filename1 = "/data/tombstones/tombstone_00";
final String filename2 = "/data/tombstones/tombstone_01";
+ final String tombstone = "What do you want on your tombstone?";
dfr.addPatterns("/data/tombstones/*");
// Search the filesystem
@@ -102,8 +298,8 @@
// Log the first file
// This gets passed verbatim to createIssForFile above
- EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename1))).andReturn(null);
- final String tombstone = "What do you want on your tombstone?";
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename1)))
+ .andReturn(new FakeFile(filename1, tombstone.length()));
mDfrIss = new ByteArrayInputStreamSource(tombstone.getBytes());
// FIXME: use captures here to make sure we get the string back out
mListener.testLog(EasyMock.eq(filename1), EasyMock.eq(LogDataType.UNKNOWN),
@@ -111,7 +307,8 @@
// Log the second file
// This gets passed verbatim to createIssForFile above
- EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename2))).andReturn(null);
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename2)))
+ .andReturn(new FakeFile(filename2, tombstone.length()));
// FIXME: use captures here to make sure we get the string back out
mListener.testLog(EasyMock.eq(filename2), EasyMock.eq(LogDataType.UNKNOWN),
EasyMock.eq(mDfrIss));
@@ -121,6 +318,40 @@
verifyMocks();
}
+ /**
+ * Make sure that data type inference works as expected
+ */
+ public void testInferDataTypes() throws Exception {
+ final String result = "/data/files/file.png\r\n/data/files/file.xml\r\n" +
+ "/data/files/file.zip\r\n";
+ final String[] filenames = {"/data/files/file.png", "/data/files/file.xml",
+ "/data/files/file.zip"};
+ final LogDataType[] expTypes = {LogDataType.PNG, LogDataType.XML, LogDataType.ZIP};
+ dfr.addPatterns("/data/files/*");
+
+ final String contents = "these are file contents";
+ mDfrIss = new ByteArrayInputStreamSource(contents.getBytes());
+
+ EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+ .andReturn(result);
+ // This gets passed verbatim to createIssForFile above
+ for (int i = 0; i < filenames.length; ++i) {
+ final String filename = filenames[i];
+ final LogDataType expType = expTypes[i];
+ EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename)))
+ .andReturn(new FakeFile(filename, contents.length()));
+
+ // FIXME: use captures here to make sure we get the string back out
+ mListener.testLog(EasyMock.eq(filename), EasyMock.eq(expType),
+ EasyMock.eq(mDfrIss));
+ }
+
+ replayMocks();
+ dfr.run();
+ verifyMocks();
+ }
+
+
private void replayMocks() {
EasyMock.replay(mDevice, mListener);
}
diff --git a/tests/src/com/android/tradefed/result/JUnitToInvocationResultForwarderTest.java b/tests/src/com/android/tradefed/result/JUnitToInvocationResultForwarderTest.java
index 39b6847..3adc047 100644
--- a/tests/src/com/android/tradefed/result/JUnitToInvocationResultForwarderTest.java
+++ b/tests/src/com/android/tradefed/result/JUnitToInvocationResultForwarderTest.java
@@ -15,7 +15,6 @@
*/
package com.android.tradefed.result;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import junit.framework.AssertionFailedError;
@@ -46,24 +45,12 @@
}
/**
- * Test method for {@link JUnitToInvocationResultForwarder#addError(Test, Throwable)}.
- */
- public void testAddError() {
- final Throwable t = new Throwable();
- mListener.testFailed(EasyMock.eq(TestFailure.ERROR),
- EasyMock.eq(new TestIdentifier(JUnitToInvocationResultForwarderTest.class.getName(),
- "testAddError")), (String)EasyMock.anyObject());
- EasyMock.replay(mListener);
- mForwarder.addError(this, t);
- }
-
- /**
* Test method for
* {@link JUnitToInvocationResultForwarder#addFailure(Test, AssertionFailedError)}.
*/
public void testAddFailure() {
final AssertionFailedError a = new AssertionFailedError();
- mListener.testFailed(EasyMock.eq(TestFailure.FAILURE),
+ mListener.testFailed(
EasyMock.eq(new TestIdentifier(JUnitToInvocationResultForwarderTest.class.getName(),
"testAddFailure")), (String)EasyMock.anyObject());
EasyMock.replay(mListener);
diff --git a/tests/src/com/android/tradefed/result/XmlResultReporterTest.java b/tests/src/com/android/tradefed/result/XmlResultReporterTest.java
index b22e2b5..0181f1b 100644
--- a/tests/src/com/android/tradefed/result/XmlResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/XmlResultReporterTest.java
@@ -15,7 +15,6 @@
*/
package com.android.tradefed.result;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.build.BuildInfo;
import com.android.tradefed.build.IBuildInfo;
@@ -139,7 +138,7 @@
mResultReporter.invocationStarted(new BuildInfo());
mResultReporter.testRunStarted("run", 1);
mResultReporter.testStarted(testId);
- mResultReporter.testFailed(TestFailure.FAILURE, testId, trace);
+ mResultReporter.testFailed(testId, trace);
mResultReporter.testEnded(testId, emptyMap);
mResultReporter.testRunEnded(3, emptyMap);
mResultReporter.invocationEnded(1);
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java b/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
index 4a112e6..be5172c 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
@@ -56,6 +56,7 @@
mMockDevice = EasyMock.createMock(ITestDevice.class);
EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("foo").anyTimes();
mMockBuildInfo = new DeviceBuildInfo("0", "", "");
+ mMockBuildInfo.setDeviceImageFile(new File("foo"), "0");
mMockBuildInfo.setBuildFlavor("flavor");
mDeviceFlashPreparer = new DeviceFlashPreparer() {
@Override
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
index 013c61d..da734da 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
@@ -22,14 +22,15 @@
import com.android.tradefed.build.IDeviceBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.targetprep.DeviceSetup.BinaryState;
import com.android.tradefed.util.FileUtil;
import junit.framework.TestCase;
+import org.easymock.Capture;
import org.easymock.EasyMock;
import java.io.File;
-import java.io.IOException;
/**
* Unit tests for {@link DeviceSetup}.
@@ -53,7 +54,6 @@
EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("foo").anyTimes();
mMockBuildInfo = new DeviceBuildInfo("0", "", "");
mDeviceSetup = new DeviceSetup();
- mDeviceSetup.setMinExternalStoreSpace(-1);
mTmpDir = FileUtil.createTempDir("tmp");
}
@@ -70,27 +70,647 @@
* Simple normal case test for {@link DeviceSetup#setUp(ITestDevice, IBuildInfo)}.
*/
public void testSetup() throws Exception {
- doSetupExpectations();
+ Capture<String> setPropCapture = new Capture<>();
+ doSetupExpectations(true, setPropCapture);
+ doCheckExternalStoreSpaceExpectations();
EasyMock.replay(mMockDevice);
+
mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+
+ String setProp = setPropCapture.getValue();
+ assertTrue("Set prop doesn't contain ro.telephony.disable-call=true",
+ setProp.contains("ro.telephony.disable-call=true\n"));
+ assertTrue("Set prop doesn't contain ro.audio.silent=1",
+ setProp.contains("ro.audio.silent=1\n"));
+ assertTrue("Set prop doesn't contain ro.test_harness=1",
+ setProp.contains("ro.test_harness=1\n"));
+ assertTrue("Set prop doesn't contain ro.monkey=1",
+ setProp.contains("ro.monkey=1\n"));
}
- /**
- * Set EasyMock expectations for a normal setup call
- */
- private void doSetupExpectations() throws TargetSetupError, DeviceNotAvailableException {
- EasyMock.expect(mMockDevice.enableAdbRoot()).andReturn(Boolean.TRUE);
- mMockDevice.postBootSetup();
- EasyMock.expect(mMockDevice.clearErrorDialogs()).andReturn(Boolean.TRUE);
- EasyMock.expect(mMockDevice.executeShellCommand("getprop dev.bootcomplete")).andReturn("1");
- // expect push of local.prop file to change system properties
- EasyMock.expect(mMockDevice.pushString((String)EasyMock.anyObject(),
- EasyMock.contains("local.prop"))).andReturn(Boolean.TRUE);
- mMockDevice.reboot();
- // expect a bunch of shell commands - no need to verify which ones
- EasyMock.expect(mMockDevice.executeShellCommand((String)EasyMock.anyObject())).
- andReturn("").anyTimes();
- EasyMock.expect(mMockDevice.getProperty("ro.build.id")).andReturn("IMM76K");
+ public void testSetup_airplane_mode_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true,
+ "settings put global \"airplane_mode_on\" \"1\"",
+ "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setAirplaneMode(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_airplane_mode_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true,
+ "settings put global \"airplane_mode_on\" \"0\"",
+ "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setAirplaneMode(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_wifi_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true,
+ "settings put global \"wifi_on\" \"1\"",
+ "svc wifi enable");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setWifi(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_wifi_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true,
+ "settings put global \"wifi_on\" \"0\"",
+ "svc wifi disable");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setWifi(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_wifi_watchdog_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put global \"wifi_watchdog\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setWifiWatchdog(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_wifi_watchdog_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put global \"wifi_watchdog\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setWifiWatchdog(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_wifi_scan_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put global \"wifi_scan_always_enabled\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setWifiScanAlwaysEnabled(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_wifi_scan_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put global \"wifi_scan_always_enabled\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setWifiScanAlwaysEnabled(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_ethernet_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(false, "ifconfig eth0 up");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setEthernet(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_ethernet_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(false, "ifconfig eth0 down");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setEthernet(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_bluetooth_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(false, "service call bluetooth_manager 6");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setBluetooth(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_bluetooth_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(false, "service call bluetooth_manager 8");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setBluetooth(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_adaptive_on() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"screen_brightness_mode\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenAdaptiveBrightness(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_adaptive_off() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"screen_brightness_mode\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenAdaptiveBrightness(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_brightness() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"screen_brightness\" \"50\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenBrightness(50);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_stayon_default() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations(false /* Expect no screen on command */, new Capture<String>());
+ doCheckExternalStoreSpaceExpectations();
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenAlwaysOn(BinaryState.IGNORE);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_stayon_off() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations(false /* Expect no screen on command */, new Capture<String>());
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(false, "svc power stayon false");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenAlwaysOn(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_timeout() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations(false /* Expect no screen on command */, new Capture<String>());
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"screen_off_timeout\" \"5000\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenAlwaysOn(BinaryState.IGNORE);
+ mDeviceSetup.setScreenTimeoutSecs(5l);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_ambient_on() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put secure \"doze_enabled\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenAmbientMode(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_ambient_off() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put secure \"doze_enabled\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenAmbientMode(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_wake_gesture_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put secure \"wake_gesture_enabled\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setWakeGesture(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_wake_gesture_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put secure \"wake_gesture_enabled\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setWakeGesture(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_saver_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put secure \"screensaver_enabled\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenSaver(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_screen_saver_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put secure \"screensaver_enabled\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setScreenSaver(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_notification_led_on() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"notification_light_pulse\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setNotificationLed(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_notification_led_off() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"notification_light_pulse\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setNotificationLed(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_trigger_media_mounted() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(false, "am broadcast -a android.intent.action.MEDIA_MOUNTED " +
+ "-d file://${EXTERNAL_STORAGE}");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setTriggerMediaMounted(true);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_location_gps_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put secure \"location_providers_allowed\" \"+gps\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setLocationGps(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_location_gps_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put secure \"location_providers_allowed\" \"-gps\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setLocationGps(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_location_network_on() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true,
+ "settings put secure \"location_providers_allowed\" \"+network\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setLocationNetwork(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_location_network_off() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true,
+ "settings put secure \"location_providers_allowed\" \"-network\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setLocationNetwork(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_rotate_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"accelerometer_rotation\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setAutoRotate(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_rotate_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"accelerometer_rotation\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setAutoRotate(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_battery_saver_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true,
+ "dumpsys battery set usb 0",
+ "settings put global \"low_power\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setBatterySaver(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_battery_saver_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put global \"low_power\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setBatterySaver(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_battery_saver_trigger() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put global \"low_power_trigger_level\" \"50\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setBatterySaverTrigger(50);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_disable_doze() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(false, "dumpsys deviceidle disable");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setDisableDoze(true);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_update_time_on() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"auto_time\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setAutoUpdateTime(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_update_time_off() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"auto_time\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setAutoUpdateTime(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_update_timezone_on() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"auto_timezone\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setAutoUpdateTimezone(BinaryState.ON);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_update_timezone_off() throws DeviceNotAvailableException,
+ TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put system \"auto_timezone\" \"0\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setAutoUpdateTimezone(BinaryState.OFF);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_no_disable_dialing() throws DeviceNotAvailableException,
+ TargetSetupError {
+ Capture<String> setPropCapture = new Capture<>();
+ doSetupExpectations(true, setPropCapture);
+ doCheckExternalStoreSpaceExpectations();
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setDisableDialing(false);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+
+ assertFalse("Set prop contains ro.telephony.disable-call=true",
+ setPropCapture.getValue().contains("ro.telephony.disable-call=true\n"));
+ }
+
+ public void testSetup_sim_data() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put global \"multi_sim_data_call\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setDefaultSimData(1);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_sim_voice() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put global \"multi_sim_voice_call\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setDefaultSimVoice(1);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_sim_sms() throws DeviceNotAvailableException, TargetSetupError {
+ doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
+ doCommandsExpectations(true, "settings put global \"multi_sim_sms\" \"1\"");
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setDefaultSimSms(1);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+ }
+
+ public void testSetup_no_disable_audio() throws DeviceNotAvailableException, TargetSetupError {
+ Capture<String> setPropCapture = new Capture<>();
+ doSetupExpectations(true, setPropCapture);
+ doCheckExternalStoreSpaceExpectations();
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setDisableAudio(false);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+
+ assertFalse("Set prop contains ro.audio.silent=1",
+ setPropCapture.getValue().contains("ro.audio.silent=1\n"));
+ }
+
+ public void testSetup_no_test_harness() throws DeviceNotAvailableException, TargetSetupError {
+ Capture<String> setPropCapture = new Capture<>();
+ doSetupExpectations(true, setPropCapture);
+ doCheckExternalStoreSpaceExpectations();
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setTestHarness(false);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+
+ String setProp = setPropCapture.getValue();
+ assertFalse("Set prop contains ro.test_harness=1",
+ setProp.contains("ro.test_harness=1\n"));
+ assertFalse("Set prop contains ro.monkey=1",
+ setProp.contains("ro.monkey=1\n"));
+ }
+
+ public void testSetup_disalbe_dalvik_verifier() throws DeviceNotAvailableException,
+ TargetSetupError {
+ Capture<String> setPropCapture = new Capture<>();
+ doSetupExpectations(true, setPropCapture);
+ doCheckExternalStoreSpaceExpectations();
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setDisableDalvikVerifier(true);
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+
+ String setProp = setPropCapture.getValue();
+ assertTrue("Set prop doesn't contain dalvik.vm.dexopt-flags=v=n",
+ setProp.contains("dalvik.vm.dexopt-flags=v=n\n"));
}
/**
@@ -98,7 +718,7 @@
*/
public void testSetup_freespace() throws Exception {
doSetupExpectations();
- mDeviceSetup.setMinExternalStoreSpace(500);
+ mDeviceSetup.setMinExternalStorageKb(500);
EasyMock.expect(mMockDevice.getExternalStoreFreeSpace()).andReturn(1L);
EasyMock.replay(mMockDevice);
try {
@@ -131,23 +751,12 @@
*/
public void testSetup_syncData() throws Exception {
doSetupExpectations();
+ doCheckExternalStoreSpaceExpectations();
doSyncDataExpectations(true);
EasyMock.replay(mMockDevice, mMockIDevice);
mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
- }
-
- /**
- * Perform common EasyMock expect operations for a setUp call which syncs local data
- */
- private void doSyncDataExpectations(boolean result) throws IOException,
- DeviceNotAvailableException {
- mDeviceSetup.setLocalDataPath(mTmpDir);
- EasyMock.expect(mMockDevice.getIDevice()).andReturn(mMockIDevice);
- String mntPoint = "/sdcard";
- EasyMock.expect(mMockIDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE)).andReturn(
- mntPoint);
- EasyMock.expect(mMockDevice.syncFiles(mTmpDir, mntPoint)).andReturn(result);
+ EasyMock.verify(mMockDevice, mMockIDevice);
}
/**
@@ -166,20 +775,80 @@
}
}
- public void testBuildName() {
- assertTrue("failed to verify IML74K", mDeviceSetup.isReleaseBuildName("IML74K"));
- assertTrue("failed to verify GRJ90", mDeviceSetup.isReleaseBuildName("GRJ90"));
- assertFalse("failed to reject MASTER", mDeviceSetup.isReleaseBuildName("MASTER"));
- assertFalse("failed to reject 123456", mDeviceSetup.isReleaseBuildName("123456"));
- assertFalse("failed to reject empty string", mDeviceSetup.isReleaseBuildName(""));
- assertFalse("failed to reject random stuff", mDeviceSetup.isReleaseBuildName("!@#$%^&*("));
+ @SuppressWarnings("deprecation")
+ public void testSetup_legacy() throws DeviceNotAvailableException, TargetSetupError {
+ Capture<String> setPropCapture = new Capture<>();
+ doSetupExpectations(true, setPropCapture);
+ doCheckExternalStoreSpaceExpectations();
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setDeprecatedAudioSilent(false);
+ mDeviceSetup.setDeprecatedMinExternalStoreSpace(1000);
+ mDeviceSetup.setDeprecatedSetProp("key=value");
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+ EasyMock.verify(mMockDevice);
+
+ String setProp = setPropCapture.getValue();
+ assertTrue("Set prop doesn't contain ro.telephony.disable-call=true",
+ setProp.contains("ro.telephony.disable-call=true\n"));
+ assertTrue("Set prop doesn't contain ro.test_harness=1",
+ setProp.contains("ro.test_harness=1\n"));
+ assertTrue("Set prop doesn't contain ro.monkey=1",
+ setProp.contains("ro.monkey=1\n"));
+ assertTrue("Set prop doesn't contain key=value",
+ setProp.contains("key=value\n"));
+ }
+
+ @SuppressWarnings("deprecation")
+ public void testSetup_legacy_storage_conflict() throws DeviceNotAvailableException {
+ doSetupExpectations();
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setMinExternalStorageKb(1000);
+ mDeviceSetup.setDeprecatedMinExternalStoreSpace(1000);
+ try {
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+ fail("TargetSetupError expected");
+ } catch (TargetSetupError e) {
+ // Expected
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public void testSetup_legacy_silent_conflict() throws DeviceNotAvailableException {
+ doSetupExpectations();
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setDisableAudio(false);
+ mDeviceSetup.setDeprecatedAudioSilent(false);
+ try {
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+ fail("TargetSetupError expected");
+ } catch (TargetSetupError e) {
+ // Expected
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public void testSetup_legacy_setprop_conflict() throws DeviceNotAvailableException {
+ doSetupExpectations();
+ EasyMock.replay(mMockDevice);
+
+ mDeviceSetup.setProperty("key", "value");
+ mDeviceSetup.setDeprecatedSetProp("key=value");
+ try {
+ mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+ fail("TargetSetupError expected");
+ } catch (TargetSetupError e) {
+ // Expected
+ }
}
public void testTearDown() throws Exception {
- EasyMock.expect(mMockDevice.isWifiEnabled()).andReturn(Boolean.TRUE);
EasyMock.replay(mMockDevice);
-
mDeviceSetup.tearDown(mMockDevice, mMockBuildInfo, null);
+ EasyMock.verify(mMockDevice);
}
public void testTearDown_disconnectFromWifi() throws Exception {
@@ -187,7 +856,58 @@
EasyMock.expect(mMockDevice.disconnectFromWifi()).andReturn(Boolean.TRUE);
mDeviceSetup.setWifiNetwork("wifi_network");
EasyMock.replay(mMockDevice);
-
mDeviceSetup.tearDown(mMockDevice, mMockBuildInfo, null);
+ EasyMock.verify(mMockDevice);
+ }
+
+ /**
+ * Set EasyMock expectations for a normal setup call
+ */
+ private void doSetupExpectations() throws DeviceNotAvailableException {
+ doSetupExpectations(true, new Capture<String>());
+ }
+
+ /**
+ * Set EasyMock expectations for a normal setup call
+ */
+ private void doSetupExpectations(boolean screenOn, Capture<String> setPropCapture)
+ throws DeviceNotAvailableException {
+ EasyMock.expect(mMockDevice.enableAdbRoot()).andReturn(Boolean.TRUE);
+ EasyMock.expect(mMockDevice.clearErrorDialogs()).andReturn(Boolean.TRUE);
+ // expect push of local.prop file to change system properties
+ EasyMock.expect(mMockDevice.pushString(EasyMock.capture(setPropCapture),
+ EasyMock.contains("local.prop"))).andReturn(Boolean.TRUE);
+ EasyMock.expect(mMockDevice.executeShellCommand(
+ EasyMock.matches("chmod 644 .*local.prop"))).andReturn("");
+ mMockDevice.reboot();
+ if (screenOn) {
+ EasyMock.expect(mMockDevice.executeShellCommand("svc power stayon true")).andReturn("");
+ }
+ }
+
+ /**
+ * Perform common EasyMock expect operations for a setUp call which syncs local data
+ */
+ private void doSyncDataExpectations(boolean result) throws DeviceNotAvailableException {
+ mDeviceSetup.setLocalDataPath(mTmpDir);
+ EasyMock.expect(mMockDevice.getIDevice()).andReturn(mMockIDevice);
+ String mntPoint = "/sdcard";
+ EasyMock.expect(mMockIDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE)).andReturn(
+ mntPoint);
+ EasyMock.expect(mMockDevice.syncFiles(mTmpDir, mntPoint)).andReturn(result);
+ }
+
+ private void doCheckExternalStoreSpaceExpectations() throws DeviceNotAvailableException {
+ EasyMock.expect(mMockDevice.getExternalStoreFreeSpace()).andReturn(1000l);
+ }
+
+ private void doCommandsExpectations(boolean settings, String... commands)
+ throws DeviceNotAvailableException {
+ if (settings) {
+ EasyMock.expect(mMockDevice.getApiLevel()).andReturn(22);
+ }
+ for (String command : commands) {
+ EasyMock.expect(mMockDevice.executeShellCommand(command)).andReturn("");
+ }
}
}
diff --git a/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java b/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java
index 031528d..ef9ef7a 100644
--- a/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java
@@ -16,7 +16,6 @@
package com.android.tradefed.targetprep;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.build.DeviceBuildInfo;
import com.android.tradefed.build.IDeviceBuildInfo;
@@ -61,8 +60,8 @@
public void run(ITestInvocationListener listener) {
listener.testRunStarted(packageName, 1);
listener.testStarted(test);
- listener.testEnded(test, Collections.EMPTY_MAP);
- listener.testRunEnded(0, Collections.EMPTY_MAP);
+ listener.testEnded(test, Collections.<String, String>emptyMap());
+ listener.testRunEnded(0, Collections.<String, String>emptyMap());
}
};
mInstrumentationPreparer = new InstrumentationPreparer() {
@@ -75,7 +74,7 @@
mInstrumentationPreparer.setUp(mMockDevice, mMockBuildInfo);
}
- public void testRun_testError() throws Exception {
+ public void testRun_testFailed() throws Exception {
final String packageName = "packageName";
final TestIdentifier test = new TestIdentifier("FooTest", "testFoo");
mMockITest = new InstrumentationTest() {
@@ -83,9 +82,9 @@
public void run(ITestInvocationListener listener) {
listener.testRunStarted(packageName, 1);
listener.testStarted(test);
- listener.testFailed(TestFailure.ERROR, test, null);
- listener.testEnded(test, Collections.EMPTY_MAP);
- listener.testRunEnded(0, Collections.EMPTY_MAP);
+ listener.testFailed(test, null);
+ listener.testEnded(test, Collections.<String, String>emptyMap());
+ listener.testRunEnded(0, Collections.<String, String>emptyMap());
}
};
mInstrumentationPreparer = new InstrumentationPreparer() {
diff --git a/tests/src/com/android/tradefed/targetprep/TestFilePushSetupFuncTest.java b/tests/src/com/android/tradefed/targetprep/TestFilePushSetupFuncTest.java
index 2c181f1..7cfec7d 100644
--- a/tests/src/com/android/tradefed/targetprep/TestFilePushSetupFuncTest.java
+++ b/tests/src/com/android/tradefed/targetprep/TestFilePushSetupFuncTest.java
@@ -22,6 +22,7 @@
import com.android.tradefed.device.StubTestDevice;
import com.android.tradefed.util.FakeTestsZipFolder;
import com.android.tradefed.util.FakeTestsZipFolder.ItemType;
+import com.google.common.io.Files;
import junit.framework.TestCase;
@@ -39,6 +40,9 @@
private Map<String, ItemType> mFiles;
private List<String> mDeviceLocationList;
private FakeTestsZipFolder mFakeTestsZipFolder;
+ private File mAltDirFile1, mAltDirFile2;
+ private static final String ALT_FILENAME1 = "foobar";
+ private static final String ALT_FILENAME2 = "barfoo";
@Override
protected void setUp() throws Exception {
@@ -47,12 +51,18 @@
mFiles.put("app/AndroidCommonTests.apk", ItemType.FILE);
mFiles.put("app/GalleryTests.apk", ItemType.FILE);
mFiles.put("testinfo", ItemType.DIRECTORY);
+ mFiles.put(ALT_FILENAME1, ItemType.FILE);
mFakeTestsZipFolder = new FakeTestsZipFolder(mFiles);
assertTrue(mFakeTestsZipFolder.createItems());
mDeviceLocationList = new ArrayList<String>();
for (String file : mFiles.keySet()) {
mDeviceLocationList.add(TestFilePushSetup.getDevicePathFromUserData(file));
}
+ File tmpBase = Files.createTempDir();
+ mAltDirFile1 = new File(tmpBase, ALT_FILENAME1);
+ assertTrue("failed to create temp file", mAltDirFile1.createNewFile());
+ mAltDirFile2 = new File(tmpBase, ALT_FILENAME2);
+ assertTrue("failed to create temp file", mAltDirFile2.createNewFile());
}
public void testSetup() throws TargetSetupError, BuildError, DeviceNotAvailableException {
@@ -86,9 +96,55 @@
assertTrue(mDeviceLocationList.isEmpty());
}
+ /**
+ * Test that the artifact path resolution logic correctly uses alt dir as override
+ * @throws Exception
+ */
+ public void testAltDirOverride() throws Exception {
+ TestFilePushSetup setup = new TestFilePushSetup();
+ setup.setAltDirBehavior(AltDirBehavior.OVERRIDE);
+ DeviceBuildInfo info = new DeviceBuildInfo();
+ info.setTestsDir(mFakeTestsZipFolder.getBasePath(), "0");
+ setup.setAltDir(mAltDirFile1.getParentFile());
+ File apk = setup.getLocalPathForFilename(info, ALT_FILENAME1);
+ assertEquals(mAltDirFile1.getAbsolutePath(), apk.getAbsolutePath());
+ }
+
+ /**
+ * Test that the artifact path resolution logic correctly favors the one in test dir
+ * @throws Exception
+ */
+ public void testAltDirNoFallback() throws Exception {
+ TestFilePushSetup setup = new TestFilePushSetup();
+ DeviceBuildInfo info = new DeviceBuildInfo();
+ info.setTestsDir(mFakeTestsZipFolder.getBasePath(), "0");
+ setup.setAltDir(mAltDirFile1.getParentFile());
+ File apk = setup.getLocalPathForFilename(info, ALT_FILENAME1);
+ File apkInTestDir = new File(
+ new File(mFakeTestsZipFolder.getBasePath(), "DATA"), ALT_FILENAME1);
+ assertEquals(apkInTestDir.getAbsolutePath(), apk.getAbsolutePath());
+ }
+
+ /**
+ * Test that the artifact path resolution logic correctly falls back to alt dir
+ * @throws Exception
+ */
+ public void testAltDirFallback() throws Exception {
+ TestFilePushSetup setup = new TestFilePushSetup();
+ DeviceBuildInfo info = new DeviceBuildInfo();
+ info.setTestsDir(mFakeTestsZipFolder.getBasePath(), "0");
+ setup.setAltDir(mAltDirFile2.getParentFile());
+ File apk = setup.getLocalPathForFilename(info, ALT_FILENAME2);
+ assertEquals(mAltDirFile2.getAbsolutePath(), apk.getAbsolutePath());
+ }
+
@Override
protected void tearDown() throws Exception {
mFakeTestsZipFolder.cleanUp();
+ File tmpDir = mAltDirFile1.getParentFile();
+ mAltDirFile1.delete();
+ mAltDirFile2.delete();
+ tmpDir.delete();
super.tearDown();
}
}
diff --git a/tests/src/com/android/tradefed/testtype/DeviceTestCaseTest.java b/tests/src/com/android/tradefed/testtype/DeviceTestCaseTest.java
index b99a6a1..423e1d8 100644
--- a/tests/src/com/android/tradefed/testtype/DeviceTestCaseTest.java
+++ b/tests/src/com/android/tradefed/testtype/DeviceTestCaseTest.java
@@ -15,7 +15,6 @@
*/
package com.android.tradefed.testtype;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.result.ITestInvocationListener;
@@ -101,7 +100,7 @@
final TestIdentifier test1 = new TestIdentifier(MockAbortTest.class.getName(), "test1");
listener.testRunStarted(MockAbortTest.class.getName(), 1);
listener.testStarted(test1);
- listener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(test1),
+ listener.testFailed(EasyMock.eq(test1),
EasyMock.contains(MockAbortTest.EXCEP_MSG));
listener.testEnded(test1, Collections.EMPTY_MAP);
listener.testRunFailed(EasyMock.contains(MockAbortTest.EXCEP_MSG));
diff --git a/tests/src/com/android/tradefed/testtype/DeviceTestSuiteTest.java b/tests/src/com/android/tradefed/testtype/DeviceTestSuiteTest.java
index 0d8680a..229e911 100644
--- a/tests/src/com/android/tradefed/testtype/DeviceTestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/DeviceTestSuiteTest.java
@@ -15,7 +15,6 @@
*/
package com.android.tradefed.testtype;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.result.ITestInvocationListener;
@@ -84,7 +83,7 @@
final TestIdentifier test1 = new TestIdentifier(MockAbortTest.class.getName(), "test1");
listener.testRunStarted(DeviceTestSuite.class.getName(), 1);
listener.testStarted(test1);
- listener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(test1),
+ listener.testFailed(EasyMock.eq(test1),
EasyMock.contains(MockAbortTest.EXCEP_MSG));
listener.testEnded(test1, Collections.EMPTY_MAP);
listener.testRunFailed(EasyMock.contains(MockAbortTest.EXCEP_MSG));
diff --git a/tests/src/com/android/tradefed/testtype/FakeTestTest.java b/tests/src/com/android/tradefed/testtype/FakeTestTest.java
index ad620b3..2d05367 100644
--- a/tests/src/com/android/tradefed/testtype/FakeTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/FakeTestTest.java
@@ -16,7 +16,6 @@
package com.android.tradefed.testtype;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.result.ITestInvocationListener;
@@ -164,31 +163,17 @@
EasyMock.verify(mListener);
}
- public void testRun_simpleError() throws Exception {
- final String name = "com.moo.cow";
- mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(1));
- testErrorExpectations(mListener, name, 1);
- mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
-
- EasyMock.replay(mListener);
- mOption.setOptionMapValue("run", name, "E");
- mTest.run(mListener);
- EasyMock.verify(mListener);
- }
-
public void testRun_basicSequence() throws Exception {
final String name = "com.moo.cow";
int i = 1;
- mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(5));
+ mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(3));
testPassExpectations(mListener, name, i++);
testFailExpectations(mListener, name, i++);
- testErrorExpectations(mListener, name, i++);
- testFailExpectations(mListener, name, i++);
testPassExpectations(mListener, name, i++);
mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
EasyMock.replay(mListener);
- mOption.setOptionMapValue("run", name, "PFEFP");
+ mOption.setOptionMapValue("run", name, "PFP");
mTest.run(mListener);
EasyMock.verify(mListener);
}
@@ -212,7 +197,7 @@
public void testRun_recursiveParens() throws Exception {
final String name = "com.moo.cow";
int i = 1;
- mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(11));
+ mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(8));
testPassExpectations(mListener, name, i++);
testFailExpectations(mListener, name, i++);
@@ -225,14 +210,10 @@
testPassExpectations(mListener, name, i++);
testFailExpectations(mListener, name, i++);
- testErrorExpectations(mListener, name, i++);
- testErrorExpectations(mListener, name, i++);
- testErrorExpectations(mListener, name, i++);
-
mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
EasyMock.replay(mListener);
- mOption.setOptionMapValue("run", name, "((PF)2)2E3");
+ mOption.setOptionMapValue("run", name, "((PF)2)2");
mTest.run(mListener);
EasyMock.verify(mListener);
}
@@ -240,16 +221,14 @@
public void testMultiRun() throws Exception {
final String name1 = "com.moo.cow";
int i = 1;
- mListener.testRunStarted(EasyMock.eq(name1), EasyMock.eq(3));
+ mListener.testRunStarted(EasyMock.eq(name1), EasyMock.eq(2));
testPassExpectations(mListener, name1, i++);
testFailExpectations(mListener, name1, i++);
- testErrorExpectations(mListener, name1, i++);
mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
final String name2 = "com.quack.duck";
i = 1;
- mListener.testRunStarted(EasyMock.eq(name2), EasyMock.eq(3));
- testErrorExpectations(mListener, name2, i++);
+ mListener.testRunStarted(EasyMock.eq(name2), EasyMock.eq(2));
testFailExpectations(mListener, name2, i++);
testPassExpectations(mListener, name2, i++);
mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
@@ -261,8 +240,8 @@
mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
EasyMock.replay(mListener);
- mOption.setOptionMapValue("run", name1, "PFE");
- mOption.setOptionMapValue("run", name2, "EFP");
+ mOption.setOptionMapValue("run", name1, "PF");
+ mOption.setOptionMapValue("run", name2, "FP");
mOption.setOptionMapValue("run", name3, "");
mTest.run(mListener);
EasyMock.verify(mListener);
@@ -281,18 +260,7 @@
final String name = String.format("testMethod%d", idx);
final TestIdentifier test = new TestIdentifier(klass, name);
l.testStarted(test);
- l.testFailed(EasyMock.eq(TestFailure.FAILURE), EasyMock.eq(test),
- EasyMock.<String>anyObject());
- l.testEnded(EasyMock.eq(test), EasyMock.<Map<String, String>>anyObject());
- }
-
- private void testErrorExpectations(ITestInvocationListener l, String klass,
- int idx) {
- final String name = String.format("testMethod%d", idx);
- final TestIdentifier test = new TestIdentifier(klass, name);
- l.testStarted(test);
- l.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(test),
- EasyMock.<String>anyObject());
+ l.testFailed(EasyMock.eq(test), EasyMock.<String>anyObject());
l.testEnded(EasyMock.eq(test), EasyMock.<Map<String, String>>anyObject());
}
}
diff --git a/tests/src/com/android/tradefed/testtype/GTestFuncTest.java b/tests/src/com/android/tradefed/testtype/GTestFuncTest.java
index ae00761..b930b02 100644
--- a/tests/src/com/android/tradefed/testtype/GTestFuncTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestFuncTest.java
@@ -17,7 +17,6 @@
package com.android.tradefed.testtype;
import com.android.ddmlib.Log;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.result.ITestInvocationListener;
@@ -80,7 +79,7 @@
mMockListener.testStarted(id);
if (testName.endsWith("Fail")) {
- mMockListener.testFailed(EasyMock.eq(TestFailure.FAILURE),
+ mMockListener.testFailed(
EasyMock.eq(id),
EasyMock.isA(String.class));
}
@@ -103,7 +102,7 @@
mGTest.setModuleName(NATIVE_TESTAPP_MODULE_NAME);
mMockListener.testRunStarted(NATIVE_TESTAPP_MODULE_NAME, 1);
mMockListener.testStarted(EasyMock.eq(testId));
- mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(testId),
+ mMockListener.testFailed(EasyMock.eq(testId),
EasyMock.isA(String.class));
mMockListener.testEnded(EasyMock.eq(testId), EasyMock.eq(emptyMap));
mMockListener.testRunFailed((String)EasyMock.anyObject());
diff --git a/tests/src/com/android/tradefed/testtype/GTestResultParserTest.java b/tests/src/com/android/tradefed/testtype/GTestResultParserTest.java
index 09aa151..cb50d59 100644
--- a/tests/src/com/android/tradefed/testtype/GTestResultParserTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestResultParserTest.java
@@ -170,7 +170,7 @@
(Map<String, String>)EasyMock.anyObject());
// test failure
mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
- mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.FAILURE),
+ mockRunListener.testFailed(
(TestIdentifier)EasyMock.anyObject(), (String)EasyMock.anyObject());
mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
(Map<String, String>)EasyMock.anyObject());
@@ -182,13 +182,13 @@
}
// 2 consecutive test failures
mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
- mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.FAILURE),
+ mockRunListener.testFailed(
(TestIdentifier)EasyMock.anyObject(), (String)EasyMock.anyObject());
mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
(Map<String, String>)EasyMock.anyObject());
mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
- mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.FAILURE),
+ mockRunListener.testFailed(
(TestIdentifier)EasyMock.anyObject(), EasyMock.matches(MESSAGE_OUTPUT));
mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
(Map<String, String>)EasyMock.anyObject());
@@ -223,7 +223,7 @@
(Map<String, String>)EasyMock.anyObject());
// test failure
mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
- mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.ERROR),
+ mockRunListener.testFailed(
(TestIdentifier)EasyMock.anyObject(), (String)EasyMock.anyObject());
mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
(Map<String, String>)EasyMock.anyObject());
@@ -235,7 +235,7 @@
}
// another test error
mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
- mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.ERROR),
+ mockRunListener.testFailed(
(TestIdentifier)EasyMock.anyObject(), (String)EasyMock.anyObject());
mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
(Map<String, String>)EasyMock.anyObject());
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java
index 45eda48..e546056 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java
@@ -15,28 +15,19 @@
*/
package com.android.tradefed.testtype;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.util.FileUtil;
import junit.framework.TestCase;
import org.easymock.EasyMock;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.List;
/**
* Unit tests for {@link InstrumentationSerialTest}.
@@ -143,7 +134,7 @@
// now expect test to be marked as failed
mMockListener.testStarted(test);
- mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(test),
+ mMockListener.testFailed(EasyMock.eq(test),
EasyMock.contains(runFailureMsg));
mMockListener.testEnded(test, Collections.EMPTY_MAP);
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestFuncTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestFuncTest.java
index 137a158..04e8d5c 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestFuncTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestFuncTest.java
@@ -17,8 +17,8 @@
package com.android.tradefed.testtype;
import com.android.ddmlib.Log;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.tradefed.TestAppConstants;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.result.CollectingTestListener;
@@ -49,7 +49,7 @@
mInstrumentationTest.setPackageName(TestAppConstants.TESTAPP_PACKAGE);
mInstrumentationTest.setDevice(getDevice());
// use no timeout by default
- mInstrumentationTest.setTestTimeout(-1);
+ mInstrumentationTest.setShellTimeout(-1);
// set to no rerun by default
mInstrumentationTest.setRerunMode(false);
mMockListener = EasyMock.createStrictMock(ITestInvocationListener.class);
@@ -88,7 +88,7 @@
mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
mMockListener.testStarted(EasyMock.eq(expectedTest));
// TODO: add stricter checking on stackTrace
- mMockListener.testFailed(EasyMock.eq(TestFailure.FAILURE), EasyMock.eq(expectedTest),
+ mMockListener.testFailed(EasyMock.eq(expectedTest),
(String)EasyMock.anyObject());
mMockListener.testEnded(EasyMock.eq(expectedTest),
(Map<String, String>)EasyMock.anyObject());
@@ -110,7 +110,7 @@
mInstrumentationTest.setMethodName(TestAppConstants.CRASH_TEST_METHOD);
mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
mMockListener.testStarted(EasyMock.eq(expectedTest));
- mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(expectedTest),
+ mMockListener.testFailed(EasyMock.eq(expectedTest),
(String)EasyMock.anyObject());
mMockListener.testEnded(EasyMock.eq(expectedTest),
(Map<String, String>)EasyMock.anyObject());
@@ -132,10 +132,10 @@
TestAppConstants.TIMEOUT_TEST_METHOD);
mInstrumentationTest.setClassName(TestAppConstants.TESTAPP_CLASS);
mInstrumentationTest.setMethodName(TestAppConstants.TIMEOUT_TEST_METHOD);
- mInstrumentationTest.setTestTimeout(timeout);
+ mInstrumentationTest.setShellTimeout(timeout);
mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
mMockListener.testStarted(EasyMock.eq(expectedTest));
- mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(expectedTest),
+ mMockListener.testFailed(EasyMock.eq(expectedTest),
(String)EasyMock.anyObject());
mMockListener.testEnded(EasyMock.eq(expectedTest),
(Map<String, String>)EasyMock.anyObject());
@@ -159,7 +159,7 @@
mInstrumentationTest.setMethodName(TestAppConstants.TIMEOUT_TEST_METHOD);
mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
mMockListener.testStarted(EasyMock.eq(expectedTest));
- mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(expectedTest),
+ mMockListener.testFailed(EasyMock.eq(expectedTest),
(String)EasyMock.anyObject());
mMockListener.testEnded(EasyMock.eq(expectedTest),
(Map<String, String>)EasyMock.anyObject());
@@ -194,7 +194,8 @@
CollectingTestListener uilistener = new CollectingTestListener();
uiTest.run(uilistener);
assertFalse(uilistener.hasFailedTests());
- assertEquals(TestAppConstants.UI_TOTAL_TESTS, uilistener.getNumPassedTests());
+ assertEquals(TestAppConstants.UI_TOTAL_TESTS,
+ uilistener.getNumTestsInState(TestStatus.PASSED));
}
/**
@@ -212,7 +213,7 @@
mInstrumentationTest.setMethodName(TestAppConstants.TIMEOUT_TEST_METHOD);
mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
mMockListener.testStarted(EasyMock.eq(expectedTest));
- mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(expectedTest),
+ mMockListener.testFailed(EasyMock.eq(expectedTest),
(String)EasyMock.anyObject());
mMockListener.testEnded(EasyMock.eq(expectedTest),
(Map<String, String>)EasyMock.anyObject());
@@ -251,7 +252,8 @@
CollectingTestListener uilistener = new CollectingTestListener();
uiTest.run(uilistener);
assertFalse(uilistener.hasFailedTests());
- assertEquals(TestAppConstants.UI_TOTAL_TESTS, uilistener.getNumPassedTests());
+ assertEquals(TestAppConstants.UI_TOTAL_TESTS,
+ uilistener.getNumTestsInState(TestStatus.PASSED));
}
/**
@@ -265,11 +267,12 @@
// run all tests in class
mInstrumentationTest.setClassName(TestAppConstants.TESTAPP_CLASS);
mInstrumentationTest.setRerunMode(true);
- mInstrumentationTest.setTestTimeout(1000);
+ mInstrumentationTest.setShellTimeout(1000);
CollectingTestListener listener = new CollectingTestListener();
mInstrumentationTest.run(listener);
assertEquals(TestAppConstants.TOTAL_TEST_CLASS_TESTS, listener.getNumTotalTests());
- assertEquals(TestAppConstants.TOTAL_TEST_CLASS_PASSED_TESTS, listener.getNumPassedTests());
+ assertEquals(TestAppConstants.TOTAL_TEST_CLASS_PASSED_TESTS,
+ listener.getNumTestsInState(TestStatus.PASSED));
}
/**
@@ -283,7 +286,7 @@
mInstrumentationTest.setClassName(TestAppConstants.CRASH_ON_INIT_TEST_CLASS);
mInstrumentationTest.setMethodName(TestAppConstants.CRASH_ON_INIT_TEST_METHOD);
mInstrumentationTest.setRerunMode(true);
- mInstrumentationTest.setTestTimeout(1000);
+ mInstrumentationTest.setShellTimeout(1000);
CollectingTestListener listener = new CollectingTestListener();
mInstrumentationTest.run(listener);
assertEquals(0, listener.getNumTotalTests());
@@ -303,7 +306,7 @@
mInstrumentationTest.setClassName(TestAppConstants.HANG_ON_INIT_TEST_CLASS);
mInstrumentationTest.setRerunMode(true);
- mInstrumentationTest.setTestTimeout(1000);
+ mInstrumentationTest.setShellTimeout(1000);
mInstrumentationTest.setCollectsTestsShellTimeout(2 * 1000);
CollectingTestListener listener = new CollectingTestListener();
mInstrumentationTest.run(listener);
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
index 0f44ab8..e7cb6d8 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
@@ -39,6 +39,7 @@
public class InstrumentationTestTest extends TestCase {
private static final int TEST_TIMEOUT = 0;
+ private static final long SHELL_TIMEOUT = 0;
private static final String TEST_PACKAGE_VALUE = "com.foo";
private static final String TEST_RUNNER_VALUE = ".FooRunner";
private static final TestIdentifier TEST1 = new TestIdentifier("Test", "test1");
@@ -107,15 +108,18 @@
return mMockRemoteRunner;
}
};
- mInstrumentationTest.setPackageName(TEST_PACKAGE_VALUE);
- mInstrumentationTest.setRunnerName(TEST_RUNNER_VALUE);
- mInstrumentationTest.setDevice(mMockTestDevice);
- // default to no rerun, for simplicity
- mInstrumentationTest.setRerunMode(false);
- // default to no timeout for simplicity
- mInstrumentationTest.setTestTimeout(TEST_TIMEOUT);
- mMockRemoteRunner.setMaxTimeToOutputResponse(0, TimeUnit.MILLISECONDS);
- mInstrumentationTest.setCollectsTestsShellTimeout(COLLECT_TESTS_SHELL_TIMEOUT);
+ mInstrumentationTest.setPackageName(TEST_PACKAGE_VALUE);
+ mInstrumentationTest.setRunnerName(TEST_RUNNER_VALUE);
+ mInstrumentationTest.setDevice(mMockTestDevice);
+ // default to no rerun, for simplicity
+ mInstrumentationTest.setRerunMode(false);
+ // default to no timeout for simplicity
+ mInstrumentationTest.setTestTimeout(TEST_TIMEOUT);
+ mInstrumentationTest.setShellTimeout(SHELL_TIMEOUT);
+ mMockRemoteRunner.setMaxTimeToOutputResponse(SHELL_TIMEOUT, TimeUnit.MILLISECONDS);
+ mMockRemoteRunner.addInstrumentationArg(InstrumentationTest.TEST_TIMEOUT_INST_ARGS_KEY,
+ Long.toString(SHELL_TIMEOUT));
+ mInstrumentationTest.setCollectsTestsShellTimeout(COLLECT_TESTS_SHELL_TIMEOUT);
}
/**
@@ -227,12 +231,8 @@
*/
public void testRun_rerunEmpty() throws Exception {
mInstrumentationTest.setRerunMode(true);
- // expect log only mode run first to collect tests
- mMockRemoteRunner.setLogOnly(true);
- mMockRemoteRunner.addInstrumentationArg(InstrumentationTest.DELAY_MSEC_ARG,
- Long.toString(mInstrumentationTest.getTestDelay()));
- mMockRemoteRunner.setMaxTimeToOutputResponse(
- COLLECT_TESTS_SHELL_TIMEOUT, TimeUnit.MILLISECONDS);
+ // expect test collection mode run first to collect tests
+ mMockRemoteRunner.setTestCollection(true);
// collect tests run
CollectTestAnswer collectTestResponse = new CollectTestAnswer() {
@Override
@@ -244,9 +244,8 @@
};
setCollectTestsExpectations(collectTestResponse);
// expect normal mode to be turned back on
- mMockRemoteRunner.setLogOnly(false);
- mMockRemoteRunner.removeInstrumentationArg(InstrumentationTest.DELAY_MSEC_ARG);
- mMockRemoteRunner.setMaxTimeToOutputResponse(0, TimeUnit.MILLISECONDS);
+ mMockRemoteRunner.setMaxTimeToOutputResponse(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+ mMockRemoteRunner.setTestCollection(false);
// note: expect run to not be reported
EasyMock.replay(mMockRemoteRunner, mMockTestDevice, mMockListener);
@@ -296,7 +295,9 @@
}
};
setRerunExpectations(firstRunResponse);
- mMockRemoteRunner.setMaxTimeToOutputResponse(0, TimeUnit.MILLISECONDS);
+ mMockRemoteRunner.setMaxTimeToOutputResponse(SHELL_TIMEOUT, TimeUnit.MILLISECONDS);
+ mMockRemoteRunner.addInstrumentationArg(InstrumentationTest.TEST_TIMEOUT_INST_ARGS_KEY,
+ Long.toString(SHELL_TIMEOUT));
EasyMock.replay(mMockRemoteRunner, mMockTestDevice, mMockListener);
try {
mInstrumentationTest.run(mMockListener);
@@ -308,23 +309,32 @@
EasyMock.verify(mMockRemoteRunner, mMockTestDevice, mMockListener);
}
-
+ /**
+ * Test that IllegalArgumentException is thrown when attempting run with negative timeout args.
+ */
+ public void testRun_negativeTimeouts() throws Exception {
+ mInstrumentationTest.setShellTimeout(-1);
+ mInstrumentationTest.setTestTimeout(-2);
+ EasyMock.replay(mMockRemoteRunner);
+ try {
+ mInstrumentationTest.run(mMockListener);
+ fail("IllegalArgumentException not thrown");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
/**
* Set EasyMock expectations for a run that fails.
*
- * @param firstRunResponse the behavior callback of the first run. It should perform callbacks
+ * @param firstRunAnswer the behavior callback of the first run. It should perform callbacks
* on listeners to indicate only TEST1 was run
*/
private void setRerunExpectations(RunTestAnswer firstRunAnswer)
throws DeviceNotAvailableException {
mInstrumentationTest.setRerunMode(true);
- // expect log only mode run first to collect tests
- mMockRemoteRunner.setLogOnly(true);
- mMockRemoteRunner.addInstrumentationArg(InstrumentationTest.DELAY_MSEC_ARG,
- Long.toString(mInstrumentationTest.getTestDelay()));
- mMockRemoteRunner.setMaxTimeToOutputResponse(
- COLLECT_TESTS_SHELL_TIMEOUT, TimeUnit.MILLISECONDS);
+ // expect test collection mode run first to collect tests
+ mMockRemoteRunner.setTestCollection(true);
CollectTestAnswer collectTestAnswer = new CollectTestAnswer() {
@Override
public Boolean answer(IRemoteAndroidTestRunner runner, ITestRunListener listener) {
@@ -340,10 +350,9 @@
};
setCollectTestsExpectations(collectTestAnswer);
- // now expect second run with log only mode off
- mMockRemoteRunner.setLogOnly(false);
- mMockRemoteRunner.removeInstrumentationArg(InstrumentationTest.DELAY_MSEC_ARG);
+ // now expect second run with test collection mode off
mMockRemoteRunner.setMaxTimeToOutputResponse(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+ mMockRemoteRunner.setTestCollection(false);
setRunTestExpectations(firstRunAnswer);
// now expect second run to run remaining test
diff --git a/tests/src/com/android/tradefed/util/AaptParserTest.java b/tests/src/com/android/tradefed/util/AaptParserTest.java
index b357222..d825c40 100644
--- a/tests/src/com/android/tradefed/util/AaptParserTest.java
+++ b/tests/src/com/android/tradefed/util/AaptParserTest.java
@@ -22,13 +22,31 @@
*/
public class AaptParserTest extends TestCase {
- public void testParsePackageName() {
+ public void testParsePackageNameVersionLabel() {
AaptParser p = new AaptParser();
p.parse("package: name='com.android.foo' versionCode='13' versionName='2.3'\n" +
"sdkVersion:'5'\n" +
+ "application-label:'Foo'\n" +
+ "application-label-fr:'Faa'\n"+
"uses-permission:'android.permission.INTERNET'");
assertEquals("com.android.foo", p.getPackageName());
assertEquals("13", p.getVersionCode());
assertEquals("2.3", p.getVersionName());
+ assertEquals("Foo", p.getLabel());
+ }
+
+ public void testParseVersionMultipleFieldsNoLabel() {
+ AaptParser p = new AaptParser();
+ p.parse("package: name='com.android.foo' versionCode='217173' versionName='1.7173' " +
+ "platformBuildVersionName=''\n" +
+ "install-location:'preferExternal'\n" +
+ "sdkVersion:'10'\n" +
+ "targetSdkVersion:'21'\n" +
+ "uses-permission: name='android.permission.INTERNET'\n" +
+ "uses-permission: name='android.permission.ACCESS_NETWORK_STATE'\n");
+ assertEquals("com.android.foo", p.getPackageName());
+ assertEquals("217173", p.getVersionCode());
+ assertEquals("1.7173", p.getVersionName());
+ assertEquals("com.android.foo", p.getLabel());
}
}
diff --git a/tests/src/com/android/tradefed/util/AbiFormatterTest.java b/tests/src/com/android/tradefed/util/AbiFormatterTest.java
index 520b222..67dea05 100644
--- a/tests/src/com/android/tradefed/util/AbiFormatterTest.java
+++ b/tests/src/com/android/tradefed/util/AbiFormatterTest.java
@@ -48,6 +48,7 @@
ITestDevice device = EasyMock.createMock(ITestDevice.class);
EasyMock.expect(device.getProperty("ro.product.cpu.abilist32")).andReturn(null);
+ EasyMock.expect(device.getProperty("ro.product.cpu.abi")).andReturn(null);
EasyMock.replay(device);
assertEquals(null, AbiFormatter.getDefaultAbi(device, "32"));
@@ -59,7 +60,35 @@
EasyMock.reset(device);
EasyMock.expect(device.getProperty(EasyMock.eq("ro.product.cpu.abilist64"))).andReturn("");
+ EasyMock.expect(device.getProperty("ro.product.cpu.abi")).andReturn(null);
EasyMock.replay(device);
assertEquals(null, AbiFormatter.getDefaultAbi(device, "64"));
}
+
+ /**
+ * Verify that {@link AbiFormatter#getSupportedAbis} works as expected.
+ */
+ public void testGetSupportedAbis() throws Exception {
+ ITestDevice device = EasyMock.createMock(ITestDevice.class);
+
+ EasyMock.expect(device.getProperty("ro.product.cpu.abilist32")).andReturn("abi1,abi2");
+ EasyMock.replay(device);
+ String[] supportedAbiArray = AbiFormatter.getSupportedAbis(device, "32");
+ assertEquals("abi1", supportedAbiArray[0]);
+ assertEquals("abi2", supportedAbiArray[1]);
+
+ EasyMock.reset(device);
+ EasyMock.expect(device.getProperty("ro.product.cpu.abilist32")).andReturn(null);
+ EasyMock.expect(device.getProperty("ro.product.cpu.abi")).andReturn("abi");
+ EasyMock.replay(device);
+ supportedAbiArray = AbiFormatter.getSupportedAbis(device, "32");
+ assertEquals("abi", supportedAbiArray[0]);
+
+ EasyMock.reset(device);
+ EasyMock.expect(device.getProperty("ro.product.cpu.abilist")).andReturn("");
+ EasyMock.expect(device.getProperty("ro.product.cpu.abi")).andReturn("abi");
+ EasyMock.replay(device);
+ supportedAbiArray = AbiFormatter.getSupportedAbis(device, "");
+ assertEquals("abi", supportedAbiArray[0]);
+ }
}
diff --git a/tests/src/com/android/tradefed/util/FileUtilFuncTest.java b/tests/src/com/android/tradefed/util/FileUtilFuncTest.java
index 21c9f4f..a7dc5ab 100644
--- a/tests/src/com/android/tradefed/util/FileUtilFuncTest.java
+++ b/tests/src/com/android/tradefed/util/FileUtilFuncTest.java
@@ -293,6 +293,22 @@
}
}
+ /**
+ * Verify {@link FileUtil#calculateMd5(File)} works.
+ * @throws IOException
+ */
+ public void testCalculateMd5() throws IOException {
+ final String source = "testtesttesttesttest";
+ final String md5 = "f317f682fafe0309c6a423af0b4efa59";
+ File tmpFile = FileUtil.createTempFile("testCalculateMd5", ".txt");
+ try {
+ FileUtil.writeToFile(source, tmpFile);
+ String actualMd5 = FileUtil.calculateMd5(tmpFile);
+ assertEquals(md5, actualMd5);
+ } finally {
+ FileUtil.deleteFile(tmpFile);
+ }
+ }
// Assertions
private String assertUnixPerms(File file, String expPerms) {
diff --git a/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java b/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java
index 3a29a04..00d098b 100644
--- a/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java
+++ b/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java
@@ -17,9 +17,9 @@
package com.android.tradefed.util;
import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.result.CollectingTestListener;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
import junit.framework.TestCase;
@@ -61,7 +61,7 @@
public void testParse() throws ParseException, IOException {
mParser.parse(extractTestXml("JUnitXmlParserTest_testParse.xml"));
assertEquals(3, mListener.getNumTotalTests());
- assertEquals(1, mListener.getNumFailedTests());
+ assertEquals(1, mListener.getNumAllFailedTests());
TestRunResult runData = mListener.getCurrentRunResults();
assertEquals("null", runData.getName());
assertTrue(runData.getTestResults().containsKey(new TestIdentifier("PassTest", "testPass")));
diff --git a/tests/src/com/android/tradefed/util/RunUtilFuncTest.java b/tests/src/com/android/tradefed/util/RunUtilFuncTest.java
index 749cde3..83669ca 100644
--- a/tests/src/com/android/tradefed/util/RunUtilFuncTest.java
+++ b/tests/src/com/android/tradefed/util/RunUtilFuncTest.java
@@ -132,4 +132,25 @@
StreamUtil.close(s);
}
}
+
+ /**
+ * Test case for {@link RunUtil#unsetEnvVariable(String key)}
+ */
+ public void testUnsetEnvVariable() {
+ long timeout = 200;
+ RunUtil runUtil = new RunUtil();
+ runUtil.setEnvVariable("bar", "foo");
+ // FIXME: this test case is not ideal, as it will only work on platforms that support
+ // printenv
+ CommandResult result = runUtil.runTimedCmd(timeout, "printenv", "bar");
+ assertTrue(result.getStatus() == CommandStatus.SUCCESS);
+ assertTrue("foo".equals(result.getStdout().trim()));
+
+ // remove env variable
+ runUtil.unsetEnvVariable("bar");
+ // printenv with non-exist variable will fail
+ result = runUtil.runTimedCmd(timeout, "printenv", "bar");
+ assertTrue(result.getStatus() == CommandStatus.FAILED);
+ assertTrue("".equals(result.getStdout().trim()));
+ }
}
diff --git a/tests/src/com/android/tradefed/util/RunUtilTest.java b/tests/src/com/android/tradefed/util/RunUtilTest.java
index 0d682b7..0bcdcb9 100644
--- a/tests/src/com/android/tradefed/util/RunUtilTest.java
+++ b/tests/src/com/android/tradefed/util/RunUtilTest.java
@@ -118,6 +118,19 @@
}
/**
+ * Verify that calling {@link RunUtil#unsetEnvVariable(String)} is not allowed on default
+ * instance.
+ */
+ public void testUnsetEnvVariable_default() {
+ try {
+ RunUtil.getDefault().unsetEnvVariable("foo");
+ fail("could unset env var on RunUtil.getDefault()");
+ } catch (Exception e) {
+ // expected
+ }
+ }
+
+ /**
* Test that {@link RunUtil#runEscalatingTimedRetry()} fails when operation continually fails,
* and that the maxTime variable is respected.
*/
@@ -158,4 +171,56 @@
assertEquals(maxTime, mSleepTime);
EasyMock.verify(mockRunnable);
}
+
+ /**
+ * Test a success case for {@link RunUtil#interrupt}.
+ */
+ public void testInterrupt() {
+ final String message = "it is alright now";
+ mRunUtil.allowInterrupt(true);
+ mRunUtil.interrupt(Thread.currentThread(), message);
+ try{
+ mRunUtil.sleep(1);
+ fail("RunInterruptedException was expected, but not thrown.");
+ } catch (final RunInterruptedException e) {
+ assertEquals(message, e.getMessage());
+ }
+ }
+
+ /**
+ * Test whether a {@link RunUtil#interrupt} call is respected when called while interrupts are
+ * not allowed.
+ */
+ public void testInterrupt_delayed() {
+ final String message = "it is alright now";
+ mRunUtil.allowInterrupt(false);
+ mRunUtil.interrupt(Thread.currentThread(), message);
+ mRunUtil.sleep(1);
+ mRunUtil.allowInterrupt(true);
+ try{
+ mRunUtil.sleep(1);
+ fail("RunInterruptedException was expected, but not thrown.");
+ } catch (final RunInterruptedException e) {
+ assertEquals(message, e.getMessage());
+ }
+ }
+
+ /**
+ * Test whether a {@link RunUtil#interrupt} call is respected when called multiple times.
+ */
+ public void testInterrupt_multiple() {
+ final String message1 = "it is alright now";
+ final String message2 = "without a fight";
+ final String message3 = "rock this town";
+ mRunUtil.allowInterrupt(true);
+ mRunUtil.interrupt(Thread.currentThread(), message1);
+ mRunUtil.interrupt(Thread.currentThread(), message2);
+ mRunUtil.interrupt(Thread.currentThread(), message3);
+ try{
+ mRunUtil.sleep(1);
+ fail("RunInterruptedException was expected, but not thrown.");
+ } catch (final RunInterruptedException e) {
+ assertEquals(message3, e.getMessage());
+ }
+ }
}
\ No newline at end of file
diff --git a/tests/src/com/android/tradefed/util/StreamUtilTest.java b/tests/src/com/android/tradefed/util/StreamUtilTest.java
index 5473acf..8033943 100644
--- a/tests/src/com/android/tradefed/util/StreamUtilTest.java
+++ b/tests/src/com/android/tradefed/util/StreamUtilTest.java
@@ -21,6 +21,7 @@
import junit.framework.TestCase;
import java.io.ByteArrayInputStream;
+import java.io.IOException;
import java.io.InputStream;
/**
@@ -82,5 +83,17 @@
new ByteArrayInputStream(contents.getBytes()));
assertEquals(contents, output);
}
+
+ /**
+ * Verify that {@link StreamUtil#calculateMd5(InputStream)} works as expected.
+ * @throws IOException
+ */
+ public void testCalculateMd5() throws IOException {
+ final String source = "testtesttesttesttest";
+ final String md5 = "f317f682fafe0309c6a423af0b4efa59";
+ ByteArrayInputStream inputSource = new ByteArrayInputStream(source.getBytes());
+ String actualMd5 = StreamUtil.calculateMd5(inputSource);
+ assertEquals(md5, actualMd5);
+ }
}
diff --git a/tests/src/com/android/tradefed/util/TimeValTest.java b/tests/src/com/android/tradefed/util/TimeValTest.java
new file mode 100644
index 0000000..75770a9
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/TimeValTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link TimeVal}.
+ */
+public class TimeValTest extends TestCase {
+
+ public void testBasic() {
+ assertEquals(0, TimeVal.fromString("0"));
+ assertEquals(1, TimeVal.fromString("1"));
+
+ assertEquals(1000, TimeVal.fromString("1000"));
+ assertEquals(1000, TimeVal.fromString("1000ms"));
+ assertEquals(1000, TimeVal.fromString("1000Ms"));
+ assertEquals(1000, TimeVal.fromString("1000mS"));
+ assertEquals(1000, TimeVal.fromString("1000MS"));
+
+ assertEquals(1000, TimeVal.fromString("1s"));
+ assertEquals(1000, TimeVal.fromString("1S"));
+
+ assertEquals(1000 * 60, TimeVal.fromString("1m"));
+ assertEquals(1000 * 60, TimeVal.fromString("1M"));
+
+ assertEquals(1000 * 3600, TimeVal.fromString("1h"));
+ assertEquals(1000 * 3600, TimeVal.fromString("1H"));
+
+ assertEquals(1000 * 86400, TimeVal.fromString("1d"));
+ assertEquals(1000 * 86400, TimeVal.fromString("1D"));
+ }
+
+ public void testComposition() {
+ assertEquals(1303, TimeVal.fromString("1s303"));
+ assertEquals(1000 * 60 + 303, TimeVal.fromString("1m303"));
+ assertEquals(1000 * 3600 + 303, TimeVal.fromString("1h303ms"));
+ assertEquals(1000 * 86400 + 303, TimeVal.fromString("1d303MS"));
+
+ assertEquals(4 * 1000 * 86400 + 5 * 1000 * 3600 + 303, TimeVal.fromString("4D5h303mS"));
+ assertEquals(5 + 1000 * (4 + 60 * (3 + 60 * (2 + 24 * 1))),
+ TimeVal.fromString("1d2h3m4s5ms"));
+ }
+
+ public void testWhitespace() {
+ assertEquals(1, TimeVal.fromString("1 ms"));
+ assertEquals(1, TimeVal.fromString(" 1 ms "));
+
+ assertEquals(1002, TimeVal.fromString("1s2"));
+ assertEquals(1002, TimeVal.fromString("1s2 ms"));
+ assertEquals(1002, TimeVal.fromString(" 1s2ms"));
+ assertEquals(1002, TimeVal.fromString("1s 2 ms"));
+ // This is non-ideal, but is a side-effect of discarding all whitespace prior to parsing
+ assertEquals(3 * 1000 * 60 + 1002, TimeVal.fromString(" 3 m 1 s 2 m s"));
+
+ assertEquals(5 + 1000 * (4 + 60 * (3 + 60 * (2 + 24 * 1))),
+ TimeVal.fromString("1d 2h 3m 4s 5ms"));
+ }
+
+ public void testInvalid() {
+ assertInvalid("-1");
+ assertInvalid("1m1h");
+ assertInvalid("+5");
+ }
+
+ /**
+ * Because of TimeVal's multiplication features, it can be easier for a user to unexpectedly
+ * trigger an overflow without realizing that their value has become quite so large. Here,
+ * we verify that various kinds of overflows and ensure that we detect them.
+ */
+ public void testOverflow() {
+ // 2**63 - 1
+ assertEquals(Long.MAX_VALUE, TimeVal.fromString("9223372036854775807"));
+ // 2**63
+ assertInvalid("9223372036854775808");
+
+ // (2**63 - 1).divmod(1000 * 86400) = [106751991167, 25975807] (days, msecs)
+ // This should be the greatest number of days that does not overflow
+ assertEquals(106751991167L * 1000L * 86400L, TimeVal.fromString("106751991167d"));
+ // This should be 2**63 - 1
+ assertEquals(Long.MAX_VALUE, TimeVal.fromString("106751991167d 25975807ms"));
+ // Adding 1 more ms should cause an overflow.
+ assertInvalid("106751991167d 25975808ms");
+
+ // 2**64 + 1 should be a positive value after an overflow. Make sure we can still detect
+ // this non-negative overflow
+ long l = 1<<62;
+ l *= 2;
+ l *= 2;
+ l += 1;
+ assertTrue(l > 0);
+ // 2**64 + 1 == 18446744073709551617
+ assertInvalid("18446744073709551617ms");
+ }
+
+ private void assertInvalid(String input) {
+ try {
+ final long val = TimeVal.fromString(input);
+ fail(String.format("Did not reject input: %s. Produced value: %d", input, val));
+ } catch (NumberFormatException e) {
+ // expected
+ }
+ }
+}
diff --git a/tests/src/com/android/tradefed/util/net/HttpHelperTest.java b/tests/src/com/android/tradefed/util/net/HttpHelperTest.java
index 0d0cc41..ce417e6 100644
--- a/tests/src/com/android/tradefed/util/net/HttpHelperTest.java
+++ b/tests/src/com/android/tradefed/util/net/HttpHelperTest.java
@@ -126,6 +126,18 @@
}
/**
+ * Normal case test for {@link HttpHelper#doGet(String, OutputStream)}
+ */
+ public void testDoGetStream() throws IOException, DataSizeException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ mHelper.doGet(TEST_URL_STRING, out);
+ StreamUtil.flushAndCloseStream(out);
+
+ assertEquals(TEST_DATA, out.toString());
+ }
+
+ /**
* Normal case test for {@link HttpHelper#doGetWithRetry(String)}.
*/
public void testDoGetWithRetry() throws IOException, DataSizeException {
diff --git a/tradefed.sh b/tradefed.sh
index 3b63625..804e9d2 100755
--- a/tradefed.sh
+++ b/tradefed.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-# Copyright (C) 2010 The Android Open Source Project
+# Copyright (C) 2015 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,80 +14,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# A helper script that launches TradeFederation from the current build
-# environment.
+# A helper script that launches Trade Federation
-checkPath() {
- if ! type -P $1 &> /dev/null; then
- echo "Unable to find $1 in path."
- exit
- fi;
-}
-
-checkFile() {
- if [ ! -f "$1" ]; then
- echo "Unable to locate $1"
- exit
- fi;
-}
-
+shdir=`dirname $0`/
+source "${shdir}/script_help.sh"
+# At this point, we're guaranteed to have the right Java version, and the following
+# env variables will be set, if appropriate:
+# JAVA_VERSION, RDBG_FLAG, TF_PATH, TRADEFED_OPTS
checkPath adb
-checkPath java
-# check java version
-java_version_string=$(java -version 2>&1)
-JAVA_VERSION=$(echo "$java_version_string" | grep '[ "]1\.[67][\. "$$]')
-if [ "${JAVA_VERSION}" == "" ]; then
- echo "Wrong java version. 1.7 is required."
- exit
-else
- # We have 1.6 or 1.7. Now print a warning if the version was 1.6
- java_version_16=$(echo "$java_version_string" | grep '[ "]1\.6[\. "$$]')
- if [ "${java_version_16}" != "" ]; then
- echo "DEPRECATION WARNING: Please update your java to version 1.7."
- echo
- fi
-fi
-# check debug flag and set up remote debugging
-if [ -n "${TF_DEBUG}" ]; then
- if [ -z "${TF_DEBUG_PORT}" ]; then
- TF_DEBUG_PORT=10088
- fi
- RDBG_FLAG="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=${TF_DEBUG_PORT}"
-fi
-
-# first try to find TF jars in same dir as this script
-CUR_DIR=$(dirname "$0")
-if [ -f "${CUR_DIR}/tradefed.jar" ]; then
- tf_path="${CUR_DIR}/*"
-elif [ ! -z "${ANDROID_HOST_OUT}" ]; then
- # in an Android build env, tradefed.jar should be in
- # $ANDROID_HOST_OUT/tradefed/
- if [ -f "${ANDROID_HOST_OUT}/tradefed/tradefed.jar" ]; then
- # We intentionally pass the asterisk through without shell expansion
- tf_path="${ANDROID_HOST_OUT}/tradefed/*"
- # ddmlib-prebuilt is in the framework subdir
- ddmlib_path="${ANDROID_HOST_OUT}/framework/ddmlib-prebuilt.jar"
- fi
-fi
-
-if [ -z "${tf_path}" ]; then
- echo "ERROR: Could not find tradefed jar files"
- exit
-fi
-
-# set any host specific options
-# file format for file at $TRADEFED_OPTS_FILE is one line per host with the following format:
-# <hostname>=<options>
-# for example:
-# hostname.domain.com=-Djava.io.tmpdir=/location/on/disk -Danother=false ...
-# hostname2.domain.com=-Djava.io.tmpdir=/different/location -Danother=true ...
-if [ -e "${TRADEFED_OPTS_FILE}" ]; then
- # pull the line for this host and take everything after the first =
- export TRADEFED_OPTS=`grep "^$HOSTNAME=" "$TRADEFED_OPTS_FILE" | cut -d '=' -f 2-`
-fi
-
-# Note: must leave ${RDBG_FLAG} unquoted so that it goes away when unset
-java ${RDBG_FLAG} -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow $TRADEFED_OPTS \
- -cp "${ddmlib_path}:${tf_path}" com.android.tradefed.command.Console "$@"
+# Note: must leave $RDBG_FLAG and $TRADEFED_OPTS unquoted so that they go away when unset
+java $RDBG_FLAG -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow $TRADEFED_OPTS \
+ -cp "${TF_PATH}" com.android.tradefed.command.Console "$@"
diff --git a/tradefed_win.bat b/tradefed_win.bat
new file mode 100755
index 0000000..165516e
--- /dev/null
+++ b/tradefed_win.bat
@@ -0,0 +1,103 @@
+@echo off
+
+:: Copyright (C) 2014 The Android Open Source Project
+
+:: Licensed under the Apache License, Version 2.0 (the "License");
+:: you may not use this file except in compliance with the License.
+:: You may obtain a copy of the License at
+
+:: http://www.apache.org/licenses/LICENSE-2.0
+
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+
+:: A helper script that launches TradeFederation from the current build
+:: environment.
+
+setlocal EnableDelayedExpansion
+call:checkCommand adb
+call:checkCommand java
+
+:: check java version
+set JAVA_VERSION=
+
+for /f "delims=" %%j in ('java -version 2^>^&1 ^| findstr /i """1.7"') do (
+ set JAVA_VERSION=7
+)
+
+if "%JAVA_VERSION%" == "" (
+ echo "Wrong java version. 1.7 is required."
+ exit /B
+)
+
+:: check debug flag and set up remote debugging
+if not "%TF_DEBUG%"=="" (
+ if "%TF_DEBUG_PORT%" == "" (
+ set TF_DEBUG_PORT=10088
+ )
+ set RDBG_FLAG=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=!TF_DEBUG_PORT!
+)
+
+:: first try to find TF jars in same dir as this script
+set CUR_DIR=%CD%
+
+if exist "%CUR_DIR%\tradefed.jar" (
+ set tf_path="%CUR_DIR%\*"
+) else (
+ if not "%ANDROID_HOST_OUT%" == "" (
+ if exist "%ANDROID_HOST_OUT%\tradefed\tradefed.jar" (
+ set tf_path="%ANDROID_HOST_OUT%\tradefed\*"
+ )
+ )
+)
+
+if "%tf_path%" == "" (
+ echo "ERROR: Could not find tradefed jar files"
+ exit /B
+)
+
+:: set any host specific options
+:: file format for file at $TRADEFED_OPTS_FILE is one line per host with the following format:
+:: <hostname>=<options>
+:: for example:
+:: hostname.domain.com=-Djava.io.tmpdir=/location/on/disk -Danother=false ...
+:: hostname2.domain.com=-Djava.io.tmpdir=/different/location -Danother=true ...
+if exist "%TRADEFED_OPTS_FILE%" (
+ call:commandResult "hostname" HOST_NAME
+ call:commandResult "findstr /i /b "%HOST_NAME%" "%TRADEFED_OPTS_FILE%"" TRADEFED_OPTS
+:: delete the hostname part
+ set TRADEFED_OPTS=!TRADEFED_OPTS:%HOST_NAME%=!
+:: delete the first =
+ set TRADEFED_OPTS=!TRADEFED_OPTS:~1!
+)
+
+java %RDBG_FLAG% -XX:+HeapDumpOnOutOfMemoryError ^
+-XX:-OmitStackTraceInFastThrow %TRADEFED_OPTS% -cp %tf_path% com.android.tradefed.command.Console %*
+
+endlocal
+::end of file
+goto:eof
+
+:: check command exist or not
+:: if command not exist, exit
+:checkCommand
+for /f "delims=" %%i in ('where %~1') do (
+ if %%i == "" (
+ echo %~1 not exist
+ exit /B
+ )
+ goto:eof
+)
+goto:eof
+
+:: get the command result
+:: usage: call:commandResult "command" result
+:commandResult
+for /f "delims=" %%i in ('%~1') do (
+ set %~2=%%i
+ goto:eof
+)
+goto:eof
diff --git a/util-apps/WifiUtil/AndroidManifest.xml b/util-apps/WifiUtil/AndroidManifest.xml
index 83fa0a4..353b57c 100644
--- a/util-apps/WifiUtil/AndroidManifest.xml
+++ b/util-apps/WifiUtil/AndroidManifest.xml
@@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-sdk android:minSdkVersion="3"
android:targetSdkVersion="7" />
diff --git a/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java b/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
index d8b6c8a..16115dc 100644
--- a/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
+++ b/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
@@ -42,6 +42,7 @@
private static final String TAG = WifiConnector.class.getSimpleName();
private static final long DEFAULT_TIMEOUT = 30 * 1000;
+ private static final long DEFAULT_WAIT_TIME = 5 * 1000;
private static final long POLL_TIME = 1000;
private Context mContext;
@@ -190,6 +191,15 @@
}
}, "failed to enable wifi");
+ // Wait for some seconds to let wifi to be stable. This increases the chance of success for
+ // subsequent operations.
+ try {
+ Thread.sleep(DEFAULT_WAIT_TIME);
+ } catch (InterruptedException e) {
+ throw new WifiException(String.format("failed to sleep for %d ms", DEFAULT_WAIT_TIME),
+ e);
+ }
+
removeAllNetworks(false);
final int networkId = addNetwork(ssid, psk);
diff --git a/verify.sh b/verify.sh
new file mode 100755
index 0000000..7484194
--- /dev/null
+++ b/verify.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A helper script that launches Trade Federation's "Verify" entrypoint, to perform
+# standalone command file verification
+
+shdir=`dirname $0`/
+source "${shdir}/script_help.sh"
+# At this point, we're guaranteed to have the right Java version, and the following
+# env variables will be set, if appropriate:
+# JAVA_VERSION, RDBG_FLAG, TF_PATH, TRADEFED_OPTS
+
+
+# Note: must leave $RDBG_FLAG and $TRADEFED_OPTS unquoted so that they go away when unset
+java $RDBG_FLAG -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow $TRADEFED_OPTS \
+ -cp "${TF_PATH}" com.android.tradefed.command.Verify "$@"