1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package dalvik.system; 17 18import java.lang.reflect.Method; 19import java.util.ArrayList; 20import java.util.Collection; 21import java.util.Collections; 22import java.util.List; 23import java.util.Set; 24import java.util.concurrent.ConcurrentHashMap; 25import java.util.function.BiConsumer; 26import org.junit.rules.TestRule; 27import org.junit.runner.Description; 28import org.junit.runners.model.Statement; 29 30/** 31 * Provides support for testing classes that use {@link CloseGuard} in order to detect resource 32 * leakages. 33 * 34 * <p>This class should not be used directly by tests as that will prevent them from being 35 * compilable and testable on OpenJDK platform. Instead they should use 36 * {@code libcore.junit.util.ResourceLeakageDetector} which accesses the capabilities of this using 37 * reflection and if it cannot find it (because it is running on OpenJDK) then it will just skip 38 * leakage detection. 39 * 40 * <p>This provides two entry points that are accessed reflectively: 41 * <ul> 42 * <li> 43 * <p>The {@link #getRule()} method. This returns a {@link TestRule} that will fail a test if it 44 * detects any resources that were allocated during the test but were not released. 45 * 46 * <p>This only tracks resources that were allocated on the test thread, although it does not care 47 * what thread they were released on. This avoids flaky false positives where a background thread 48 * allocates a resource during a test but releases it after the test. 49 * 50 * <p>It is still possible to have a false positive in the case where the test causes a caching 51 * mechanism to open a resource and hold it open past the end of the test. In that case if there is 52 * no way to clear the cached data then it should be relatively simple to move the code that invokes 53 * the caching mechanism to outside the scope of this rule. i.e. 54 * 55 * <pre>{@code 56 * @Rule 57 * public final TestRule ruleChain = org.junit.rules.RuleChain 58 * .outerRule(new ...invoke caching mechanism...) 59 * .around(CloseGuardSupport.getRule()); 60 * }</pre> 61 * </li> 62 * <li> 63 * <p>The {@link #getFinalizerChecker()} method. This returns a {@link BiConsumer} that takes an 64 * object that owns resources and an expected number of unreleased resources. It will call the 65 * {@link Object#finalize()} method on the object using reflection and throw an 66 * {@link AssertionError} if the number of reported unreleased resources does not match the 67 * expected number. 68 * </li> 69 * </ul> 70 */ 71public class CloseGuardSupport { 72 73 private static final TestRule CLOSE_GUARD_RULE = new FailTestWhenResourcesNotClosedRule(); 74 75 /** 76 * Get a {@link TestRule} that will detect when resources that use the {@link CloseGuard} 77 * mechanism are not cleaned up properly by a test. 78 * 79 * <p>If the {@link CloseGuard} mechanism is not supported, e.g. on OpenJDK, then the returned 80 * rule does nothing. 81 */ 82 public static TestRule getRule() { 83 return CLOSE_GUARD_RULE; 84 } 85 86 private CloseGuardSupport() { 87 } 88 89 /** 90 * Fails a test when resources are not cleaned up properly. 91 */ 92 private static class FailTestWhenResourcesNotClosedRule implements TestRule { 93 /** 94 * Returns a {@link Statement} that will fail the test if it ends with unreleased resources. 95 * @param base the test to be run. 96 */ 97 public Statement apply(Statement base, Description description) { 98 return new Statement() { 99 @Override 100 public void evaluate() throws Throwable { 101 // Get the previous tracker so that it can be restored afterwards. 102 CloseGuard.Tracker previousTracker = CloseGuard.getTracker(); 103 // Get the previous enabled state so that it can be restored afterwards. 104 boolean previousEnabled = CloseGuard.isEnabled(); 105 TestCloseGuardTracker tracker = new TestCloseGuardTracker(); 106 Throwable thrown = null; 107 try { 108 // Set the test tracker and enable close guard detection. 109 CloseGuard.setTracker(tracker); 110 CloseGuard.setEnabled(true); 111 base.evaluate(); 112 } catch (Throwable throwable) { 113 // Catch and remember the throwable so that it can be rethrown in the 114 // finally block. 115 thrown = throwable; 116 } finally { 117 // Restore the previous tracker and enabled state. 118 CloseGuard.setEnabled(previousEnabled); 119 CloseGuard.setTracker(previousTracker); 120 121 Collection<Throwable> allocationSites = 122 tracker.getAllocationSitesForUnreleasedResources(); 123 if (!allocationSites.isEmpty()) { 124 if (thrown == null) { 125 thrown = new IllegalStateException( 126 "Unreleased resources found in test"); 127 } 128 for (Throwable allocationSite : allocationSites) { 129 thrown.addSuppressed(allocationSite); 130 } 131 } 132 if (thrown != null) { 133 throw thrown; 134 } 135 } 136 } 137 }; 138 } 139 } 140 141 /** 142 * A tracker that keeps a record of the allocation sites for all resources allocated but not 143 * yet released. 144 * 145 * <p>It only tracks resources allocated for the test thread. 146 */ 147 private static class TestCloseGuardTracker implements CloseGuard.Tracker { 148 149 /** 150 * A set would be preferable but this is the closest that matches the concurrency 151 * requirements for the use case which prioritise speed of addition and removal over 152 * iteration and access. 153 */ 154 private final Set<Throwable> allocationSites = 155 Collections.newSetFromMap(new ConcurrentHashMap<>()); 156 157 private final Thread testThread = Thread.currentThread(); 158 159 @Override 160 public void open(Throwable allocationSite) { 161 if (Thread.currentThread() == testThread) { 162 allocationSites.add(allocationSite); 163 } 164 } 165 166 @Override 167 public void close(Throwable allocationSite) { 168 // Closing the resource twice could pass null into here. 169 if (allocationSite != null) { 170 allocationSites.remove(allocationSite); 171 } 172 } 173 174 /** 175 * Get the collection of allocation sites for any unreleased resources. 176 */ 177 Collection<Throwable> getAllocationSitesForUnreleasedResources() { 178 return new ArrayList<>(allocationSites); 179 } 180 } 181 182 private static final BiConsumer<Object, Integer> FINALIZER_CHECKER 183 = new BiConsumer<Object, Integer>() { 184 @Override 185 public void accept(Object resourceOwner, Integer expectedCount) { 186 finalizerChecker(resourceOwner, expectedCount); 187 } 188 }; 189 190 /** 191 * Get access to a {@link BiConsumer} that will determine how many unreleased resources the 192 * first parameter owns and throw a {@link AssertionError} if that does not match the 193 * expected number of resources specified by the second parameter. 194 * 195 * <p>This uses a {@link BiConsumer} as it is a standard interface that is available in all 196 * environments. That helps avoid the caller from having compile time dependencies on this 197 * class which will not be available on OpenJDK. 198 */ 199 public static BiConsumer<Object, Integer> getFinalizerChecker() { 200 return FINALIZER_CHECKER; 201 } 202 203 /** 204 * Checks that the supplied {@code resourceOwner} has overridden the {@link Object#finalize()} 205 * method and uses {@link CloseGuard#warnIfOpen()} correctly to detect when the resource is 206 * not released. 207 * 208 * @param resourceOwner the owner of the resource protected by {@link CloseGuard}. 209 * @param expectedCount the expected number of unreleased resources to be held by the owner. 210 * 211 */ 212 private static void finalizerChecker(Object resourceOwner, int expectedCount) { 213 Class<?> clazz = resourceOwner.getClass(); 214 Method finalizer = null; 215 while (clazz != null && clazz != Object.class) { 216 try { 217 finalizer = clazz.getDeclaredMethod("finalize"); 218 break; 219 } catch (NoSuchMethodException e) { 220 // Carry on up the class hierarchy. 221 clazz = clazz.getSuperclass(); 222 } 223 } 224 225 if (finalizer == null) { 226 // No finalizer method could be found. 227 throw new AssertionError("Class " + resourceOwner.getClass().getName() 228 + " does not have a finalize() method"); 229 } 230 231 // Make the method accessible. 232 finalizer.setAccessible(true); 233 234 CloseGuard.Reporter oldReporter = CloseGuard.getReporter(); 235 try { 236 CollectingReporter reporter = new CollectingReporter(); 237 CloseGuard.setReporter(reporter); 238 239 // Invoke the finalizer to cause it to get CloseGuard to report a problem if it has 240 // not yet been closed. 241 try { 242 finalizer.invoke(resourceOwner); 243 } catch (ReflectiveOperationException e) { 244 throw new AssertionError( 245 "Could not invoke the finalizer() method on " + resourceOwner, e); 246 } 247 248 reporter.assertUnreleasedResources(expectedCount); 249 } finally { 250 CloseGuard.setReporter(oldReporter); 251 } 252 } 253 254 /** 255 * A {@link CloseGuard.Reporter} that collects any reports about unreleased resources. 256 */ 257 private static class CollectingReporter implements CloseGuard.Reporter { 258 259 private final Thread callingThread = Thread.currentThread(); 260 261 private final List<Throwable> unreleasedResourceAllocationSites = new ArrayList<>(); 262 263 @Override 264 public void report(String message, Throwable allocationSite) { 265 // Only care about resources that are not reported on this thread. 266 if (callingThread == Thread.currentThread()) { 267 unreleasedResourceAllocationSites.add(allocationSite); 268 } 269 } 270 271 void assertUnreleasedResources(int expectedCount) { 272 int unreleasedResourceCount = unreleasedResourceAllocationSites.size(); 273 if (unreleasedResourceCount == expectedCount) { 274 return; 275 } 276 277 AssertionError error = new AssertionError( 278 "Expected " + expectedCount + " unreleased resources, found " 279 + unreleasedResourceCount + "; see suppressed exceptions for details"); 280 for (Throwable unreleasedResourceAllocationSite : unreleasedResourceAllocationSites) { 281 error.addSuppressed(unreleasedResourceAllocationSite); 282 } 283 throw error; 284 } 285 } 286} 287