diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java index ddd661558be59..27d781dd7a8f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java @@ -52,7 +52,6 @@ public class TileServicesTest extends SysuiTestCase { @Before public void setUp() throws Exception { - TestableLooper.get(this).setAsMainLooper(); mManagers = new ArrayList<>(); QSTileHost host = new QSTileHost(mContext, null, mock(StatusBarIconController.class)); diff --git a/tests/testables/src/android/testing/AndroidTestingRunner.java b/tests/testables/src/android/testing/AndroidTestingRunner.java index a425f70e836ce..cf5d4cf2f8287 100644 --- a/tests/testables/src/android/testing/AndroidTestingRunner.java +++ b/tests/testables/src/android/testing/AndroidTestingRunner.java @@ -35,6 +35,8 @@ import java.util.List; /** * A runner with support for extra annotations provided by the Testables library. + * @see UiThreadTest + * @see TestableLooper.RunWithLooper */ public class AndroidTestingRunner extends BlockJUnit4ClassRunner { diff --git a/tests/testables/src/android/testing/BaseFragmentTest.java b/tests/testables/src/android/testing/BaseFragmentTest.java index 32ee091a46c94..5cedbdffed358 100644 --- a/tests/testables/src/android/testing/BaseFragmentTest.java +++ b/tests/testables/src/android/testing/BaseFragmentTest.java @@ -80,6 +80,10 @@ public abstract class BaseFragmentTest { }); } + /** + * Allows tests to sub-class TestableContext if they want to provide any extended functionality + * or provide a {@link LeakCheck} to the TestableContext upon instantiation. + */ protected TestableContext getContext() { return new TestableContext(InstrumentationRegistry.getContext()); } diff --git a/tests/testables/src/android/testing/LeakCheck.java b/tests/testables/src/android/testing/LeakCheck.java index 8daaa8f166640..949215b4d3119 100644 --- a/tests/testables/src/android/testing/LeakCheck.java +++ b/tests/testables/src/android/testing/LeakCheck.java @@ -14,6 +14,7 @@ package android.testing; +import android.content.Context; import android.util.ArrayMap; import android.util.Log; @@ -28,6 +29,35 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +/** + * Utility for dealing with the facts of Lifecycle. Creates trackers to check that for every + * call to registerX, addX, bindX, a corresponding call to unregisterX, removeX, and unbindX + * is performed. This should be applied to a test as a {@link org.junit.rules.TestRule} + * and will only check for leaks on successful tests. + *

+ * Example that will catch an allocation and fail: + *

+ * public class LeakCheckTest {
+ *    @Rule public LeakCheck mLeakChecker = new LeakCheck();
+ *
+ *    @Test
+ *    public void testLeak() {
+ *        Context context = new ContextWrapper(...) {
+ *            public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ *                mLeakChecker.getTracker("receivers").addAllocation(new Throwable());
+ *            }
+ *            public void unregisterReceiver(BroadcastReceiver receiver) {
+ *                mLeakChecker.getTracker("receivers").clearAllocations();
+ *            }
+ *        };
+ *        context.registerReceiver(...);
+ *    }
+ *  }
+ * 
+ * + * Note: {@link TestableContext} supports leak tracking when using + * {@link TestableContext#TestableContext(Context, LeakCheck)}. + */ public class LeakCheck extends TestWatcher { private final Map mTrackers = new HashMap<>(); @@ -40,6 +70,13 @@ public class LeakCheck extends TestWatcher { verify(); } + /** + * Acquire a {@link Tracker}. Gets a tracker for the specified tag, creating one if necessary. + * There should be one tracker for each pair of add/remove callbacks (e.g. one tracker for + * registerReceiver/unregisterReceiver). + * + * @param tag Unique tag to use for this set of allocation tracking. + */ public Tracker getTracker(String tag) { Tracker t = mTrackers.get(tag); if (t == null) { @@ -49,10 +86,13 @@ public class LeakCheck extends TestWatcher { return t; } - public void verify() { + private void verify() { mTrackers.values().forEach(Tracker::verify); } + /** + * Holds allocations associated with a specific callback (such as a BroadcastReceiver). + */ public static class LeakInfo { private static final String TAG = "LeakInfo"; private List mThrowables = new ArrayList<>(); @@ -60,11 +100,20 @@ public class LeakCheck extends TestWatcher { LeakInfo() { } + /** + * Should be called once for each callback/listener added. addAllocation may be + * called several times, but it only takes one clearAllocations call to remove all + * of them. + */ public void addAllocation(Throwable t) { // TODO: Drop off the first element in the stack trace here to have a cleaner stack. mThrowables.add(t); } + /** + * Should be called when the callback/listener has been removed. One call to + * clearAllocations will counteract any number of calls to addAllocation. + */ public void clearAllocations() { mThrowables.clear(); } @@ -82,9 +131,16 @@ public class LeakCheck extends TestWatcher { } } + /** + * Tracks allocations related to a specific tag or method(s). + * @see #getTracker(String) + */ public static class Tracker { private Map mObjects = new ArrayMap<>(); + private Tracker() { + } + public LeakInfo getLeakInfo(Object object) { LeakInfo leakInfo = mObjects.get(object); if (leakInfo == null) { diff --git a/tests/testables/src/android/testing/TestableContentResolver.java b/tests/testables/src/android/testing/TestableContentResolver.java index bfafbe043ffbf..0850916ccbe57 100644 --- a/tests/testables/src/android/testing/TestableContentResolver.java +++ b/tests/testables/src/android/testing/TestableContentResolver.java @@ -27,7 +27,11 @@ import com.google.android.collect.Maps; import java.util.Map; /** - * Alternative to a MockContentResolver that falls back to real providers. + * A version of ContentResolver that allows easy mocking of providers. + * By default it acts as a normal ContentResolver and returns all the + * same providers. + * @see #addProvider(String, ContentProvider) + * @see #setFallbackToExisting(boolean) */ public class TestableContentResolver extends ContentResolver { diff --git a/tests/testables/src/android/testing/TestableContext.java b/tests/testables/src/android/testing/TestableContext.java index d6c06e47fcdf7..498d517c104bb 100644 --- a/tests/testables/src/android/testing/TestableContext.java +++ b/tests/testables/src/android/testing/TestableContext.java @@ -43,6 +43,7 @@ import org.junit.runners.model.Statement; *
    *
  • System services can be mocked out with {@link #addMockSystemService}
  • *
  • Service binding can be mocked out with {@link #addMockService}
  • + *
  • Resources can be mocked out using {@link #getOrCreateTestableResources()}
  • *
  • Settings support {@link TestableSettingsProvider}
  • *
  • Has support for {@link LeakCheck} for services and receivers
  • *
@@ -50,10 +51,8 @@ import org.junit.runners.model.Statement; *

TestableContext should be defined as a rule on your test so it can clean up after itself. * Like the following:

*
- * {@literal
- * @Rule
+ * @Rule
  * private final TestableContext mContext = new TestableContext(InstrumentationRegister.getContext());
- * }
  * 
*/ public class TestableContext extends ContextWrapper implements TestRule { @@ -132,20 +131,26 @@ public class TestableContext extends ContextWrapper implements TestRule { : super.getResources(); } + /** + * @see #getSystemService(String) + */ public void addMockSystemService(Class service, T mock) { addMockSystemService(getSystemServiceName(service), mock); } + /** + * @see #getSystemService(String) + */ public void addMockSystemService(String name, Object service) { if (mMockSystemServices == null) mMockSystemServices = new ArrayMap<>(); mMockSystemServices.put(name, service); } - public void addMockService(ComponentName component, IBinder service) { - if (mMockServices == null) mMockServices = new ArrayMap<>(); - mMockServices.put(component, service); - } - + /** + * If a matching mock service has been added through {@link #addMockSystemService} then + * that will be returned, otherwise the real service will be acquired from the base + * context. + */ @Override public Object getSystemService(String name) { if (mMockSystemServices != null && mMockSystemServices.containsKey(name)) { @@ -166,6 +171,10 @@ public class TestableContext extends ContextWrapper implements TestRule { return mTestableContentResolver; } + /** + * Will always return itself for a TestableContext to ensure the testable effects extend + * to the application context. + */ @Override public Context getApplicationContext() { // Return this so its always a TestableContext. @@ -199,6 +208,24 @@ public class TestableContext extends ContextWrapper implements TestRule { super.unregisterReceiver(receiver); } + /** + * Adds a mock service to be connected to by a bindService call. + *

+ * Normally a TestableContext will pass through all bind requests to the base context + * but when addMockService has been called for a ComponentName being bound, then + * TestableContext will immediately trigger a {@link ServiceConnection#onServiceConnected} + * with the specified service, and will call {@link ServiceConnection#onServiceDisconnected} + * when the service is unbound. + *

+ */ + public void addMockService(ComponentName component, IBinder service) { + if (mMockServices == null) mMockServices = new ArrayMap<>(); + mMockServices.put(component, service); + } + + /** + * @see #addMockService(ComponentName, IBinder) + */ @Override public boolean bindService(Intent service, ServiceConnection conn, int flags) { if (mService != null) mService.getLeakInfo(conn).addAllocation(new Throwable()); @@ -206,6 +233,9 @@ public class TestableContext extends ContextWrapper implements TestRule { return super.bindService(service, conn, flags); } + /** + * @see #addMockService(ComponentName, IBinder) + */ @Override public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, Handler handler, UserHandle user) { @@ -214,6 +244,9 @@ public class TestableContext extends ContextWrapper implements TestRule { return super.bindServiceAsUser(service, conn, flags, handler, user); } + /** + * @see #addMockService(ComponentName, IBinder) + */ @Override public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, UserHandle user) { @@ -232,6 +265,9 @@ public class TestableContext extends ContextWrapper implements TestRule { return false; } + /** + * @see #addMockService(ComponentName, IBinder) + */ @Override public void unbindService(ServiceConnection conn) { if (mService != null) mService.getLeakInfo(conn).clearAllocations(); @@ -243,6 +279,13 @@ public class TestableContext extends ContextWrapper implements TestRule { super.unbindService(conn); } + /** + * Check if the TestableContext has a mock binding for a specified component. Will return + * true between {@link ServiceConnection#onServiceConnected} and + * {@link ServiceConnection#onServiceDisconnected} callbacks for a mock service. + * + * @see #addMockService(ComponentName, IBinder) + */ public boolean isBound(ComponentName component) { return mActiveServices != null && mActiveServices.containsValue(component); } diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 9eddc5112d0cd..f6c3cb3ec4981 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -33,16 +33,15 @@ import java.lang.reflect.Field; import java.util.Map; /** - * Creates a looper on the current thread with control over if/when messages are - * executed. Warning: This class works through some reflection and may break/need - * to be updated from time to time. + * This is a wrapper around {@link TestLooperManager} to make it easier to manage + * and provide an easy annotation for use with tests. + * + * @see TestableLooperTest TestableLooperTest for examples. */ public class TestableLooper { private Looper mLooper; private MessageQueue mQueue; - private boolean mMain; - private Object mOriginalMain; private MessageHandler mMessageHandler; private Handler mHandler; @@ -72,35 +71,20 @@ public class TestableLooper { mHandler = new Handler(mLooper); } - public void setAsMainLooper() throws NoSuchFieldException, IllegalAccessException { - mMain = true; - setAsMainInt(); - } - - private void setAsMainInt() throws NoSuchFieldException, IllegalAccessException { - Field field = mLooper.getClass().getDeclaredField("sMainLooper"); - field.setAccessible(true); - if (mOriginalMain == null) { - mOriginalMain = field.get(null); - } - field.set(null, mLooper); - } - /** - * Must be called if setAsMainLooper is called to restore the main looper when the - * test is complete, otherwise the main looper will not be available for any subsequent - * tests. + * Must be called to release the looper when the test is complete, otherwise + * the looper will not be available for any subsequent tests. This is + * automatically handled for tests using {@link RunWithLooper}. */ public void destroy() throws NoSuchFieldException, IllegalAccessException { mQueueWrapper.release(); - if (mMain && mOriginalMain != null) { - Field field = mLooper.getClass().getDeclaredField("sMainLooper"); - field.setAccessible(true); - field.set(null, mOriginalMain); - mOriginalMain = null; - } } + /** + * Sets a callback for all messages processed on this TestableLooper. + * + * @see {@link MessageHandler} + */ public void setMessageHandler(MessageHandler handler) { mMessageHandler = handler; } @@ -119,6 +103,9 @@ public class TestableLooper { return num; } + /** + * Process messages in the queue until no more are found. + */ public void processAllMessages() { while (processQueuedMessages() != 0) ; } @@ -183,6 +170,11 @@ public class TestableLooper { void run() throws Exception; } + /** + * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and + * run this test/class on that thread. The {@link TestableLooper} can be acquired using + * {@link #get(Object)}. + */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface RunWithLooper { @@ -206,11 +198,15 @@ public class TestableLooper { private static final Map sLoopers = new ArrayMap<>(); + /** + * For use with {@link RunWithLooper}, used to get the TestableLooper that was + * automatically created for this test. + */ public static TestableLooper get(Object test) { return sLoopers.get(test); } - public static class LooperFrameworkMethod extends FrameworkMethod { + static class LooperFrameworkMethod extends FrameworkMethod { private HandlerThread mHandlerThread; private final TestableLooper mTestableLooper; @@ -319,6 +315,11 @@ public class TestableLooper { } } + /** + * Callback to control the execution of messages on the looper, when set with + * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)} + * will get called back for every message processed on the {@link TestableLooper}. + */ public interface MessageHandler { /** * Return true to have the message executed and delivered to target. diff --git a/tests/testables/src/android/testing/UiThreadTest.java b/tests/testables/src/android/testing/UiThreadTest.java index e40e1d741f286..32a58240bae39 100644 --- a/tests/testables/src/android/testing/UiThreadTest.java +++ b/tests/testables/src/android/testing/UiThreadTest.java @@ -20,8 +20,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * When applied to a class, all tests, befores, and afters will behave as if - * they have @UiThreadTest applied to them. + * When applied to a class, all {@link org.junit.Test}s, {@link org.junit.After}s, and + * {@link org.junit.Before} will behave as if they have @UiThreadTest applied to them. */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) diff --git a/tests/testables/src/android/testing/ViewUtils.java b/tests/testables/src/android/testing/ViewUtils.java index 5a651aacc74f2..74789989939eb 100644 --- a/tests/testables/src/android/testing/ViewUtils.java +++ b/tests/testables/src/android/testing/ViewUtils.java @@ -21,8 +21,16 @@ import android.view.View; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; +/** + * Utilities to make testing views easier. + */ public class ViewUtils { + /** + * Causes the view (and its children) to have {@link View#onAttachedToWindow()} called. + * + * This is currently done by adding the view to a window. + */ public static void attachView(View view) { // Make sure hardware acceleration isn't turned on. view.getContext().getApplicationInfo().flags &= @@ -35,6 +43,11 @@ public class ViewUtils { .getSystemService(WindowManager.class).addView(view, lp); } + /** + * Causes the view (and its children) to have {@link View#onDetachedFromWindow()} called. + * + * This is currently done by removing the view from a window. + */ public static void detachView(View view) { InstrumentationRegistry.getContext() .getSystemService(WindowManager.class).removeViewImmediate(view);