diff options
author | Kevin Jin <kjin@google.com> | 2013-10-23 14:08:12 -0700 |
---|---|---|
committer | Kevin Jin <kjin@google.com> | 2013-10-23 15:33:19 -0700 |
commit | 45828d52e6a2d9694eb507b5cafd3b6fcae9c33c (patch) | |
tree | a85ff402792094377f7f93863ec3d1b2b628b863 /src | |
parent | 07faa170442ca86bb21e6076ff3309615be3b9a8 (diff) | |
download | droiddriver-45828d52e6a2d9694eb507b5cafd3b6fcae9c33c.tar.gz |
add helpers: DroidDrivers, BaseDroidDriverTest, UnrecoverableFailure
add By.withChild and By.withDescendant
Change-Id: If739957750074fefa3450903d8b866c62c4390a5
Diffstat (limited to 'src')
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); |