Home | History | Annotate | Download | only in system
      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      */
     84     public static TestRule getRule() {
     85         return CLOSE_GUARD_RULE;
     86     }
     87 
     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          */
     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 
    265         @Override
    266         public void report(String message, Throwable allocationSite) {
    267             // Only care about resources that are not reported on this thread.
    268             if (callingThread == Thread.currentThread()) {
    269                 unreleasedResourceAllocationSites.add(allocationSite);
    270             }
    271         }
    272 
    273         void assertUnreleasedResources(int expectedCount) {
    274             int unreleasedResourceCount = unreleasedResourceAllocationSites.size();
    275             if (unreleasedResourceCount == expectedCount) {
    276                 return;
    277             }
    278 
    279             AssertionError error = new AssertionError(
    280                     "Expected " + expectedCount + " unreleased resources, found "
    281                             + unreleasedResourceCount + "; see suppressed exceptions for details");
    282             for (Throwable unreleasedResourceAllocationSite : unreleasedResourceAllocationSites) {
    283                 error.addSuppressed(unreleasedResourceAllocationSite);
    284             }
    285             throw error;
    286         }
    287     }
    288 }
    289