Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 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 | |
Jason Monk | 0c40800 | 2017-05-03 15:43:52 -0400 | [diff] [blame] | 17 | import android.content.Context; |
Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 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 | |
Jason Monk | 0c40800 | 2017-05-03 15:43:52 -0400 | [diff] [blame] | 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 | */ |
Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 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 | |
Jason Monk | 0c40800 | 2017-05-03 15:43:52 -0400 | [diff] [blame] | 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 | */ |
Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 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 | |
Jason Monk | 0c40800 | 2017-05-03 15:43:52 -0400 | [diff] [blame] | 89 | private void verify() { |
Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 90 | mTrackers.values().forEach(Tracker::verify); |
| 91 | } |
| 92 | |
Jason Monk | 0c40800 | 2017-05-03 15:43:52 -0400 | [diff] [blame] | 93 | /** |
| 94 | * Holds allocations associated with a specific callback (such as a BroadcastReceiver). |
| 95 | */ |
Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 96 | public static class LeakInfo { |
| 97 | private static final String TAG = "LeakInfo"; |
| 98 | private List<Throwable> mThrowables = new ArrayList<>(); |
| 99 | |
| 100 | LeakInfo() { |
| 101 | } |
| 102 | |
Jason Monk | 0c40800 | 2017-05-03 15:43:52 -0400 | [diff] [blame] | 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 | */ |
Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 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 | |
Jason Monk | 0c40800 | 2017-05-03 15:43:52 -0400 | [diff] [blame] | 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 | */ |
Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 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 | |
Jason Monk | 0c40800 | 2017-05-03 15:43:52 -0400 | [diff] [blame] | 134 | /** |
| 135 | * Tracks allocations related to a specific tag or method(s). |
| 136 | * @see #getTracker(String) |
| 137 | */ |
Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 138 | public static class Tracker { |
| 139 | private Map<Object, LeakInfo> mObjects = new ArrayMap<>(); |
| 140 | |
Jason Monk | 0c40800 | 2017-05-03 15:43:52 -0400 | [diff] [blame] | 141 | private Tracker() { |
| 142 | } |
| 143 | |
Jason Monk | 340b0e5 | 2017-03-08 14:57:56 -0500 | [diff] [blame] | 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 | } |