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 
LeakCheck()65     public LeakCheck() {
66     }
67 
68     @Override
succeeded(Description description)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      */
getTracker(String tag)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 
verify()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 
LeakInfo()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          */
addAllocation(Throwable t)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          */
clearAllocations()117         public void clearAllocations() {
118             mThrowables.clear();
119         }
120 
verify()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 
Tracker()141         private Tracker() {
142         }
143 
getLeakInfo(Object object)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 
verify()153         void verify() {
154             mObjects.values().forEach(LeakInfo::verify);
155         }
156     }
157 }
158