blob: 7871795b69047a69bfa4831ffdda06a306789d16 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dalvik.system;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
/**
* Provides support for testing classes that use {@link CloseGuard} in order to detect resource
* leakages.
*
* <p>This class should not be used directly by tests as that will prevent them from being
* compilable and testable on OpenJDK platform. Instead they should use
* {@code libcore.junit.util.ResourceLeakageDetector} which accesses the capabilities of this using
* reflection and if it cannot find it (because it is running on OpenJDK) then it will just skip
* leakage detection.
*
* <p>This provides two entry points that are accessed reflectively:
* <ul>
* <li>
* <p>The {@link #getRule()} method. This returns a {@link TestRule} that will fail a test if it
* detects any resources that were allocated during the test but were not released.
*
* <p>This only tracks resources that were allocated on the test thread, although it does not care
* what thread they were released on. This avoids flaky false positives where a background thread
* allocates a resource during a test but releases it after the test.
*
* <p>It is still possible to have a false positive in the case where the test causes a caching
* mechanism to open a resource and hold it open past the end of the test. In that case if there is
* no way to clear the cached data then it should be relatively simple to move the code that invokes
* the caching mechanism to outside the scope of this rule. i.e.
*
* <pre>{@code
* @Rule
* public final TestRule ruleChain = org.junit.rules.RuleChain
* .outerRule(new ...invoke caching mechanism...)
* .around(CloseGuardSupport.getRule());
* }</pre>
* </li>
* <li>
* <p>The {@link #getFinalizerChecker()} method. This returns a {@link BiConsumer} that takes an
* object that owns resources and an expected number of unreleased resources. It will call the
* {@link Object#finalize()} method on the object using reflection and throw an
* {@link AssertionError} if the number of reported unreleased resources does not match the
* expected number.
* </li>
* </ul>
*/
public class CloseGuardSupport {
private static final TestRule CLOSE_GUARD_RULE = new FailTestWhenResourcesNotClosedRule();
/**
* Get a {@link TestRule} that will detect when resources that use the {@link CloseGuard}
* mechanism are not cleaned up properly by a test.
*
* <p>If the {@link CloseGuard} mechanism is not supported, e.g. on OpenJDK, then the returned
* rule does nothing.
*/
public static TestRule getRule() {
return CLOSE_GUARD_RULE;
}
private CloseGuardSupport() {
}
/**
* Fails a test when resources are not cleaned up properly.
*/
private static class FailTestWhenResourcesNotClosedRule implements TestRule {
/**
* Returns a {@link Statement} that will fail the test if it ends with unreleased resources.
* @param base the test to be run.
*/
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
// Get the previous tracker so that it can be restored afterwards.
CloseGuard.Tracker previousTracker = CloseGuard.getTracker();
// Get the previous enabled state so that it can be restored afterwards.
boolean previousEnabled = CloseGuard.isEnabled();
TestCloseGuardTracker tracker = new TestCloseGuardTracker();
Throwable thrown = null;
try {
// Set the test tracker and enable close guard detection.
CloseGuard.setTracker(tracker);
CloseGuard.setEnabled(true);
base.evaluate();
} catch (Throwable throwable) {
// Catch and remember the throwable so that it can be rethrown in the
// finally block.
thrown = throwable;
} finally {
// Restore the previous tracker and enabled state.
CloseGuard.setEnabled(previousEnabled);
CloseGuard.setTracker(previousTracker);
Collection<Throwable> allocationSites =
tracker.getAllocationSitesForUnreleasedResources();
if (!allocationSites.isEmpty()) {
if (thrown == null) {
thrown = new IllegalStateException(
"Unreleased resources found in test");
}
for (Throwable allocationSite : allocationSites) {
thrown.addSuppressed(allocationSite);
}
}
if (thrown != null) {
throw thrown;
}
}
}
};
}
}
/**
* A tracker that keeps a record of the allocation sites for all resources allocated but not
* yet released.
*
* <p>It only tracks resources allocated for the test thread.
*/
private static class TestCloseGuardTracker implements CloseGuard.Tracker {
/**
* A set would be preferable but this is the closest that matches the concurrency
* requirements for the use case which prioritise speed of addition and removal over
* iteration and access.
*/
private final Set<Throwable> allocationSites =
Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Thread testThread = Thread.currentThread();
@Override
public void open(Throwable allocationSite) {
if (Thread.currentThread() == testThread) {
allocationSites.add(allocationSite);
}
}
@Override
public void close(Throwable allocationSite) {
// Closing the resource twice could pass null into here.
if (allocationSite != null) {
allocationSites.remove(allocationSite);
}
}
/**
* Get the collection of allocation sites for any unreleased resources.
*/
Collection<Throwable> getAllocationSitesForUnreleasedResources() {
return new ArrayList<>(allocationSites);
}
}
private static final BiConsumer<Object, Integer> FINALIZER_CHECKER
= new BiConsumer<Object, Integer>() {
@Override
public void accept(Object resourceOwner, Integer expectedCount) {
finalizerChecker(resourceOwner, expectedCount);
}
};
/**
* Get access to a {@link BiConsumer} that will determine how many unreleased resources the
* first parameter owns and throw a {@link AssertionError} if that does not match the
* expected number of resources specified by the second parameter.
*
* <p>This uses a {@link BiConsumer} as it is a standard interface that is available in all
* environments. That helps avoid the caller from having compile time dependencies on this
* class which will not be available on OpenJDK.
*/
public static BiConsumer<Object, Integer> getFinalizerChecker() {
return FINALIZER_CHECKER;
}
/**
* Checks that the supplied {@code resourceOwner} has overridden the {@link Object#finalize()}
* method and uses {@link CloseGuard#warnIfOpen()} correctly to detect when the resource is
* not released.
*
* @param resourceOwner the owner of the resource protected by {@link CloseGuard}.
* @param expectedCount the expected number of unreleased resources to be held by the owner.
*
*/
private static void finalizerChecker(Object resourceOwner, int expectedCount) {
Class<?> clazz = resourceOwner.getClass();
Method finalizer = null;
while (clazz != null && clazz != Object.class) {
try {
finalizer = clazz.getDeclaredMethod("finalize");
break;
} catch (NoSuchMethodException e) {
// Carry on up the class hierarchy.
clazz = clazz.getSuperclass();
}
}
if (finalizer == null) {
// No finalizer method could be found.
throw new AssertionError("Class " + resourceOwner.getClass().getName()
+ " does not have a finalize() method");
}
// Make the method accessible.
finalizer.setAccessible(true);
CloseGuard.Reporter oldReporter = CloseGuard.getReporter();
try {
CollectingReporter reporter = new CollectingReporter();
CloseGuard.setReporter(reporter);
// Invoke the finalizer to cause it to get CloseGuard to report a problem if it has
// not yet been closed.
try {
finalizer.invoke(resourceOwner);
} catch (ReflectiveOperationException e) {
throw new AssertionError(
"Could not invoke the finalizer() method on " + resourceOwner, e);
}
reporter.assertUnreleasedResources(expectedCount);
} finally {
CloseGuard.setReporter(oldReporter);
}
}
/**
* A {@link CloseGuard.Reporter} that collects any reports about unreleased resources.
*/
private static class CollectingReporter implements CloseGuard.Reporter {
private final Thread callingThread = Thread.currentThread();
private final List<Throwable> unreleasedResourceAllocationSites = new ArrayList<>();
@Override
public void report(String message, Throwable allocationSite) {
// Only care about resources that are not reported on this thread.
if (callingThread == Thread.currentThread()) {
unreleasedResourceAllocationSites.add(allocationSite);
}
}
void assertUnreleasedResources(int expectedCount) {
int unreleasedResourceCount = unreleasedResourceAllocationSites.size();
if (unreleasedResourceCount == expectedCount) {
return;
}
AssertionError error = new AssertionError(
"Expected " + expectedCount + " unreleased resources, found "
+ unreleasedResourceCount + "; see suppressed exceptions for details");
for (Throwable unreleasedResourceAllocationSite : unreleasedResourceAllocationSites) {
error.addSuppressed(unreleasedResourceAllocationSite);
}
throw error;
}
}
}