aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKevin Jin <kjin@google.com>2013-10-23 14:08:12 -0700
committerKevin Jin <kjin@google.com>2013-10-23 15:33:19 -0700
commit45828d52e6a2d9694eb507b5cafd3b6fcae9c33c (patch)
treea85ff402792094377f7f93863ec3d1b2b628b863 /src
parent07faa170442ca86bb21e6076ff3309615be3b9a8 (diff)
downloaddroiddriver-45828d52e6a2d9694eb507b5cafd3b6fcae9c33c.tar.gz
add helpers: DroidDrivers, BaseDroidDriverTest, UnrecoverableFailure
add By.withChild and By.withDescendant Change-Id: If739957750074fefa3450903d8b866c62c4390a5
Diffstat (limited to 'src')
-rw-r--r--src/com/google/android/droiddriver/DroidDriverBuilder.java1
-rw-r--r--src/com/google/android/droiddriver/finders/By.java58
-rw-r--r--src/com/google/android/droiddriver/helpers/BaseDroidDriverTest.java171
-rw-r--r--src/com/google/android/droiddriver/helpers/DroidDrivers.java94
-rw-r--r--src/com/google/android/droiddriver/helpers/UnrecoverableFailure.java33
-rw-r--r--src/com/google/android/droiddriver/instrumentation/RootFinder.java25
-rw-r--r--src/com/google/android/droiddriver/runner/TestRunner.java3
7 files changed, 365 insertions, 20 deletions
diff --git a/src/com/google/android/droiddriver/DroidDriverBuilder.java b/src/com/google/android/droiddriver/DroidDriverBuilder.java
index 87ecafc..0eed095 100644
--- a/src/com/google/android/droiddriver/DroidDriverBuilder.java
+++ b/src/com/google/android/droiddriver/DroidDriverBuilder.java
@@ -27,6 +27,7 @@ import com.google.common.base.Preconditions;
/**
* Builds DroidDriver instances.
*/
+@Deprecated
public class DroidDriverBuilder {
public enum Implementation {
ANY, INSTRUMENTATION, UI_AUTOMATION
diff --git a/src/com/google/android/droiddriver/finders/By.java b/src/com/google/android/droiddriver/finders/By.java
index 9e6849d..b4e1a74 100644
--- a/src/com/google/android/droiddriver/finders/By.java
+++ b/src/com/google/android/droiddriver/finders/By.java
@@ -19,7 +19,7 @@ package com.google.android.droiddriver.finders;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.android.droiddriver.UiElement;
-import com.google.common.annotations.Beta;
+import com.google.android.droiddriver.exceptions.ElementNotFoundException;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
@@ -215,7 +215,6 @@ public class By {
* @param xPath The xpath to use
* @return a finder which locates elements via XPath
*/
- @Beta
public static ByXPath xpath(String xPath) {
return new ByXPath(xPath);
}
@@ -322,8 +321,8 @@ public class By {
}
/**
- * Matches a UiElement which has a sibling matching the given siblingFinder.
- * For complex cases, consider {@link #xpath}.
+ * Matches a UiElement which has a visible sibling matching the given
+ * siblingFinder. This could be inefficient; consider {@link #xpath}.
*/
public static MatchFinder withSibling(final MatchFinder siblingFinder) {
checkNotNull(siblingFinder);
@@ -334,8 +333,7 @@ public class By {
if (parent == null) {
return false;
}
- // Do not care if the sibling is visible
- for (UiElement sibling : parent.getChildren(null)) {
+ for (UiElement sibling : parent.getChildren(UiElement.VISIBLE)) {
if (sibling != element && siblingFinder.matches(sibling)) {
return true;
}
@@ -350,5 +348,53 @@ public class By {
};
}
+ /**
+ * Matches a UiElement which has a visible child matching the given
+ * childFinder. This could be inefficient; consider {@link #xpath}.
+ */
+ public static MatchFinder withChild(final MatchFinder childFinder) {
+ checkNotNull(childFinder);
+ return new MatchFinder(new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ for (UiElement child : element.getChildren(UiElement.VISIBLE)) {
+ if (childFinder.matches(child)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }) {
+ @Override
+ public String toString() {
+ return "withChild(" + childFinder + ")";
+ }
+ };
+ }
+
+ /**
+ * Matches a UiElement whose descendant matches the given descendantFinder.
+ * This could be VERY inefficient; consider {@link #xpath}.
+ */
+ public static MatchFinder withDescendant(final MatchFinder descendantFinder) {
+ checkNotNull(descendantFinder);
+ return new MatchFinder(new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ try {
+ descendantFinder.find(element);
+ return true;
+ } catch (ElementNotFoundException enfe) {
+ return false;
+ }
+ }
+ }) {
+ @Override
+ public String toString() {
+ return "withDescendant(" + descendantFinder + ")";
+ }
+ };
+ }
+
private By() {}
}
diff --git a/src/com/google/android/droiddriver/helpers/BaseDroidDriverTest.java b/src/com/google/android/droiddriver/helpers/BaseDroidDriverTest.java
new file mode 100644
index 0000000..b343d0f
--- /dev/null
+++ b/src/com/google/android/droiddriver/helpers/BaseDroidDriverTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.google.android.droiddriver.helpers;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Debug;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.util.FileUtils;
+import com.google.android.droiddriver.util.Logs;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.Thread.UncaughtExceptionHandler;
+
+/**
+ * Base class for tests using DroidDriver that handles uncaught exceptions, for
+ * example OOME, and takes screenshot on failure. It is NOT required, but
+ * provides handy utility methods.
+ */
+public abstract class BaseDroidDriverTest<T extends Activity> extends
+ ActivityInstrumentationTestCase2<T> {
+ private static boolean classSetUpDone = false;
+ // In case of device-wide fatal errors, e.g. OOME, the remaining tests will
+ // fail and the messages will not help, so skip them.
+ protected static boolean skipRemainingTests = false;
+ // Prevent crash by uncaught exception.
+ private static volatile Throwable uncaughtException;
+ static {
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ uncaughtException = ex;
+ }
+ });
+ }
+
+ protected DroidDriver driver;
+
+ protected BaseDroidDriverTest(Class<T> activityClass) {
+ super(activityClass);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ if (!classSetUpDone) {
+ classSetUp();
+ classSetUpDone = true;
+ }
+ driver = DroidDrivers.get();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ driver = null;
+ }
+
+ protected Context getTargetContext() {
+ return getInstrumentation().getTargetContext();
+ }
+
+ /**
+ * Initializes test fixture once for all tests extending this class. Typically
+ * you call {@link DroidDrivers#init} with an appropriate instance. If an
+ * InstrumentationDriver is used, this is a good place to call
+ * {@link com.google.android.droiddriver.instrumentation.ViewElement#overrideClassName}
+ */
+ protected abstract void classSetUp();
+
+ /**
+ * Takes a screenshot on failure.
+ */
+ @SuppressWarnings("finally")
+ protected void onFailure(Throwable failure) throws Throwable {
+ // Give uncaughtException (thrown by app instead of tests) high priority
+ if (uncaughtException != null) {
+ failure = uncaughtException;
+ uncaughtException = null;
+ skipRemainingTests = true;
+ }
+
+ try {
+ if (failure instanceof UnrecoverableFailure) {
+ skipRemainingTests = true;
+ }
+ if (failure instanceof OutOfMemoryError) {
+ dumpHprof();
+ } else {
+ String baseFileName = getBaseFileName();
+ driver.dumpUiElementTree(baseFileName + ".xml");
+ driver.getUiDevice().takeScreenshot(baseFileName + ".png");
+ }
+ } catch (Throwable e) {
+ // This method is for troubleshooting. Do not throw new error; we'll
+ // throw the original failure.
+ Logs.log(Log.WARN, e);
+ } finally {
+ throw failure;
+ }
+ }
+
+ /**
+ * Gets the base filename for troubleshooting files. For example, a screenshot
+ * is saved in the file "basename".png.
+ */
+ protected String getBaseFileName() {
+ return "dd/" + getClass().getSimpleName() + "." + getName();
+ }
+
+ protected void dumpHprof() throws IOException, FileNotFoundException {
+ skipRemainingTests = true;
+ String path = FileUtils.getAbsoluteFile(getBaseFileName() + ".hprof").getPath();
+ // create an empty readable file
+ FileUtils.open(path).close();
+ Debug.dumpHprofData(path);
+ }
+
+ /**
+ * Fixes JUnit3: always call tearDown even when setUp throws. Also calls
+ * {@link #onFailure}.
+ */
+ @Override
+ public void runBare() throws Throwable {
+ if (skipRemainingTests) {
+ return;
+ }
+ Throwable exception = null;
+ try {
+ setUp();
+ runTest();
+ } catch (Throwable runException) {
+ exception = runException;
+ // ActivityInstrumentationTestCase2.tearDown() finishes activity
+ // created by getActivity(), so call this before tearDown().
+ onFailure(exception);
+ } finally {
+ try {
+ tearDown();
+ } catch (Throwable tearDownException) {
+ if (exception == null) {
+ exception = tearDownException;
+ }
+ }
+ }
+ if (exception != null) {
+ throw exception;
+ }
+ if (uncaughtException != null) {
+ onFailure(uncaughtException);
+ }
+ }
+}
diff --git a/src/com/google/android/droiddriver/helpers/DroidDrivers.java b/src/com/google/android/droiddriver/helpers/DroidDrivers.java
new file mode 100644
index 0000000..a2a195b
--- /dev/null
+++ b/src/com/google/android/droiddriver/helpers/DroidDrivers.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.google.android.droiddriver.helpers;
+
+import android.app.Instrumentation;
+import android.os.Build;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.exceptions.DroidDriverException;
+import com.google.android.droiddriver.instrumentation.InstrumentationDriver;
+import com.google.android.droiddriver.uiautomation.UiAutomationDriver;
+
+/**
+ * Static utility methods pertaining to {@link DroidDriver} instances.
+ */
+public class DroidDrivers {
+ private static DroidDriver driver;
+
+ /**
+ * Gets the singleton driver. Throws if {@link #init} has not been called.
+ */
+ public static DroidDriver get() {
+ if (DroidDrivers.driver == null) {
+ throw new DroidDriverException("init() has not been called");
+ }
+ return DroidDrivers.driver;
+ }
+
+ /**
+ * Initializes the singleton driver. The singleton driver is NOT required, but
+ * it is handy and using a singleton driver can avoid memory leak if you have
+ * many instances around (for example, one in every test -- JUnit framework
+ * keeps the test instances in memory after running them).
+ */
+ public static void init(DroidDriver driver) {
+ if (DroidDrivers.driver != null) {
+ throw new DroidDriverException("init() can only be called once");
+ }
+ DroidDrivers.driver = driver;
+ }
+
+ /**
+ * Returns whether the running target (device or emulator) has
+ * {@link android.app.UiAutomation} API, which is introduced in SDK API 18
+ * (JELLY_BEAN_MR2).
+ */
+ public static boolean hasUiAutomation() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
+ }
+
+ /**
+ * Returns a new UiAutomationDriver if {@link android.app.UiAutomation} is
+ * available; otherwise a new InstrumentationDriver.
+ */
+ public static DroidDriver newDriver(Instrumentation instrumentation) {
+ if (hasUiAutomation()) {
+ return newUiAutomationDriver(instrumentation);
+ }
+ return newInstrumentationDriver(instrumentation);
+ }
+
+ /** Returns a new InstrumentationDriver */
+ public static InstrumentationDriver newInstrumentationDriver(Instrumentation instrumentation) {
+ return new InstrumentationDriver(instrumentation);
+ }
+
+ /** Returns a new UiAutomationDriver */
+ public static UiAutomationDriver newUiAutomationDriver(Instrumentation instrumentation) {
+ if (!hasUiAutomation()) {
+ throw new DroidDriverException(
+ "http://developer.android.com/reference/android/app/UiAutomation.html" +
+ " is not available below API 18");
+ }
+ if (instrumentation.getUiAutomation() == null) {
+ throw new DroidDriverException(
+ "uiAutomation==null: did you forget to set '-w' flag for 'am instrument'?");
+ }
+ return new UiAutomationDriver(instrumentation);
+ }
+}
diff --git a/src/com/google/android/droiddriver/helpers/UnrecoverableFailure.java b/src/com/google/android/droiddriver/helpers/UnrecoverableFailure.java
new file mode 100644
index 0000000..9adbac8
--- /dev/null
+++ b/src/com/google/android/droiddriver/helpers/UnrecoverableFailure.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.google.android.droiddriver.helpers;
+
+/**
+ * When an {@code UnrecoverableFailure} occurs, the rest of the tests are going
+ * to fail as well, therefore running them only adds noise to the report.
+ * {@link BaseDroidDriverTest} will skip remaining tests when this is thrown.
+ */
+@SuppressWarnings("serial")
+public class UnrecoverableFailure extends RuntimeException {
+ public UnrecoverableFailure(String message) {
+ super(message);
+ }
+
+ public UnrecoverableFailure(Throwable throwable) {
+ super(throwable);
+ }
+}
diff --git a/src/com/google/android/droiddriver/instrumentation/RootFinder.java b/src/com/google/android/droiddriver/instrumentation/RootFinder.java
index 4951bbe..2cdbd51 100644
--- a/src/com/google/android/droiddriver/instrumentation/RootFinder.java
+++ b/src/com/google/android/droiddriver/instrumentation/RootFinder.java
@@ -29,7 +29,6 @@ import java.lang.reflect.Method;
/**
* Class to find the root view.
- * Note(twickham): This class is no longer being used.
*/
public class RootFinder {
@@ -38,8 +37,9 @@ public class RootFinder {
private static final Object windowManagerObj;
static {
- String windowManagerClassName = Build.VERSION.SDK_INT >= 17 ? "android.view.WindowManagerGlobal"
- : "android.view.WindowManagerImpl";
+ String windowManagerClassName =
+ Build.VERSION.SDK_INT >= 17 ? "android.view.WindowManagerGlobal"
+ : "android.view.WindowManagerImpl";
String instanceMethod = Build.VERSION.SDK_INT >= 17 ? "getInstance" : "getDefault";
try {
Class<?> clazz = Class.forName(windowManagerClassName);
@@ -51,8 +51,8 @@ public class RootFinder {
throw new DroidDriverException(String.format("could not invoke: %s on %s", instanceMethod,
windowManagerClassName), ite.getCause());
} catch (ClassNotFoundException cnfe) {
- throw new DroidDriverException(
- String.format("could not find class: %s", windowManagerClassName), cnfe);
+ throw new DroidDriverException(String.format("could not find class: %s",
+ windowManagerClassName), cnfe);
} catch (NoSuchFieldException nsfe) {
throw new DroidDriverException(String.format("could not find field: %s on %s",
VIEW_FIELD_NAME, windowManagerClassName), nsfe);
@@ -60,13 +60,13 @@ public class RootFinder {
throw new DroidDriverException(String.format("could not find method: %s on %s",
instanceMethod, windowManagerClassName), nsme);
} catch (RuntimeException re) {
- throw new DroidDriverException(
- String.format("reflective setup failed using obj: %s method: %s field: %s",
- windowManagerClassName, instanceMethod, VIEW_FIELD_NAME), re);
+ throw new DroidDriverException(String.format(
+ "reflective setup failed using obj: %s method: %s field: %s", windowManagerClassName,
+ instanceMethod, VIEW_FIELD_NAME), re);
} catch (IllegalAccessException iae) {
- throw new DroidDriverException(
- String.format("reflective setup failed using obj: %s method: %s field: %s",
- windowManagerClassName, instanceMethod, VIEW_FIELD_NAME), iae);
+ throw new DroidDriverException(String.format(
+ "reflective setup failed using obj: %s method: %s field: %s", windowManagerClassName,
+ instanceMethod, VIEW_FIELD_NAME), iae);
}
}
@@ -78,12 +78,11 @@ public class RootFinder {
try {
views = (View[]) viewsField.get(windowManagerObj);
- Logs.log(Log.DEBUG, "View size:" +views.length);
+ Logs.log(Log.DEBUG, "View size:" + views.length);
return views;
} catch (RuntimeException re) {
throw new DroidDriverException(String.format("Reflective access to %s on %s failed.",
viewsField, windowManagerObj), re);
-
} catch (IllegalAccessException iae) {
throw new DroidDriverException(String.format("Reflective access to %s on %s failed.",
viewsField, windowManagerObj), iae);
diff --git a/src/com/google/android/droiddriver/runner/TestRunner.java b/src/com/google/android/droiddriver/runner/TestRunner.java
index 4474894..4f83e3e 100644
--- a/src/com/google/android/droiddriver/runner/TestRunner.java
+++ b/src/com/google/android/droiddriver/runner/TestRunner.java
@@ -25,6 +25,7 @@ import android.test.suitebuilder.TestMethod;
import android.util.Log;
import com.android.internal.util.Predicate;
+import com.google.android.droiddriver.helpers.DroidDrivers;
import com.google.android.droiddriver.util.ActivityUtils;
import com.google.android.droiddriver.util.Logs;
import com.google.common.base.Supplier;
@@ -121,7 +122,7 @@ public class TestRunner extends InstrumentationTestRunner {
}
UseUiAutomation useUiAutomation = getAnnotation(arg0, UseUiAutomation.class);
- if (useUiAutomation != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ if (useUiAutomation != null && !DroidDrivers.hasUiAutomation()) {
Logs.logfmt(Log.INFO,
"filtered %s#%s: Has @UseUiAutomation, but ro.build.version.sdk=%d",
arg0.getEnclosingClassname(), arg0.getName(), Build.VERSION.SDK_INT);