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 * @Rule public LeakCheck mLeakChecker = new LeakCheck(); 42 * 43 * @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