diff options
author | Brett Chabot <brettchabot@google.com> | 2014-06-11 13:18:38 -0700 |
---|---|---|
committer | Brett Chabot <brettchabot@google.com> | 2014-06-13 14:43:41 -0700 |
commit | bac1a737514945ce33a2be2f9b8c5d63430b5234 (patch) | |
tree | a4130fae725eed95556e84274347a092895c0abd | |
parent | 1afabcc1a3db33f786ff7cc0c8d72e40990c8876 (diff) | |
download | testing-bac1a737514945ce33a2be2f9b8c5d63430b5234.tar.gz |
Merge GoogleInstrumentationTestRunner features into android.support.test.
Change-Id: I07e47038b3e50234b5419499410a253d7ff89a99
19 files changed, 1624 insertions, 12 deletions
@@ -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 |