Home | History | Annotate | Download | only in testing
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
      5  * except in compliance with the License. You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the
     10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     11  * KIND, either express or implied. See the License for the specific language governing
     12  * permissions and limitations under the License.
     13  */
     14 
     15 package android.testing;
     16 
     17 import android.content.Context;
     18 import android.util.ArrayMap;
     19 import android.util.Log;
     20 
     21 import org.junit.Assert;
     22 import org.junit.rules.TestWatcher;
     23 import org.junit.runner.Description;
     24 
     25 import java.io.PrintWriter;
     26 import java.io.StringWriter;
     27 import java.util.ArrayList;
     28 import java.util.HashMap;
     29 import java.util.List;
     30 import java.util.Map;
     31 
     32 /**
     33  * Utility for dealing with the facts of Lifecycle. Creates trackers to check that for every
     34  * call to registerX, addX, bindX, a corresponding call to unregisterX, removeX, and unbindX
     35  * is performed. This should be applied to a test as a {@link org.junit.rules.TestRule}
     36  * and will only check for leaks on successful tests.
     37  * <p>
     38  * Example that will catch an allocation and fail:
     39  * <pre class="prettyprint">
     40  * public class LeakCheckTest {
     41  *    &#064;Rule public LeakCheck mLeakChecker = new LeakCheck();
     42  *
     43  *    &#064;Test
     44  *    public void testLeak() {
     45  *        Context context = new ContextWrapper(...) {
     46  *            public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
     47  *                mLeakChecker.getTracker("receivers").addAllocation(new Throwable());
     48  *            }
     49  *            public void unregisterReceiver(BroadcastReceiver receiver) {
     50  *                mLeakChecker.getTracker("receivers").clearAllocations();
     51  *            }
     52  *        };
     53  *        context.registerReceiver(...);
     54  *    }
     55  *  }
     56  * </pre>
     57  *
     58  * Note: {@link TestableContext} supports leak tracking when using
     59  * {@link TestableContext#TestableContext(Context, LeakCheck)}.
     60  */
     61 public class LeakCheck extends TestWatcher {
     62 
     63     private final Map<String, Tracker> mTrackers = new HashMap<>();
     64 
     65     public LeakCheck() {
     66     }
     67 
     68     @Override
     69     protected void succeeded(Description description) {
     70         verify();
     71     }
     72 
     73     /**
     74      * Acquire a {@link Tracker}. Gets a tracker for the specified tag, creating one if necessary.
     75      * There should be one tracker for each pair of add/remove callbacks (e.g. one tracker for
     76      * registerReceiver/unregisterReceiver).
     77      *
     78      * @param tag Unique tag to use for this set of allocation tracking.
     79      */
     80     public Tracker getTracker(String tag) {
     81         Tracker t = mTrackers.get(tag);
     82         if (t == null) {
     83             t = new Tracker();
     84             mTrackers.put(tag, t);
     85         }
     86         return t;
     87     }
     88 
     89     private void verify() {
     90         mTrackers.values().forEach(Tracker::verify);
     91     }
     92 
     93     /**
     94      * Holds allocations associated with a specific callback (such as a BroadcastReceiver).
     95      */
     96     public static class LeakInfo {
     97         private static final String TAG = "LeakInfo";
     98         private List<Throwable> mThrowables = new ArrayList<>();
     99 
    100         LeakInfo() {
    101         }
    102 
    103         /**
    104          * Should be called once for each callback/listener added. addAllocation may be
    105          * called several times, but it only takes one clearAllocations call to remove all
    106          * of them.
    107          */
    108         public void addAllocation(Throwable t) {
    109             // TODO: Drop off the first element in the stack trace here to have a cleaner stack.
    110             mThrowables.add(t);
    111         }
    112 
    113         /**
    114          * Should be called when the callback/listener has been removed. One call to
    115          * clearAllocations will counteract any number of calls to addAllocation.
    116          */
    117         public void clearAllocations() {
    118             mThrowables.clear();
    119         }
    120 
    121         void verify() {
    122             if (mThrowables.size() == 0) return;
    123             Log.e(TAG, "Listener or binding not properly released");
    124             for (Throwable t : mThrowables) {
    125                 Log.e(TAG, "Allocation found", t);
    126             }
    127             StringWriter writer = new StringWriter();
    128             mThrowables.get(0).printStackTrace(new PrintWriter(writer));
    129             Assert.fail("Listener or binding not properly released\n"
    130                     + writer.toString());
    131         }
    132     }
    133 
    134     /**
    135      * Tracks allocations related to a specific tag or method(s).
    136      * @see #getTracker(String)
    137      */
    138     public static class Tracker {
    139         private Map<Object, LeakInfo> mObjects = new ArrayMap<>();
    140 
    141         private Tracker() {
    142         }
    143 
    144         public LeakInfo getLeakInfo(Object object) {
    145             LeakInfo leakInfo = mObjects.get(object);
    146             if (leakInfo == null) {
    147                 leakInfo = new LeakInfo();
    148                 mObjects.put(object, leakInfo);
    149             }
    150             return leakInfo;
    151         }
    152 
    153         void verify() {
    154             mObjects.values().forEach(LeakInfo::verify);
    155         }
    156     }
    157 }
    158