summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrett Chabot <brettchabot@google.com>2014-06-11 13:18:38 -0700
committerBrett Chabot <brettchabot@google.com>2014-06-13 14:43:41 -0700
commitbac1a737514945ce33a2be2f9b8c5d63430b5234 (patch)
treea4130fae725eed95556e84274347a092895c0abd
parent1afabcc1a3db33f786ff7cc0c8d72e40990c8876 (diff)
downloadtesting-bac1a737514945ce33a2be2f9b8c5d63430b5234.tar.gz
Merge GoogleInstrumentationTestRunner features into android.support.test.
Change-Id: I07e47038b3e50234b5419499410a253d7ff89a99
-rw-r--r--.gitignore2
-rw-r--r--support/src/android/support/test/internal/runner/InstrumentationArgumentsRegistry.java61
-rw-r--r--support/src/android/support/test/internal/runner/InstrumentationRegistry.java54
-rw-r--r--support/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorImpl.java205
-rw-r--r--support/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorRegistry.java63
-rw-r--r--support/src/android/support/test/internal/runner/listener/ActivityFinisherRunListener.java64
-rw-r--r--support/src/android/support/test/internal/util/Checks.java84
-rw-r--r--support/src/android/support/test/runner/AndroidJUnitRunner.java58
-rw-r--r--support/src/android/support/test/runner/MonitoringInstrumentation.java418
-rw-r--r--support/src/android/support/test/runner/lifecycle/ActivityLifecycleCallback.java36
-rw-r--r--support/src/android/support/test/runner/lifecycle/ActivityLifecycleMonitor.java108
-rw-r--r--support/src/android/support/test/runner/lifecycle/Stage.java42
-rw-r--r--support/tests/.classpath2
-rw-r--r--support/tests/.project10
-rw-r--r--support/tests/Android.mk2
-rw-r--r--support/tests/AndroidManifest.xml1
-rw-r--r--support/tests/src/android/support/test/internal/runner/InstrumentationArgumentsRegistryTest.java60
-rw-r--r--support/tests/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorImplTest.java128
-rw-r--r--support/tests/src/android/support/test/runner/AndroidJUnitRunnerLifeCycleTest.java238
19 files changed, 1624 insertions, 12 deletions
diff --git a/.gitignore b/.gitignore
index aaf6b34..a341061 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,4 @@ bin/
*.cproject
*.settings
*.pyc
-
+.DS_Store
diff --git a/support/src/android/support/test/internal/runner/InstrumentationArgumentsRegistry.java b/support/src/android/support/test/internal/runner/InstrumentationArgumentsRegistry.java
new file mode 100644
index 0000000..b8bf8fb
--- /dev/null
+++ b/support/src/android/support/test/internal/runner/InstrumentationArgumentsRegistry.java
@@ -0,0 +1,61 @@
+/*
+ * 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 android.support.test.internal.runner;
+
+import android.os.Bundle;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * An exposed registry instance to make it easy for callers to find the instrumentation arguments.
+ */
+public final class InstrumentationArgumentsRegistry {
+
+ private static final AtomicReference<Bundle> sArguments = new AtomicReference<Bundle>(null);
+
+ /**
+ * Returns a copy of instrumentation arguments Bundle.
+ * <p>
+ * This Bundle is not guaranteed to be present under all instrumentations.
+ * </p>
+ *
+ * @return Bundle the arguments for this instrumentation.
+ * @throws IllegalStateException if no argument Bundle has been registered.
+ */
+ public static Bundle getInstance() {
+ Bundle instance = sArguments.get();
+ if (null == instance) {
+ throw new IllegalStateException("No instrumentation arguments registered! "
+ + "Are you running under an Instrumentation which registers arguments?");
+ }
+ return new Bundle(instance);
+ }
+
+ /**
+ * Stores a copy of the instrumentation arguments Bundle in the registry.
+ * <p>
+ * This is a global registry - so be aware of the impact of calling this method!
+ * </p>
+ *
+ * @param arguments the arguments for this application. Null deregisters any existing arguments.
+ */
+ public static void registerInstance(Bundle arguments) {
+ InstrumentationArgumentsRegistry.sArguments.set(new Bundle(arguments));
+ }
+
+ private InstrumentationArgumentsRegistry() { }
+}
diff --git a/support/src/android/support/test/internal/runner/InstrumentationRegistry.java b/support/src/android/support/test/internal/runner/InstrumentationRegistry.java
new file mode 100644
index 0000000..e41338e
--- /dev/null
+++ b/support/src/android/support/test/internal/runner/InstrumentationRegistry.java
@@ -0,0 +1,54 @@
+/*
+ * 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 android.support.test.internal.runner;
+
+import static android.support.test.internal.util.Checks.checkNotNull;
+
+import android.app.Instrumentation;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Holds a reference to the instrumentation running in the process.
+ */
+public final class InstrumentationRegistry {
+
+ private static final AtomicReference<Instrumentation> sInstrumentationRef =
+ new AtomicReference<Instrumentation>(null);
+
+ /**
+ * Returns the instrumentation currently running.
+ *
+ * @throws IllegalStateException if instrumentation hasn't been registered
+ */
+ public static Instrumentation getInstance() {
+ return checkNotNull(sInstrumentationRef.get(), "No instrumentation registered. " +
+ "Must run under a registering instrumentation.");
+ }
+
+ /**
+ * Records/exposes the instrumentation currently running.
+ * <p>
+ * This is a global registry - so be aware of the impact of calling this method!
+ * </p>
+ */
+ public static void registerInstance(Instrumentation instrumentation) {
+ sInstrumentationRef.set(instrumentation);
+ }
+
+ private InstrumentationRegistry() { }
+}
diff --git a/support/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorImpl.java b/support/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorImpl.java
new file mode 100644
index 0000000..7a49d7f
--- /dev/null
+++ b/support/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorImpl.java
@@ -0,0 +1,205 @@
+/*
+ * 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 android.support.test.internal.runner.lifecycle;
+
+import static android.support.test.internal.util.Checks.checkNotNull;
+
+import android.app.Activity;
+import android.os.Looper;
+import android.support.test.runner.AndroidJUnitRunner;
+import android.support.test.runner.lifecycle.ActivityLifecycleCallback;
+import android.support.test.runner.lifecycle.ActivityLifecycleMonitor;
+import android.support.test.runner.lifecycle.Stage;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * The lifecycle monitor used by {@link AndroidJUnitRunner}.
+ */
+public final class ActivityLifecycleMonitorImpl implements ActivityLifecycleMonitor {
+ private static final String TAG = "LifecycleMonitor";
+ private final boolean mDeclawThreadCheck;
+
+ public ActivityLifecycleMonitorImpl() {
+ this(false);
+ }
+
+ // For Testing
+ public ActivityLifecycleMonitorImpl(boolean declawThreadCheck) {
+ this.mDeclawThreadCheck = declawThreadCheck;
+ }
+
+ // Accessed from any thread.
+ private List<WeakReference<ActivityLifecycleCallback>> mCallbacks =
+ new ArrayList<WeakReference<ActivityLifecycleCallback>>();
+
+ // Only accessed on main thread.
+ private List<ActivityStatus> mActivityStatuses = new ArrayList<ActivityStatus>();
+
+ @Override
+ public void addLifecycleCallback(ActivityLifecycleCallback callback) {
+ // there will never be too many callbacks, so iterating over a list will probably
+ // be faster then the constant time costs of setting up and maintaining a map.
+ checkNotNull(callback);
+
+ synchronized (mCallbacks) {
+ boolean needsAdd = true;
+ Iterator<WeakReference<ActivityLifecycleCallback>> refIter = mCallbacks.iterator();
+ while (refIter.hasNext()) {
+ ActivityLifecycleCallback storedCallback = refIter.next().get();
+ if (null == storedCallback) {
+ refIter.remove();
+ } else if (storedCallback == callback) {
+ needsAdd = false;
+ }
+ }
+ if (needsAdd) {
+ mCallbacks.add(new WeakReference<ActivityLifecycleCallback>(callback));
+ }
+ }
+ }
+
+ @Override
+ public void removeLifecycleCallback(ActivityLifecycleCallback callback) {
+ checkNotNull(callback);
+
+ synchronized (mCallbacks) {
+ Iterator<WeakReference<ActivityLifecycleCallback>> refIter = mCallbacks.iterator();
+ while (refIter.hasNext()) {
+ ActivityLifecycleCallback storedCallback = refIter.next().get();
+ if (null == storedCallback) {
+ refIter.remove();
+ } else if (storedCallback == callback) {
+ refIter.remove();
+ }
+ }
+ }
+ }
+
+ @Override
+ public Stage getLifecycleStageOf(Activity activity) {
+ checkMainThread();
+ checkNotNull(activity);
+ Iterator<ActivityStatus> statusIterator = mActivityStatuses.iterator();
+ while (statusIterator.hasNext()) {
+ ActivityStatus status = statusIterator.next();
+ Activity statusActivity = status.mActivityRef.get();
+ if (null == statusActivity) {
+ statusIterator.remove();
+ } else if (activity == statusActivity) {
+ return status.mLifecycleStage;
+ }
+ }
+ throw new IllegalArgumentException("Unknown activity: " + activity);
+ }
+
+ @Override
+ public Collection<Activity> getActivitiesInStage(Stage stage) {
+ checkMainThread();
+ checkNotNull(stage);
+
+ List<Activity> activities = new ArrayList<Activity>();
+ Iterator<ActivityStatus> statusIterator = mActivityStatuses.iterator();
+ while (statusIterator.hasNext()) {
+ ActivityStatus status = statusIterator.next();
+ Activity statusActivity = status.mActivityRef.get();
+ if (null == statusActivity) {
+ statusIterator.remove();
+ } else if (stage == status.mLifecycleStage) {
+ activities.add(statusActivity);
+ }
+ }
+
+ return activities;
+ }
+
+ /**
+ * Called by the runner after a particular onXXX lifecycle method has been called on a given
+ * activity.
+ */
+ public void signalLifecycleChange(Stage stage, Activity activity) {
+ // there are never too many activities in existence in an application - so we keep
+ // track of everything in a single list.
+ Log.d(TAG, "Lifecycle status change: " + activity + " in: " + stage);
+
+ boolean needsAdd = true;
+ Iterator<ActivityStatus> statusIterator = mActivityStatuses.iterator();
+ while (statusIterator.hasNext()) {
+ ActivityStatus status = statusIterator.next();
+ Activity statusActivity = status.mActivityRef.get();
+ if (null == statusActivity) {
+ statusIterator.remove();
+ } else if (activity == statusActivity) {
+ needsAdd = false;
+ status.mLifecycleStage = stage;
+ }
+ }
+
+ if (needsAdd) {
+ mActivityStatuses.add(new ActivityStatus(activity, stage));
+ }
+
+ synchronized (mCallbacks) {
+ Iterator<WeakReference<ActivityLifecycleCallback>> refIter = mCallbacks.iterator();
+ while (refIter.hasNext()) {
+ ActivityLifecycleCallback callback = refIter.next().get();
+ if (null == callback) {
+ refIter.remove();
+ } else {
+ try {
+ Log.d(TAG, "running callback: " + callback);
+ callback.onActivityLifecycleChanged(activity, stage);
+ Log.d(TAG, "callback completes: " + callback);
+ } catch (RuntimeException re) {
+ Log.e(TAG, String.format(
+ "Callback threw exception! (callback: %s activity: %s stage: %s)",
+ callback,
+ activity,
+ stage),
+ re);
+ }
+ }
+ }
+ }
+ }
+
+ private void checkMainThread() {
+ if (mDeclawThreadCheck) {
+ return;
+ }
+
+ if (!Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
+ throw new IllegalStateException(
+ "Querying activity state off main thread is not allowed.");
+ }
+ }
+
+ private static class ActivityStatus {
+ private final WeakReference<Activity> mActivityRef;
+ private Stage mLifecycleStage;
+
+ ActivityStatus(Activity activity, Stage stage) {
+ this.mActivityRef = new WeakReference<Activity>(checkNotNull(activity));
+ this.mLifecycleStage = checkNotNull(stage);
+ }
+ }
+}
diff --git a/support/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorRegistry.java b/support/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorRegistry.java
new file mode 100644
index 0000000..b618354
--- /dev/null
+++ b/support/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorRegistry.java
@@ -0,0 +1,63 @@
+/*
+ * 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 android.support.test.internal.runner.lifecycle;
+
+import android.support.test.runner.lifecycle.ActivityLifecycleMonitor;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * An exposed registry instance to make it easy for callers to find the lifecycle monitor for their
+ * application.
+ */
+public final class ActivityLifecycleMonitorRegistry {
+
+ private static final AtomicReference<ActivityLifecycleMonitor> sLifecycleMonitor =
+ new AtomicReference<ActivityLifecycleMonitor>(null);
+
+ // singleton - disallow creation
+ private ActivityLifecycleMonitorRegistry() { }
+
+ /**
+ * Returns the ActivityLifecycleMonitor.
+ *
+ * This monitor is not guaranteed to be present under all instrumentations.
+ *
+ * @return ActivityLifecycleMonitor the monitor for this application.
+ * @throws IllegalStateException if no monitor has been registered.
+ */
+ public static ActivityLifecycleMonitor getInstance() {
+ ActivityLifecycleMonitor instance = sLifecycleMonitor.get();
+ if (null == instance) {
+ throw new IllegalStateException("No lifecycle monitor registered! Are you running "
+ + "under an Instrumentation which registers lifecycle monitors?");
+ }
+ return instance;
+ }
+
+ /**
+ * Stores a lifecycle monitor in the registry.
+ * <p>
+ * This is a global registry - so be aware of the impact of calling this method!
+ * </p>
+ *
+ * @param monitor the monitor for this application. Null deregisters any existing monitor.
+ */
+ public static void registerInstance(ActivityLifecycleMonitor monitor) {
+ sLifecycleMonitor.set(monitor);
+ }
+}
diff --git a/support/src/android/support/test/internal/runner/listener/ActivityFinisherRunListener.java b/support/src/android/support/test/internal/runner/listener/ActivityFinisherRunListener.java
new file mode 100644
index 0000000..ae5cc07
--- /dev/null
+++ b/support/src/android/support/test/internal/runner/listener/ActivityFinisherRunListener.java
@@ -0,0 +1,64 @@
+package android.support.test.internal.runner.listener;
+
+import static android.support.test.internal.util.Checks.checkNotNull;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.support.test.runner.lifecycle.ActivityLifecycleMonitor;
+import android.support.test.runner.lifecycle.Stage;
+import android.util.Log;
+
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunListener;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * Ensures that no activities are running when a test method starts and that no activities are still
+ * running when it ends.
+ */
+public class ActivityFinisherRunListener extends RunListener {
+ private static final String TAG = "ActivityFinisher";
+ private final Instrumentation instrumentation;
+ private final ActivityLifecycleMonitor activityLifecycleMonitor;
+ public ActivityFinisherRunListener(Instrumentation instrumentation, ActivityLifecycleMonitor activityLifecycleMonitor) {
+ this.instrumentation = checkNotNull(instrumentation);
+ this.activityLifecycleMonitor = checkNotNull(activityLifecycleMonitor);
+ }
+
+ @Override
+ public void testStarted(Description description) throws Exception {
+ instrumentation.runOnMainSync(makeActivityFinisher());
+ }
+
+ @Override
+ public void testFinished(Description description) throws Exception {
+ instrumentation.runOnMainSync(makeActivityFinisher());
+ }
+
+ private Runnable makeActivityFinisher() {
+ return new Runnable() {
+ @Override
+ public void run() {
+
+ List<Activity> activities = new ArrayList<Activity>();
+ for (Stage s : EnumSet.range(Stage.CREATED, Stage.PAUSED)) {
+ activities.addAll(activityLifecycleMonitor.getActivitiesInStage(s));
+ }
+ for (Activity activity : activities) {
+ if (!activity.isFinishing()) {
+ Log.i(TAG, "Finishing: " + activity);
+ try {
+ activity.finish();
+ } catch (RuntimeException re) {
+ Log.e(TAG, "Failed to finish: " + activity, re);
+ }
+ }
+ }
+ }
+
+ };
+ }
+} \ No newline at end of file
diff --git a/support/src/android/support/test/internal/util/Checks.java b/support/src/android/support/test/internal/util/Checks.java
new file mode 100644
index 0000000..82fd2f1
--- /dev/null
+++ b/support/src/android/support/test/internal/util/Checks.java
@@ -0,0 +1,84 @@
+/*
+ * 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 android.support.test.internal.util;
+
+/**
+ * Utility method for checking for null references
+ */
+public final class Checks {
+
+ private Checks() {
+ }
+
+ public static <T> T checkNotNull(T reference) {
+ if (reference == null) {
+ throw new NullPointerException();
+ }
+ return reference;
+ }
+
+ public static <T> T checkNotNull(T reference, Object errorMessage) {
+ if (reference == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ return reference;
+ }
+
+ public static <T> T checkNotNull(T reference,
+ String errorMessageTemplate,
+ Object... errorMessageArgs) {
+ if (reference == null) {
+ // If either of these parameters is null, the right thing happens
+ // anyway
+ throw new NullPointerException(
+ format(errorMessageTemplate, errorMessageArgs));
+ }
+ return reference;
+ }
+
+ private static String format(String template, Object... args) {
+ template = String.valueOf(template); // null -> "null"
+
+ // start substituting the arguments into the '%s' placeholders
+ StringBuilder builder = new StringBuilder(
+ template.length() + 16 * args.length);
+ int templateStart = 0;
+ int i = 0;
+ while (i < args.length) {
+ int placeholderStart = template.indexOf("%s", templateStart);
+ if (placeholderStart == -1) {
+ break;
+ }
+ builder.append(template.substring(templateStart, placeholderStart));
+ builder.append(args[i++]);
+ templateStart = placeholderStart + 2;
+ }
+ builder.append(template.substring(templateStart));
+
+ // if we run out of placeholders, append the extra args in square braces
+ if (i < args.length) {
+ builder.append(" [");
+ builder.append(args[i++]);
+ while (i < args.length) {
+ builder.append(", ");
+ builder.append(args[i++]);
+ }
+ builder.append(']');
+ }
+ return builder.toString();
+ }
+}
diff --git a/support/src/android/support/test/runner/AndroidJUnitRunner.java b/support/src/android/support/test/runner/AndroidJUnitRunner.java
index 6abf44a..42cdc73 100644
--- a/support/src/android/support/test/runner/AndroidJUnitRunner.java
+++ b/support/src/android/support/test/runner/AndroidJUnitRunner.java
@@ -17,12 +17,18 @@
package android.support.test.runner;
import android.app.Activity;
+import android.app.Application;
import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
import android.content.pm.InstrumentationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Debug;
+import android.os.IBinder;
import android.os.Looper;
import android.support.test.internal.runner.TestRequest;
import android.support.test.internal.runner.TestRequestBuilder;
@@ -145,7 +151,7 @@ import java.util.List;
* meta-data android:name="listener"
* android:value="com.foo.Listener,com.foo.Listener2"
*/
-public class AndroidJUnitRunner extends Instrumentation {
+public class AndroidJUnitRunner extends MonitoringInstrumentation {
// constants for supported instrumentation arguments
public static final String ARGUMENT_TEST_CLASS = "class";
@@ -172,6 +178,7 @@ public class AndroidJUnitRunner extends Instrumentation {
public void onCreate(Bundle arguments) {
super.onCreate(arguments);
mArguments = arguments;
+ specifyDexMakerCacheProperty();
start();
}
@@ -211,8 +218,7 @@ public class AndroidJUnitRunner extends Instrumentation {
@Override
public void onStart() {
- // Wait for target context to finish.
- waitForIdleSync();
+ super.onStart();
prepareLooper();
@@ -220,7 +226,7 @@ public class AndroidJUnitRunner extends Instrumentation {
Debug.waitForDebugger();
}
- setupDexmaker();
+ setupDexmakerClassloader();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PrintStream writer = new PrintStream(byteArrayOutputStream);
@@ -483,12 +489,7 @@ public class AndroidJUnitRunner extends Instrumentation {
}
}
- private void setupDexmaker() {
- // Explicitly set the Dexmaker cache, so tests that use mocking frameworks work
- String dexCache = getTargetContext().getCacheDir().getPath();
- Log.i(LOG_TAG, "Setting dexmaker.dexcache to " + dexCache);
- System.setProperty("dexmaker.dexcache", getTargetContext().getCacheDir().getPath());
-
+ private void setupDexmakerClassloader() {
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
// must set the context classloader for apps that use a shared uid, see
// frameworks/base/core/java/android/app/LoadedApk.java
@@ -497,4 +498,41 @@ public class AndroidJUnitRunner extends Instrumentation {
newClassLoader.toString(), originalClassLoader.toString()));
Thread.currentThread().setContextClassLoader(newClassLoader);
}
+
+ // ActivityUnitTestCase defaults to building the ComponentName via
+ // Activity.getClass().getPackage().getName(). This will cause a problem if the Java Package of
+ // the Activity is not the Android Package of the application, specifically
+ // Activity.getPackageName() will return an incorrect value.
+ // @see b/14561718
+ @Override
+ public Activity newActivity(Class<?> clazz,
+ Context context,
+ IBinder token,
+ Application application,
+ Intent intent,
+ ActivityInfo info,
+ CharSequence title,
+ Activity parent,
+ String id,
+ Object lastNonConfigurationInstance) throws InstantiationException, IllegalAccessException {
+ String activityClassPackageName = clazz.getPackage().getName();
+ String contextPackageName = context.getPackageName();
+ ComponentName intentComponentName = intent.getComponent();
+ if (!contextPackageName.equals(intentComponentName.getPackageName())) {
+ if (activityClassPackageName.equals(intentComponentName.getPackageName())) {
+ intent.setComponent(
+ new ComponentName(contextPackageName, intentComponentName.getClassName()));
+ }
+ }
+ return super.newActivity(clazz,
+ context,
+ token,
+ application,
+ intent,
+ info,
+ title,
+ parent,
+ id,
+ lastNonConfigurationInstance);
+ }
}
diff --git a/support/src/android/support/test/runner/MonitoringInstrumentation.java b/support/src/android/support/test/runner/MonitoringInstrumentation.java
new file mode 100644
index 0000000..f6ed97a
--- /dev/null
+++ b/support/src/android/support/test/runner/MonitoringInstrumentation.java
@@ -0,0 +1,418 @@
+/*
+ * 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 android.support.test.runner;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.MessageQueue.IdleHandler;
+import android.support.test.internal.runner.InstrumentationArgumentsRegistry;
+import android.support.test.internal.runner.InstrumentationRegistry;
+import android.support.test.internal.runner.lifecycle.ActivityLifecycleMonitorImpl;
+import android.support.test.internal.runner.lifecycle.ActivityLifecycleMonitorRegistry;
+import android.support.test.runner.lifecycle.Stage;
+import android.util.Log;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * An instrumentation that enables several advanced features and makes some hard guarantees about
+ * the state of the application under instrumentation.
+ * <p/>
+ * A short list of these capabilities:
+ * <ul>
+ * <li>Forces Application.onCreate() to happen before Instrumentation.onStart() runs (ensuring your
+ * code always runs in a sane state).</li>
+ * <li>Logs application death due to exceptions.</li>
+ * <li>Allows tracking of activity lifecycle states.</li>
+ * <li>Registers instrumentation arguments in an easy to access place.</li>
+ * <li>Ensures your activities are creating themselves in reasonable amounts of time.</li>
+ * <li>Provides facilities to dump current app threads to test outputs.</li>
+ * <li>Ensures all activities finish before instrumentation exits.</li>
+ * </ul>
+ *
+ * This Instrumentation is *NOT* a test instrumentation (some of its subclasses are). It makes no
+ * assumptions about what the subclass wants to do.
+ */
+public class MonitoringInstrumentation extends Instrumentation {
+
+ private static final long MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP = TimeUnit.SECONDS.toMillis(2);
+ private static final long MILLIS_TO_POLL_FOR_ACTIVITY_STOP =
+ MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP / 40;
+
+ private static final String LOG_TAG = "MonitoringInstrumentation";
+
+ private static final int START_ACTIVITY_TIMEOUT_SECONDS = 45;
+ private ActivityLifecycleMonitorImpl mLifecycleMonitor = new ActivityLifecycleMonitorImpl();
+ private ExecutorService mExecutorService;
+ private Handler mHandlerForMainLooper;
+ private AtomicBoolean mAnActivityHasBeenLaunched = new AtomicBoolean(false);
+ private Thread mMainThread;
+ private AtomicLong mLastIdleTime = new AtomicLong(0);
+ private AtomicInteger mStartedActivityCounter = new AtomicInteger(0);
+
+ private IdleHandler mIdleHandler = new IdleHandler() {
+ @Override
+ public boolean queueIdle() {
+ mLastIdleTime.set(System.currentTimeMillis());
+ return true;
+ }
+ };
+
+ private volatile boolean mFinished = false;
+
+ /**
+ * Sets up lifecycle monitoring, and argument registry.
+ * <p>
+ * Subclasses must call up to onCreate(). This onCreate method does not call start()
+ * it is the subclasses responsibility to call start if it desires.
+ * </p>
+ */
+ @Override
+ public void onCreate(Bundle arguments) {
+ Log.i(LOG_TAG, "Instrumentation Started!");
+ logUncaughtExceptions();
+
+ InstrumentationRegistry.registerInstance(this);
+ ActivityLifecycleMonitorRegistry.registerInstance(mLifecycleMonitor);
+
+ InstrumentationArgumentsRegistry.registerInstance(arguments);
+
+ mHandlerForMainLooper = new Handler(Looper.getMainLooper());
+ mMainThread = Thread.currentThread();
+ mExecutorService = Executors.newCachedThreadPool();
+ Looper.myQueue().addIdleHandler(mIdleHandler);
+ super.onCreate(arguments);
+ }
+
+ protected final void specifyDexMakerCacheProperty() {
+ // DexMaker uses heuristics to figure out where to store its temporary dex files
+ // these heuristics may break (eg - they no longer work on JB MR2). So we create
+ // our own cache dir to be used if the app doesnt specify a cache dir, rather then
+ // relying on heuristics.
+ //
+ File dexCache = getTargetContext().getDir("dxmaker_cache", Context.MODE_PRIVATE);
+ System.getProperties().put("dexmaker.dexcache", dexCache.getAbsolutePath());
+ }
+
+ private void logUncaughtExceptions() {
+ final Thread.UncaughtExceptionHandler standardHandler =
+ Thread.currentThread().getUncaughtExceptionHandler();
+ Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread t, Throwable e) {
+ onException(t, e);
+ if (null != standardHandler) {
+ standardHandler.uncaughtException(t, e);
+ }
+ }
+ });
+ }
+
+ /**
+ * This implementation of onStart() will guarantee that the Application's onCreate method
+ * has completed when it returns.
+ * <p>
+ * Subclasses should call super.onStart() before executing any code that touches the application
+ * and it's state.
+ * </p>
+ */
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // Due to the way Android initializes instrumentation - all instrumentations have the
+ // possibility of seeing the Application and its classes in an inconsistent state.
+ // Specifically ActivityThread creates Instrumentation first, initializes it, and calls
+ // instrumentation.onCreate(). After it does that, it calls
+ // instrumentation.callApplicationOnCreate() which ends up calling the application's
+ // onCreateMethod.
+ //
+ // So, Android's InstrumentationTestRunner's onCreate method() spawns a separate thread to
+ // execute tests. This causes tests to start accessing the application and its classes while
+ // the ActivityThread is calling callApplicationOnCreate() in its own thread.
+ //
+ // This makes it possible for tests to see the application in a state that is normally never
+ // visible: pre-application.onCreate() and during application.onCreate()).
+ //
+ // *phew* that sucks! Here we waitForOnIdleSync() to ensure onCreate has completed before we
+ // start executing tests.
+ waitForIdleSync();
+ }
+
+ /**
+ * Ensures all activities launched in this instrumentation are finished before the
+ * instrumentation exits.
+ * <p>
+ * Subclasses who override this method should do their finish processing and then call
+ * super.finish to invoke this logic. Not waiting for all activities to finish() before exiting
+ * can cause device wide instability.
+ * </p>
+ */
+ @Override
+ public void finish(int resultCode, Bundle results) {
+ if (mFinished) {
+ Log.w(LOG_TAG, "finish called 2x!");
+ return;
+ } else {
+ mFinished = true;
+ }
+
+ mHandlerForMainLooper.post(new ActivityFinisher());
+
+ long startTime = System.currentTimeMillis();
+ waitForActivitiesToComplete();
+ long endTime = System.currentTimeMillis();
+ Log.i(LOG_TAG, String.format("waitForActivitiesToComplete() took: %sms", endTime - startTime));
+ ActivityLifecycleMonitorRegistry.registerInstance(null);
+ super.finish(resultCode, results);
+ }
+
+ /**
+ * Ensures we've onStopped() all activities which were onStarted().
+ * <p>
+ * According to Activity's contract, the process is not killable between onStart and onStop.
+ * Breaking this contract (which finish() will if you let it) can cause bad behaviour (including
+ * a full restart of system_server).
+ * </p>
+ * <p>
+ * We give the app 2 seconds to stop all its activities, then we proceed.
+ * </p>
+ */
+ protected void waitForActivitiesToComplete() {
+ long endTime = System.currentTimeMillis() + MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP;
+ int currentActivityCount = mStartedActivityCounter.get();
+
+ while (currentActivityCount > 0 && System.currentTimeMillis() < endTime) {
+ try {
+ Log.i(LOG_TAG, "Unstopped activity count: " + currentActivityCount);
+ Thread.sleep(MILLIS_TO_POLL_FOR_ACTIVITY_STOP);
+ currentActivityCount = mStartedActivityCounter.get();
+ } catch (InterruptedException ie) {
+ Log.i(LOG_TAG, "Abandoning activity wait due to interruption.", ie);
+ break;
+ }
+ }
+
+ if (currentActivityCount > 0) {
+ dumpThreadStateToOutputs("ThreadState-unstopped.txt");
+ Log.w(LOG_TAG, String.format("Still %s activities active after waiting %s ms.",
+ currentActivityCount, MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP));
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.i(LOG_TAG, "Instrumentation Finished!");
+ Looper.myQueue().removeIdleHandler(mIdleHandler);
+ super.onDestroy();
+ }
+
+ @Override
+ public Activity startActivitySync(final Intent intent) {
+ validateNotAppThread();
+ long lastIdleTimeBeforeLaunch = mLastIdleTime.get();
+
+ if (mAnActivityHasBeenLaunched.compareAndSet(false, true)) {
+ // All activities launched from InstrumentationTestCase.launchActivityWithIntent get
+ // started with FLAG_ACTIVITY_NEW_TASK. This includes calls to
+ // ActivityInstrumentationTestcase2.getActivity().
+ //
+ // This gives us a pristine environment - MOST OF THE TIME.
+ //
+ // However IF we've run a test method previously and that has launched an activity
+ // outside of our process our old task is still lingering around. By launching a new
+ // activity android will place our activity at the bottom of the stack and bring the
+ // previous external activity to the front of the screen.
+ //
+ // To wipe out the old task and execute within a pristine environment for each test
+ // we tell android to CLEAR_TOP the very first activity we see, no matter what.
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ }
+ Future<Activity> startedActivity = mExecutorService.submit(new Callable<Activity>() {
+ @Override
+ public Activity call() {
+ return MonitoringInstrumentation.super.startActivitySync(intent);
+ }
+ });
+
+ try {
+ return startedActivity.get(START_ACTIVITY_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (TimeoutException te) {
+ startedActivity.cancel(true);
+ dumpThreadStateToOutputs("ThreadState-startActivityTimeout.txt");
+ throw new RuntimeException(String.format("Could not launch intent %s within %s seconds."
+ + " Perhaps the main thread has not gone idle within a reasonable amount of "
+ + "time? There could be an animation or something constantly repainting the "
+ + "screen. Or the activity is doing network calls on creation? See the "
+ + "threaddump logs. For your reference the last time the event queue was idle "
+ + "before your activity launch request was %s and now the last time the queue "
+ + "went idle was: %s. If these numbers are the same your activity might be "
+ +"hogging the event queue.",
+ intent, START_ACTIVITY_TIMEOUT_SECONDS, lastIdleTimeBeforeLaunch,
+ mLastIdleTime.get()));
+ } catch (ExecutionException ee) {
+ throw new RuntimeException("Could not launch activity", ee.getCause());
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException("interrupted", ie);
+ }
+ }
+
+ private void validateNotAppThread() {
+ if (mMainThread.equals(Thread.currentThread())) {
+ throw new RuntimeException(
+ "this method cannot be called from the main application thread");
+ }
+ }
+
+ @Override
+ public boolean onException(Object obj, Throwable e) {
+ String error = String.format("Exception encountered by: %s. Dumping thread state to "
+ + "outputs and pining for the fjords.", obj);
+ Log.e(LOG_TAG, error, e);
+ dumpThreadStateToOutputs("ThreadState-onException.txt");
+ Log.e(LOG_TAG, "Dying now...");
+ return super.onException(obj, e);
+ }
+
+ protected final void dumpThreadStateToOutputs(String outputFileName) {
+ String threadState = getThreadState();
+ Log.e("THREAD_STATE", threadState);
+ }
+
+ private static String getThreadState() {
+ Set<Map.Entry<Thread, StackTraceElement[]>> threads = Thread.getAllStackTraces().entrySet();
+ StringBuilder threadState = new StringBuilder();
+ for (Map.Entry<Thread, StackTraceElement[]> threadAndStack : threads) {
+ StringBuilder threadMessage = new StringBuilder(" ").append(threadAndStack.getKey());
+ threadMessage.append("\n");
+ for (StackTraceElement ste : threadAndStack.getValue()) {
+ threadMessage.append(" ");
+ threadMessage.append(ste.toString());
+ threadMessage.append("\n");
+ }
+ threadMessage.append("\n");
+ threadState.append(threadMessage.toString());
+ }
+ return threadState.toString();
+ }
+
+ @Override
+ public void callActivityOnDestroy(Activity activity) {
+ super.callActivityOnDestroy(activity);
+ mLifecycleMonitor.signalLifecycleChange(Stage.DESTROYED, activity);
+ }
+
+ @Override
+ public void callActivityOnRestart(Activity activity) {
+ super.callActivityOnRestart(activity);
+ mLifecycleMonitor.signalLifecycleChange(Stage.RESTARTED, activity);
+ }
+
+ @Override
+ public void callActivityOnCreate(Activity activity, Bundle bundle) {
+ mLifecycleMonitor.signalLifecycleChange(Stage.PRE_ON_CREATE, activity);
+ super.callActivityOnCreate(activity, bundle);
+ mLifecycleMonitor.signalLifecycleChange(Stage.CREATED, activity);
+ }
+
+ // NOTE: we need to keep a count of activities between the start
+ // and stop lifecycle internal to our instrumentation. Exiting the test
+ // process with activities in this state can cause crashes/flakiness
+ // that would impact a subsequent test run.
+ @Override
+ public void callActivityOnStart(Activity activity) {
+ mStartedActivityCounter.incrementAndGet();
+ try {
+ super.callActivityOnStart(activity);
+ mLifecycleMonitor.signalLifecycleChange(Stage.STARTED, activity);
+ } catch (RuntimeException re) {
+ mStartedActivityCounter.decrementAndGet();
+ throw re;
+ }
+ }
+
+ @Override
+ public void callActivityOnStop(Activity activity) {
+ try {
+ super.callActivityOnStop(activity);
+ mLifecycleMonitor.signalLifecycleChange(Stage.STOPPED, activity);
+ } finally {
+ mStartedActivityCounter.decrementAndGet();
+ }
+ }
+
+ @Override
+ public void callActivityOnResume(Activity activity) {
+ super.callActivityOnResume(activity);
+ mLifecycleMonitor.signalLifecycleChange(Stage.RESUMED, activity);
+ }
+
+ @Override
+ public void callActivityOnPause(Activity activity) {
+ super.callActivityOnPause(activity);
+ mLifecycleMonitor.signalLifecycleChange(Stage.PAUSED, activity);
+ }
+
+ /**
+ * Loops through all the activities that have not yet finished and explicitly calls finish
+ * on them.
+ */
+ public class ActivityFinisher implements Runnable {
+ @Override
+ public void run() {
+ List<Activity> activities = new ArrayList<Activity>();
+
+ for (Stage s : EnumSet.range(Stage.CREATED, Stage.PAUSED)) {
+ activities.addAll(mLifecycleMonitor.getActivitiesInStage(s));
+ }
+
+ Log.i(LOG_TAG, "Activities that are still in CREATED to PAUSED: " + activities.size());
+
+ for (Activity activity : activities) {
+ if (!activity.isFinishing()) {
+ try {
+ Log.i(LOG_TAG, "Stopping activity: " + activity);
+ activity.finish();
+ } catch (RuntimeException e) {
+ Log.e(LOG_TAG, "Failed to stop activity.", e);
+ }
+ }
+ }
+ }
+ };
+}
diff --git a/support/src/android/support/test/runner/lifecycle/ActivityLifecycleCallback.java b/support/src/android/support/test/runner/lifecycle/ActivityLifecycleCallback.java
new file mode 100644
index 0000000..0ffe5c9
--- /dev/null
+++ b/support/src/android/support/test/runner/lifecycle/ActivityLifecycleCallback.java
@@ -0,0 +1,36 @@
+/*
+ * 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 android.support.test.runner.lifecycle;
+
+import android.app.Activity;
+
+/**
+ * Callback for monitoring activity lifecycle events. These callbacks are invoked on the main
+ * thread, so any long operations or violating the strict mode policies should be avoided.
+ */
+public interface ActivityLifecycleCallback {
+
+ /**
+ * Called on the main thread after an activity has processed its lifecycle change event
+ * (for example onResume or onStart)
+ *
+ * @param activity The activity
+ * @param stage its current stage.
+ */
+ public void onActivityLifecycleChanged(Activity activity, Stage stage);
+}
+
diff --git a/support/src/android/support/test/runner/lifecycle/ActivityLifecycleMonitor.java b/support/src/android/support/test/runner/lifecycle/ActivityLifecycleMonitor.java
new file mode 100644
index 0000000..f44bd59
--- /dev/null
+++ b/support/src/android/support/test/runner/lifecycle/ActivityLifecycleMonitor.java
@@ -0,0 +1,108 @@
+/*
+ * 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 android.support.test.runner.lifecycle;
+
+import android.app.Activity;
+
+import java.util.Collection;
+
+/**
+ * Interface for tests to use when they need to query the activity lifecycle state.
+ * <p>
+ * Activity lifecycle changes occur only on the UI thread - therefore listeners registered with
+ * an ActivityLifecycleMonitor should expect to be invoked on the UI thread. The direct query
+ * methods can only be called on the UI thread because otherwise they would not be able to return
+ * consistent responses.
+ * </p>
+ * <p>
+ * Retrieve instances of the monitor thru ActivityLifecycleMonitorRegistry.
+ * </p>
+ * <p>
+ * Detecting these lifecycle states requires support from Instrumentation, therefore do not expect
+ * an instance to be present under any arbitrary instrumentation.
+ * </p>
+ */
+public interface ActivityLifecycleMonitor {
+
+ /**
+ * Adds a new callback that will be notified when lifecycle changes occur.
+ * <p>
+ * Implementors will not hold a strong ref to the callback, the code which registers callbacks
+ * is responsible for this. Code which registers callbacks should responsibly
+ * remove their callback when it is no longer needed.
+ * </p>
+ * <p>
+ * Callbacks are executed on the main thread of the application, and should take care not to
+ * block or otherwise perform expensive operations as it will directly impact the application.
+ * </p>
+ *
+ * @param callback an ActivityLifecycleCallback
+ */
+ void addLifecycleCallback(ActivityLifecycleCallback callback);
+
+ /**
+ * Removes a previously registered lifecycle callback.
+ */
+ void removeLifecycleCallback(ActivityLifecycleCallback callback);
+
+ /**
+ * Returns the current lifecycle stage of a given activity.
+ * <p>
+ * This method can only return a consistant and correct answer
+ * from the main thread, therefore callers should always invoke
+ * it from the main thread and implementors are free to throw an
+ * exception if the call is not made on the main thread.
+ * </p>
+ * <p>
+ * Implementors should ensure this method returns a consistant response if called from a
+ * lifecycle callback also registered with this monitor (eg: it would be horriblely wrong if a
+ * callback sees PAUSED and calls this method with the same activity and gets RESUMED.
+ * </p>
+ *
+ * @param activity an activity in this application.
+ * @return the lifecycle stage this activity is in.
+ * @throws IllegalArgumentException if activity is unknown to the monitor.
+ * @throws NullPointerException if activity is null.
+ * @throws IllegalStateException if called off the main thread.
+ */
+ Stage getLifecycleStageOf(Activity activity);
+
+ /**
+ * Returns all activities in a given stage of their lifecycle.
+ * <p>
+ * This method can only return a consistant and correct answer from the main thread, therefore
+ * callers should always invoke it from the main thread and implementors are free to throw an
+ * exception if the call is not made on the main thread.
+ * </p>
+ * <p>
+ * Implementors should ensure this method returns a consistant response if called from a
+ * lifecycle callback also registered with this monitor (eg: it would be horriblely wrong if a
+ * callback sees PAUSED and calls this method with the PAUSED and does not see its activity in
+ * the response.
+ * </p>
+ * <p>
+ * Callers should be aware that the monitor implementation may not hold strong references to the
+ * Activities in the application. Therefore stages which are considered end stages or eligible
+ * for garbage collection on low memory situations may not return an instance of a particular
+ * activity if it has been garbage collected.
+ *
+ * @param stage the stage to query for.
+ * @return a snapshot Collection of activities in the given stage. This collection may be empty.
+ * @throws IllegalStateException if called from outside the main thread.
+ */
+ Collection<Activity> getActivitiesInStage(Stage stage);
+}
diff --git a/support/src/android/support/test/runner/lifecycle/Stage.java b/support/src/android/support/test/runner/lifecycle/Stage.java
new file mode 100644
index 0000000..49f212e
--- /dev/null
+++ b/support/src/android/support/test/runner/lifecycle/Stage.java
@@ -0,0 +1,42 @@
+/*
+ * 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 android.support.test.runner.lifecycle;
+
+/**
+ * An enumeration of the lifecycle stages an activity undergoes.
+ * <p>
+ * See the {@link android.app.Activity} javadoc for detailed documentation.
+ * </p>
+ */
+public enum Stage {
+ /** Indicates that onCreate is being called before any onCreate code executes.*/
+ PRE_ON_CREATE,
+ /** Indicates that onCreate has been called. */
+ CREATED,
+ /** Indicates that onStart has been called. */
+ STARTED,
+ /** Indicates that onResume has been called - activity is now visible to user. */
+ RESUMED,
+ /** Indicates that onPause has been called - activity is no longer in the foreground. */
+ PAUSED,
+ /** Indicates that onStop has been called - activity is no longer visible to the user. */
+ STOPPED,
+ /** Indicates that onResume has been called - we have navigated back to the activity. */
+ RESTARTED,
+ /** Indicates that onDestroy has been called - system is shutting down the activity. */
+ DESTROYED
+}
diff --git a/support/tests/.classpath b/support/tests/.classpath
index 487c349..730d799 100644
--- a/support/tests/.classpath
+++ b/support/tests/.classpath
@@ -8,6 +8,8 @@
<classpathentry kind="src" path="mockito-src"/>
<classpathentry kind="src" path="dexmaker-mockito"/>
<classpathentry kind="src" path="android-support-test-src"/>
+ <classpathentry kind="src" path="hamcrest-library-src"/>
+ <classpathentry kind="src" path="hamcrest-integration-src"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
diff --git a/support/tests/.project b/support/tests/.project
index 8070e85..6608212 100644
--- a/support/tests/.project
+++ b/support/tests/.project
@@ -52,6 +52,16 @@
<locationURI>ANDROID_TOP/external/dexmaker/src/mockito/java</locationURI>
</link>
<link>
+ <name>hamcrest-integration-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/hamcrest/integration/src</locationURI>
+ </link>
+ <link>
+ <name>hamcrest-library-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/hamcrest/library/src</locationURI>
+ </link>
+ <link>
<name>hamcrest-src</name>
<type>2</type>
<locationURI>ANDROID_TOP/external/hamcrest/src</locationURI>
diff --git a/support/tests/Android.mk b/support/tests/Android.mk
index 02cbe1a..6cf0c54 100644
--- a/support/tests/Android.mk
+++ b/support/tests/Android.mk
@@ -28,7 +28,7 @@ LOCAL_MODULE_TAGS := tests
# SDK 10 needed for mockito/objnesis. Otherwise 8 would work
LOCAL_SDK_VERSION := 10
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-test mockito-target dexmaker
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test mockito-target dexmaker hamcrest-library hamcrest-integration
LOCAL_PROGUARD_ENABLED := disabled
diff --git a/support/tests/AndroidManifest.xml b/support/tests/AndroidManifest.xml
index 9585a9e..5cf3430 100644
--- a/support/tests/AndroidManifest.xml
+++ b/support/tests/AndroidManifest.xml
@@ -18,6 +18,7 @@
package="android.support.test.tests">
<application>
+ <uses-library android:name="android.test.runner" />
</application>
<instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/support/tests/src/android/support/test/internal/runner/InstrumentationArgumentsRegistryTest.java b/support/tests/src/android/support/test/internal/runner/InstrumentationArgumentsRegistryTest.java
new file mode 100644
index 0000000..864d32e
--- /dev/null
+++ b/support/tests/src/android/support/test/internal/runner/InstrumentationArgumentsRegistryTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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 android.support.test.internal.runner;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.lessThan;
+
+import android.os.Bundle;
+import android.support.test.internal.runner.InstrumentationArgumentsRegistry;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * InstrumentationArgumentsRegistry tests.
+ */
+@SmallTest
+public class InstrumentationArgumentsRegistryTest extends TestCase {
+
+ public void testArgumentsArePopulated() {
+ assertNotNull(InstrumentationArgumentsRegistry.getInstance());
+ }
+
+ public void testModifyingReadBundleShouldNotAffectFutureReads() {
+ Bundle readArguments = InstrumentationArgumentsRegistry.getInstance();
+ int originalSize = readArguments.size();
+
+ readArguments.putString("mykey", "myvalue");
+
+ assertThat(originalSize, lessThan(readArguments.size()));
+ // Subsequent reads should not be affected by the local modifications.
+ assertEquals(originalSize, InstrumentationArgumentsRegistry.getInstance().size());
+ }
+
+ public void testModifyingSetBundleShouldNotAffectFutureReads() {
+ Bundle setArguments = new Bundle();
+ int originalSize = setArguments.size();
+ InstrumentationArgumentsRegistry.registerInstance(setArguments);
+ Bundle readArguments = InstrumentationArgumentsRegistry.getInstance();
+ assertEquals(originalSize, readArguments.size());
+
+ readArguments.putString("mykey", "myvalue");
+
+ // Subsequent reads should not be affected by the local modifications.
+ assertEquals(originalSize, InstrumentationArgumentsRegistry.getInstance().size());
+ }
+} \ No newline at end of file
diff --git a/support/tests/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorImplTest.java b/support/tests/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorImplTest.java
new file mode 100644
index 0000000..aa5de5a
--- /dev/null
+++ b/support/tests/src/android/support/test/internal/runner/lifecycle/ActivityLifecycleMonitorImplTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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 android.support.test.internal.runner.lifecycle;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.isIn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.support.test.internal.runner.lifecycle.ActivityLifecycleMonitorImpl;
+import android.support.test.runner.lifecycle.ActivityLifecycleCallback;
+import android.support.test.runner.lifecycle.Stage;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * ActivityLifecycleMonitorImpl tests.
+ */
+@SmallTest
+public class ActivityLifecycleMonitorImplTest extends TestCase {
+
+ private final Activity mMockActivity = mock(Activity.class);
+
+ private final ActivityLifecycleMonitorImpl mMonitor = new ActivityLifecycleMonitorImpl(true);
+
+ public void testAddRemoveListener() {
+ ActivityLifecycleCallback callback = mock(ActivityLifecycleCallback.class);
+
+ // multiple adds should only register once.
+ mMonitor.addLifecycleCallback(callback);
+ mMonitor.addLifecycleCallback(callback);
+ mMonitor.addLifecycleCallback(callback);
+
+ mMonitor.signalLifecycleChange(Stage.CREATED, mMockActivity);
+ mMonitor.signalLifecycleChange(Stage.STARTED, mMockActivity);
+
+ // multiple removes should no-op.
+ mMonitor.removeLifecycleCallback(callback);
+ mMonitor.removeLifecycleCallback(callback);
+
+ mMonitor.signalLifecycleChange(Stage.DESTROYED, mMockActivity);
+
+
+ verify(callback).onActivityLifecycleChanged(mMockActivity, Stage.CREATED);
+ verify(callback).onActivityLifecycleChanged(mMockActivity, Stage.STARTED);
+ verify(callback, never()).onActivityLifecycleChanged(mMockActivity, Stage.DESTROYED);
+ }
+
+ public void testCallbackConsistancy() {
+ ConsistancyCheckingCallback callback = new ConsistancyCheckingCallback();
+ mMonitor.addLifecycleCallback(callback);
+
+ for (Stage stage : Stage.values()) {
+ mMonitor.signalLifecycleChange(stage, mMockActivity);
+ if (null != callback.mError) {
+ throw callback.mError;
+ }
+ }
+ }
+
+ public void testDirectQueries() {
+ Activity mock1 = mock(Activity.class);
+ Activity mock2 = mock(Activity.class);
+ Activity mock3 = mock(Activity.class);
+
+ mMonitor.signalLifecycleChange(Stage.CREATED, mock1);
+ mMonitor.signalLifecycleChange(Stage.CREATED, mock2);
+ mMonitor.signalLifecycleChange(Stage.CREATED, mock3);
+
+ assertThat(mMonitor.getLifecycleStageOf(mock1), is(Stage.CREATED));
+ assertThat(mMonitor.getLifecycleStageOf(mock2), is(Stage.CREATED));
+ assertThat(mMonitor.getLifecycleStageOf(mock3), is(Stage.CREATED));
+
+ List<Activity> expectedActivities = new ArrayList<Activity>();
+ expectedActivities.add(mock1);
+ expectedActivities.add(mock2);
+ expectedActivities.add(mock3);
+
+ assertTrue(expectedActivities.containsAll(mMonitor.getActivitiesInStage(Stage.CREATED)));
+
+ mMonitor.signalLifecycleChange(Stage.DESTROYED, mock1);
+ mMonitor.signalLifecycleChange(Stage.PAUSED, mock2);
+ mMonitor.signalLifecycleChange(Stage.PAUSED, mock3);
+ assertThat(mMonitor.getLifecycleStageOf(mock1), is(Stage.DESTROYED));
+ assertThat(mMonitor.getLifecycleStageOf(mock2), is(Stage.PAUSED));
+ assertThat(mMonitor.getLifecycleStageOf(mock3), is(Stage.PAUSED));
+
+ assertThat(mMonitor.getActivitiesInStage(Stage.CREATED).isEmpty(), is(true));
+ assertThat(mock1, isIn(mMonitor.getActivitiesInStage(Stage.DESTROYED)));
+ assertThat(mock2, isIn(mMonitor.getActivitiesInStage(Stage.PAUSED)));
+ assertThat(mock3, isIn(mMonitor.getActivitiesInStage(Stage.PAUSED)));
+ }
+
+ private class ConsistancyCheckingCallback implements ActivityLifecycleCallback {
+ private RuntimeException mError = null;
+
+ @Override
+ public void onActivityLifecycleChanged(Activity activity, Stage stage) {
+ try {
+ assertThat(activity, isIn(mMonitor.getActivitiesInStage(stage)));
+ assertThat(mMonitor.getLifecycleStageOf(activity), is(stage));
+ } catch (RuntimeException re) {
+ mError = re;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/support/tests/src/android/support/test/runner/AndroidJUnitRunnerLifeCycleTest.java b/support/tests/src/android/support/test/runner/AndroidJUnitRunnerLifeCycleTest.java
new file mode 100644
index 0000000..8b25a20
--- /dev/null
+++ b/support/tests/src/android/support/test/runner/AndroidJUnitRunnerLifeCycleTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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 android.support.test.runner;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.isIn;
+import static org.mockito.Mockito.inOrder;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.test.internal.runner.InstrumentationArgumentsRegistry;
+import android.support.test.internal.runner.lifecycle.ActivityLifecycleMonitorRegistry;
+import android.support.test.runner.lifecycle.ActivityLifecycleCallback;
+import android.support.test.runner.lifecycle.ActivityLifecycleMonitor;
+import android.support.test.runner.lifecycle.Stage;
+import android.test.ActivityUnitTestCase;
+import android.test.UiThreadTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+
+/**
+ * Integration tests between lifecycle management methods in runner and the LifecycleMonitor.
+ */
+@MediumTest
+public class AndroidJUnitRunnerLifeCycleTest
+extends ActivityUnitTestCase<AndroidJUnitRunnerLifeCycleTest.PublicLifecycleMethodActivity> {
+
+ private final ActivityLifecycleCallback mCallback = Mockito.mock(
+ ActivityLifecycleCallback.class);
+
+ private PublicLifecycleMethodActivity mSpiedActivity;
+ private ActivityLifecycleMonitor mMonitor;
+
+ public AndroidJUnitRunnerLifeCycleTest() {
+ super(PublicLifecycleMethodActivity.class);
+ }
+
+ @Override
+ public void setActivity(Activity activity) {
+ if (null != activity) {
+ mSpiedActivity = Mockito.spy((PublicLifecycleMethodActivity) activity);
+ }
+ }
+
+ @Override
+ public PublicLifecycleMethodActivity getActivity() {
+ // otherwise ActivityUnitTestCase will call onCreate which will have side
+ // effects in lifecycle tracking which we are specifically testing for.
+ return null;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mMonitor = ActivityLifecycleMonitorRegistry.getInstance();
+ mMonitor.addLifecycleCallback(mCallback);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mMonitor.removeLifecycleCallback(mCallback);
+ super.tearDown();
+ }
+
+ public void testInstrumentationArgumentsRegistryGetsPopulated() {
+ assertNotNull(InstrumentationArgumentsRegistry.getInstance());
+ }
+
+ @UiThreadTest
+ public void testActivityPreOnCreateCalled() {
+ startActivity(new Intent(), null, null);
+ mSpiedActivity.setRunnableForOnCreate(new Runnable() {
+ @Override
+ public void run() {
+ assertThat(mSpiedActivity, isIn(mMonitor.getActivitiesInStage(Stage.PRE_ON_CREATE)));
+ assertThat(mMonitor.getLifecycleStageOf(mSpiedActivity), is(Stage.PRE_ON_CREATE));
+ }
+ });
+ getInstrumentation().callActivityOnCreate(mSpiedActivity, new Bundle());
+ }
+
+ // temporarily suppress - fails due to some sort of mockito issue
+ @Suppress
+ @UiThreadTest
+ public void testOnStartStopCalled() {
+ startActivity(new Intent(), null, null);
+
+ // if we dont pair start/stop together the test runner will block until a timeout
+ // occurs waiting for the activity to stop.
+ getInstrumentation().callActivityOnStart(mSpiedActivity);
+ assertThat(mSpiedActivity, isIn(mMonitor.getActivitiesInStage(Stage.STARTED)));
+ assertThat(mMonitor.getLifecycleStageOf(mSpiedActivity), is(Stage.STARTED));
+
+ getInstrumentation().callActivityOnStop(mSpiedActivity);
+ assertThat(mSpiedActivity, isIn(mMonitor.getActivitiesInStage(Stage.STOPPED)));
+ assertThat(mMonitor.getLifecycleStageOf(mSpiedActivity), is(Stage.STOPPED));
+
+ InOrder order = inOrder(mSpiedActivity, mCallback);
+ order.verify(mSpiedActivity).onStart();
+ order.verify(mCallback).onActivityLifecycleChanged(mSpiedActivity, Stage.STARTED);
+ order.verify(mSpiedActivity).onStop();
+ order.verify(mCallback).onActivityLifecycleChanged(mSpiedActivity, Stage.STOPPED);
+ }
+
+ @UiThreadTest
+ public void testOnCreateCalled() {
+ startActivity(new Intent(), null, null);
+ Bundle b = new Bundle();
+ getInstrumentation().callActivityOnCreate(mSpiedActivity, b);
+
+ assertThat(mSpiedActivity, isIn(mMonitor.getActivitiesInStage(Stage.CREATED)));
+ assertThat(mMonitor.getLifecycleStageOf(mSpiedActivity), is(Stage.CREATED));
+ InOrder order = inOrder(mSpiedActivity, mCallback);
+ order.verify(mSpiedActivity).onCreate(b);
+ order.verify(mCallback).onActivityLifecycleChanged(mSpiedActivity, Stage.CREATED);
+ }
+
+ @UiThreadTest
+ public void testOnResumeCalled() {
+ startActivity(new Intent(), null, null);
+ getInstrumentation().callActivityOnResume(mSpiedActivity);
+
+ assertThat(mSpiedActivity, isIn(mMonitor.getActivitiesInStage(Stage.RESUMED)));
+ assertThat(mMonitor.getLifecycleStageOf(mSpiedActivity), is(Stage.RESUMED));
+ InOrder order = inOrder(mSpiedActivity, mCallback);
+ order.verify(mSpiedActivity).onResume();
+ order.verify(mCallback).onActivityLifecycleChanged(mSpiedActivity, Stage.RESUMED);
+ }
+
+ @UiThreadTest
+ public void testOnPauseCalled() {
+ startActivity(new Intent(), null, null);
+ getInstrumentation().callActivityOnPause(mSpiedActivity);
+
+ assertThat(mSpiedActivity, isIn(mMonitor.getActivitiesInStage(Stage.PAUSED)));
+ assertThat(mMonitor.getLifecycleStageOf(mSpiedActivity), is(Stage.PAUSED));
+ InOrder order = inOrder(mSpiedActivity, mCallback);
+ order.verify(mSpiedActivity).onPause();
+ order.verify(mCallback).onActivityLifecycleChanged(mSpiedActivity, Stage.PAUSED);
+ }
+
+ @UiThreadTest
+ public void testOnRestartCalled() {
+ startActivity(new Intent(), null, null);
+ getInstrumentation().callActivityOnRestart(mSpiedActivity);
+
+ assertThat(mSpiedActivity, isIn(mMonitor.getActivitiesInStage(Stage.RESTARTED)));
+ assertThat(mMonitor.getLifecycleStageOf(mSpiedActivity), is(Stage.RESTARTED));
+ InOrder order = inOrder(mSpiedActivity, mCallback);
+ order.verify(mSpiedActivity).onRestart();
+ order.verify(mCallback).onActivityLifecycleChanged(mSpiedActivity, Stage.RESTARTED);
+ }
+
+ @UiThreadTest
+ public void testOnDestroyCalled() {
+ startActivity(new Intent(), null, null);
+ getInstrumentation().callActivityOnDestroy(mSpiedActivity);
+
+ assertThat(mSpiedActivity, isIn(mMonitor.getActivitiesInStage(Stage.DESTROYED)));
+ assertThat(mMonitor.getLifecycleStageOf(mSpiedActivity), is(Stage.DESTROYED));
+ InOrder order = inOrder(mSpiedActivity, mCallback);
+ order.verify(mSpiedActivity).onDestroy();
+ order.verify(mCallback).onActivityLifecycleChanged(mSpiedActivity, Stage.DESTROYED);
+ }
+
+ /**
+ * Makes lifecycle methods public so we can verify on them.
+ *
+ */
+ public static class PublicLifecycleMethodActivity extends Activity {
+ private Runnable runnableForOnCreate;
+
+ /**
+ * Invokes the runnable in onCreate of this activity.
+ *
+ * @param runnable runnable to invoke in onCreate or {@code null} for no-op runnable.
+ */
+ public void setRunnableForOnCreate(Runnable runnable) {
+ runnableForOnCreate = runnable;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ }
+
+ @Override
+ public void onCreate(Bundle b) {
+ super.onCreate(b);
+ if (runnableForOnCreate != null) {
+ runnableForOnCreate.run();
+ }
+ }
+
+ @Override
+ public void onRestart() {
+ super.onRestart();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+ }
+} \ No newline at end of file