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