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