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