From fe0ee8ef8870338ad67ebfb6b62785e0cbdb325b Mon Sep 17 00:00:00 2001 From: Paul Duffin Date: Fri, 12 Aug 2016 11:32:44 +0100 Subject: [PATCH] Improve resource leakage detection testing This change makes various improvements to the support for testing resource leakage detection. Adds CloseGuard.Tracker to allow tests to track the lifecycle of CloseGuard allocation sites directly. Adds CloseGuardSupport to use CloseGuard.Tracker to provide: * A BiConsumer that can be used to help check that objects which own resources protected by CloseGuard correctly detect resource leakage. * A TestRule that allows tests to check that system code does not leak resources, especially under error conditions. Adds a new ResourceLeakageDetector that uses reflection to access CloseGuardSupport. This can safely be used in code that needs to compile and run on OpenJDK. Adds TestCaseWithRules to allow the TestRule above to be used with JUnit 3 style tests without converting the whole test to JUnit 4 style. Changed libcore.java.lang.ProcessBuilderTest to use TestCase as ProcessBuilder does not have any CloseGuard protected resources. That allows AbstractResourceLeakageDetectorTestCase to be deleted. Changed RandomAccessFileTest to use TestCaseWithRules and ResourceLeakageDetector. Fixed issues that it highlighted, fixed testRandomAccessFileHasCleanupFinalizer test and removed it from expectations/knownfailures.txt. Adds core-test-rules/-hostdex targets to encapsulate TestRule and related classes that can be shared with other projects. Removes the following now unused classes: * CloseGuardMonitor * AbstractResourceLeakageDetectorTest * The old ResourceLeakageDetector and associated test. Bug: 31542223 Test: Ran tests on both Vogar and CTS Change-Id: I8f802b52fdbeac0a30f339a9ceca5d2eaaafd180 --- JavaLibrary.mk | 31 +- .../main/java/dalvik/system/CloseGuard.java | 74 ++++- .../java/dalvik/system/CloseGuardMonitor.java | 119 -------- .../java/dalvik/system/CloseGuardTest.java | 173 +++++++++++ .../java/dalvik/system/CloseGuardSupport.java | 286 ++++++++++++++++++ .../dalvik/system/CloseGuardSupportTest.java | 182 +++++++++++ .../libcore/java/lang/ProcessBuilderTest.java | 5 +- ...stractResourceLeakageDetectorTestCase.java | 44 --- .../java/util/ResourceLeakageDetector.java | 90 ------ .../util/ResourceLeakageDetectorTest.java | 80 ----- .../junit/junit3/TestCaseWithRules.java | 112 +++++++ .../junit/util/ResourceLeakageDetector.java | 150 +++++++++ 12 files changed, 1004 insertions(+), 342 deletions(-) delete mode 100644 dalvik/src/test/java/dalvik/system/CloseGuardMonitor.java create mode 100644 dalvik/src/test/java/dalvik/system/CloseGuardTest.java create mode 100644 dalvik/test-rules/src/main/java/dalvik/system/CloseGuardSupport.java create mode 100644 dalvik/test-rules/src/test/java/dalvik/system/CloseGuardSupportTest.java delete mode 100644 luni/src/test/java/libcore/java/util/AbstractResourceLeakageDetectorTestCase.java delete mode 100644 luni/src/test/java/libcore/java/util/ResourceLeakageDetector.java delete mode 100644 luni/src/test/java/libcore/java/util/ResourceLeakageDetectorTest.java create mode 100644 test-rules/src/main/java/libcore/junit/junit3/TestCaseWithRules.java create mode 100644 test-rules/src/main/java/libcore/junit/util/ResourceLeakageDetector.java diff --git a/JavaLibrary.mk b/JavaLibrary.mk index ef7703ef9..ffca748a5 100644 --- a/JavaLibrary.mk +++ b/JavaLibrary.mk @@ -53,7 +53,7 @@ core_resource_dirs := \ luni/src/main/java \ ojluni/src/main/resources/ test_resource_dirs := $(call all-core-resource-dirs,test) -test_src_files := $(call all-test-java-files-under,dalvik dom harmony-tests json luni xml) +test_src_files := $(call all-test-java-files-under,dalvik dalvik/test-rules dom harmony-tests json luni xml) ojtest_src_files := $(call all-test-java-files-under,ojluni) ifeq ($(EMMA_INSTRUMENT),true) @@ -166,6 +166,24 @@ LOCAL_REQUIRED_MODULES := tzdata LOCAL_CORE_LIBRARY := true include $(BUILD_JAVA_LIBRARY) +# Build libcore test rules for target +include $(CLEAR_VARS) +LOCAL_SRC_FILES := $(call all-java-files-under, dalvik/test-rules/src/main test-rules/src/main) +LOCAL_NO_STANDARD_LIBRARIES := true +LOCAL_MODULE := core-test-rules +LOCAL_JAVA_LIBRARIES := core-all core-junit +LOCAL_STATIC_JAVA_LIBRARIES := junit4-target +include $(BUILD_STATIC_JAVA_LIBRARY) + +# Build libcore test rules for host +include $(CLEAR_VARS) +LOCAL_SRC_FILES := $(call all-java-files-under, dalvik/test-rules/src/main test-rules/src/main) +LOCAL_NO_STANDARD_LIBRARIES := true +LOCAL_MODULE := core-test-rules-hostdex +LOCAL_JAVA_LIBRARIES := core-oj-hostdex core-libart-hostdex core-junit-hostdex +LOCAL_STATIC_JAVA_LIBRARIES := junit4-target-hostdex +include $(BUILD_HOST_DALVIK_JAVA_LIBRARY) + include $(CLEAR_VARS) LOCAL_SRC_FILES := $(non_openjdk_java_files) $(android_icu4j_src_files) LOCAL_JAVA_RESOURCE_DIRS := $(android_icu4j_resource_dirs) @@ -189,7 +207,12 @@ LOCAL_SRC_FILES := $(test_src_files) LOCAL_JAVA_RESOURCE_DIRS := $(test_resource_dirs) LOCAL_NO_STANDARD_LIBRARIES := true LOCAL_JAVA_LIBRARIES := core-oj core-libart okhttp core-junit junit4-target bouncycastle mockito-target -LOCAL_STATIC_JAVA_LIBRARIES := core-tests-support sqlite-jdbc mockwebserver nist-pkix-tests +LOCAL_STATIC_JAVA_LIBRARIES := \ + core-test-rules \ + core-tests-support \ + mockwebserver \ + nist-pkix-tests \ + sqlite-jdbc LOCAL_JAVACFLAGS := $(local_javac_flags) LOCAL_JAVA_LANGUAGE_VERSION := 1.8 LOCAL_MODULE := core-tests @@ -329,14 +352,14 @@ LOCAL_JAVA_LIBRARIES := core-all-hostdex LOCAL_CORE_LIBRARY := true include $(BUILD_HOST_DALVIK_JAVA_LIBRARY) -# Make the core-tests library. +# Make the core-tests-hostdex library. ifeq ($(LIBCORE_SKIP_TESTS),) include $(CLEAR_VARS) LOCAL_SRC_FILES := $(test_src_files) LOCAL_JAVA_RESOURCE_DIRS := $(test_resource_dirs) LOCAL_NO_STANDARD_LIBRARIES := true LOCAL_JAVA_LIBRARIES := core-oj-hostdex core-libart-hostdex okhttp-hostdex bouncycastle-hostdex core-junit-hostdex junit4-target-hostdex core-tests-support-hostdex mockito-api-hostdex - LOCAL_STATIC_JAVA_LIBRARIES := sqlite-jdbc-host mockwebserver-host nist-pkix-tests-host + LOCAL_STATIC_JAVA_LIBRARIES := sqlite-jdbc-host mockwebserver-host nist-pkix-tests-host core-test-rules-hostdex LOCAL_JAVACFLAGS := $(local_javac_flags) LOCAL_MODULE_TAGS := optional LOCAL_JAVA_LANGUAGE_VERSION := 1.8 diff --git a/dalvik/src/main/java/dalvik/system/CloseGuard.java b/dalvik/src/main/java/dalvik/system/CloseGuard.java index a45ffa10d..e718ee785 100644 --- a/dalvik/src/main/java/dalvik/system/CloseGuard.java +++ b/dalvik/src/main/java/dalvik/system/CloseGuard.java @@ -117,6 +117,16 @@ public final class CloseGuard { */ private static volatile Reporter REPORTER = new DefaultReporter(); + /** + * The default {@link Tracker}. + */ + private static final DefaultTracker DEFAULT_TRACKER = new DefaultTracker(); + + /** + * Hook for customizing how CloseGuard issues are tracked. + */ + private static volatile Tracker currentTracker = DEFAULT_TRACKER; + /** * Returns a CloseGuard instance. If CloseGuard is enabled, {@code * #open(String)} can be used to set up the instance to warn on @@ -138,6 +148,13 @@ public static void setEnabled(boolean enabled) { ENABLED = enabled; } + /** + * True if CloseGuard mechanism is enabled. + */ + public static boolean isEnabled() { + return ENABLED; + } + /** * Used to replace default Reporter used to warn of CloseGuard * violations. Must be non-null. @@ -156,6 +173,32 @@ public static Reporter getReporter() { return REPORTER; } + /** + * Sets the {@link Tracker} that is notified when resources are allocated and released. + * + *

This is only intended for use by {@code dalvik.system.CloseGuardSupport} class and so + * MUST NOT be used for any other purposes. + * + * @throws NullPointerException if tracker is null + */ + public static void setTracker(Tracker tracker) { + if (tracker == null) { + throw new NullPointerException("tracker == null"); + } + currentTracker = tracker; + } + + /** + * Returns {@link #setTracker(Tracker) last Tracker that was set}, or otherwise a default + * Tracker that does nothing. + * + *

This is only intended for use by {@code dalvik.system.CloseGuardSupport} class and so + * MUST NOT be used for any other purposes. + */ + public static Tracker getTracker() { + return currentTracker; + } + private CloseGuard() {} /** @@ -178,6 +221,7 @@ public void open(String closer) { } String message = "Explicit termination method '" + closer + "' not called"; allocationSite = new Throwable(message); + currentTracker.open(allocationSite); } private Throwable allocationSite; @@ -187,6 +231,7 @@ public void open(String closer) { * finalization. */ public void close() { + currentTracker.close(allocationSite); allocationSite = null; } @@ -208,11 +253,36 @@ public void warnIfOpen() { REPORTER.report(message, allocationSite); } + /** + * Interface to allow customization of tracking behaviour. + * + *

This is only intended for use by {@code dalvik.system.CloseGuardSupport} class and so + * MUST NOT be used for any other purposes. + */ + public interface Tracker { + void open(Throwable allocationSite); + void close(Throwable allocationSite); + } + + /** + * Default tracker which does nothing special and simply leaves it up to the GC to detect a + * leak. + */ + private static final class DefaultTracker implements Tracker { + @Override + public void open(Throwable allocationSite) { + } + + @Override + public void close(Throwable allocationSite) { + } + } + /** * Interface to allow customization of reporting behavior. */ - public static interface Reporter { - public void report (String message, Throwable allocationSite); + public interface Reporter { + void report (String message, Throwable allocationSite); } /** diff --git a/dalvik/src/test/java/dalvik/system/CloseGuardMonitor.java b/dalvik/src/test/java/dalvik/system/CloseGuardMonitor.java deleted file mode 100644 index b5bf380e2..000000000 --- a/dalvik/src/test/java/dalvik/system/CloseGuardMonitor.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2014 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 dalvik.system.CloseGuard.Reporter; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.ref.WeakReference; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * Provides support for detecting issues found by {@link CloseGuard} from within tests. - * - *

This is a best effort as it relies on both {@link CloseGuard} being enabled and being able to - * force a GC and finalization, none of which are directly controllable by this. - * - *

This is loaded using reflection by the AbstractResourceLeakageDetectorTestCase class as that - * class needs to run on the reference implementation which does not have this class. It implements - * {@link Runnable} because that is simpler than trying to manage a specialized interface. - * - * @hide - */ -public class CloseGuardMonitor implements Runnable { - /** - * The {@link Reporter} instance used to receive warnings from {@link CloseGuard}. - */ - private final Reporter closeGuardReporter; - - /** - * The list of allocation sites that {@link CloseGuard} has reported as not being released. - * - *

Is thread safe as this will be called during finalization and so there are no guarantees - * as to whether it will be called concurrently or not. - */ - private final List closeGuardAllocationSites = new CopyOnWriteArrayList<>(); - - /** - * Default constructor required for reflection. - */ - public CloseGuardMonitor() { - System.logI("Creating CloseGuard monitor"); - - // Save current reporter. - closeGuardReporter = CloseGuard.getReporter(); - - // Override the reporter with our own which collates the allocation sites. - CloseGuard.setReporter(new Reporter() { - @Override - public void report(String message, Throwable allocationSite) { - // Ignore message as it's always the same. - closeGuardAllocationSites.add(allocationSite); - } - }); - } - - /** - * Check to see whether any resources monitored by {@link CloseGuard} were not released before - * they were garbage collected. - */ - @Override - public void run() { - // Create a weak reference to an object so that we can detect when it is garbage collected. - WeakReference reference = new WeakReference<>(new Object()); - - try { - // 'Force' a GC and finalize to cause CloseGuards to report warnings. Doesn't loop - // forever as there are no guarantees that the following code does anything at all so - // don't want a potential infinite loop. - Runtime runtime = Runtime.getRuntime(); - for (int i = 0; i < 20; ++i) { - runtime.gc(); - System.runFinalization(); - try { - Thread.sleep(1); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - - // Check to see if the weak reference has been garbage collected. - if (reference.get() == null) { - System.logI("Sentry object has been freed so assuming CloseGuards have reported" - + " any resource leakages"); - break; - } - } - } finally { - // Restore the reporter. - CloseGuard.setReporter(closeGuardReporter); - } - - if (!closeGuardAllocationSites.isEmpty()) { - StringWriter writer = new StringWriter(); - PrintWriter printWriter = new PrintWriter(writer); - int i = 0; - for (Throwable allocationSite : closeGuardAllocationSites) { - printWriter.print(++i); - printWriter.print(") "); - allocationSite.printStackTrace(printWriter); - printWriter.println(" --------------------------------"); - } - throw new AssertionError("Potential resource leakage detected:\n" + writer); - } - } -} diff --git a/dalvik/src/test/java/dalvik/system/CloseGuardTest.java b/dalvik/src/test/java/dalvik/system/CloseGuardTest.java new file mode 100644 index 000000000..a1d1f42b6 --- /dev/null +++ b/dalvik/src/test/java/dalvik/system/CloseGuardTest.java @@ -0,0 +1,173 @@ +/* + * 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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Tests {@link CloseGuard}. + */ +public class CloseGuardTest { + + /** + * Resets the {@link CloseGuard#ENABLED} state back to the value it had when the test started. + */ + @Rule + public TestRule rule = this::preserveEnabledState; + + private Statement preserveEnabledState(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + boolean oldEnabledState = CloseGuard.isEnabled(); + try { + base.evaluate(); + } finally { + CloseGuard.setEnabled(oldEnabledState); + } + } + }; + } + + @Test + public void testEnabled_NotOpen() throws Throwable { + CloseGuard.setEnabled(true); + ResourceOwner owner = new ResourceOwner(); + assertUnreleasedResources(owner, 0); + } + + @Test + public void testEnabled_OpenNotClosed() throws Throwable { + CloseGuard.setEnabled(true); + ResourceOwner owner = new ResourceOwner(); + owner.open(); + assertUnreleasedResources(owner, 1); + } + + @Test + public void testEnabled_OpenThenClosed() throws Throwable { + CloseGuard.setEnabled(true); + ResourceOwner owner = new ResourceOwner(); + owner.open(); + owner.close(); + assertUnreleasedResources(owner, 0); + } + + @Test + public void testEnabledWhenCreated_DisabledWhenOpen() throws Throwable { + CloseGuard.setEnabled(true); + ResourceOwner owner = new ResourceOwner(); + CloseGuard.setEnabled(false); + owner.open(); + + // Although the resource was not released it should not report it because CloseGuard was + // not enabled when the CloseGuard was opened. + assertUnreleasedResources(owner, 0); + } + + @Test + public void testEnabledWhenOpened_DisabledWhenFinalized() throws Throwable { + CloseGuard.setEnabled(true); + ResourceOwner owner = new ResourceOwner(); + owner.open(); + CloseGuard.setEnabled(false); + + // Although the resource was not released it should not report it because CloseGuard was + // not enabled when the CloseGuard was finalized. + assertUnreleasedResources(owner, 0); + } + + @Test + public void testDisabled_NotOpen() throws Throwable { + CloseGuard.setEnabled(false); + ResourceOwner owner = new ResourceOwner(); + assertUnreleasedResources(owner, 0); + } + + @Test + public void testDisabled_OpenNotClosed() throws Throwable { + CloseGuard.setEnabled(false); + ResourceOwner owner = new ResourceOwner(); + owner.open(); + assertUnreleasedResources(owner, 0); + } + + @Test + public void testDisabled_OpenThenClosed() throws Throwable { + CloseGuard.setEnabled(false); + ResourceOwner owner = new ResourceOwner(); + owner.open(); + owner.close(); + assertUnreleasedResources(owner, 0); + } + + @Test + public void testDisabledWhenCreated_EnabledWhenOpen() throws Throwable { + CloseGuard.setEnabled(false); + ResourceOwner owner = new ResourceOwner(); + CloseGuard.setEnabled(true); + owner.open(); + + // Although the resource was not released it should not report it because CloseGuard was + // not enabled when the CloseGuard was created. + assertUnreleasedResources(owner, 0); + } + + private void assertUnreleasedResources(ResourceOwner owner, int expectedCount) + throws Throwable { + try { + CloseGuardSupport.getFinalizerChecker().accept(owner, expectedCount); + } finally { + // Close the resource so that CloseGuard does not generate a warning for real when it + // is actually finalized. + owner.close(); + } + } + + /** + * A test user of {@link CloseGuard}. + */ + private static class ResourceOwner { + + private final CloseGuard closeGuard; + + ResourceOwner() { + closeGuard = CloseGuard.get(); + } + + public void open() { + closeGuard.open("close"); + } + + public void close() { + closeGuard.close(); + } + + /** + * Make finalize public so that it can be tested directly without relying on garbage + * collection to trigger it. + */ + @Override + public void finalize() throws Throwable { + closeGuard.warnIfOpen(); + super.finalize(); + } + } +} diff --git a/dalvik/test-rules/src/main/java/dalvik/system/CloseGuardSupport.java b/dalvik/test-rules/src/main/java/dalvik/system/CloseGuardSupport.java new file mode 100644 index 000000000..7871795b6 --- /dev/null +++ b/dalvik/test-rules/src/main/java/dalvik/system/CloseGuardSupport.java @@ -0,0 +1,286 @@ +/* + * 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. + * + *

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. + * + *

This provides two entry points that are accessed reflectively: + *

    + *
  • + *

    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. + * + *

    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. + * + *

    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. + * + *

    {@code
    + *     @Rule
    + *     public final TestRule ruleChain = org.junit.rules.RuleChain
    + *         .outerRule(new ...invoke caching mechanism...)
    + *         .around(CloseGuardSupport.getRule());
    + * }
    + *
  • + *
  • + *

    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. + *

  • + *
+ */ +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. + * + *

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 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. + * + *

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 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 getAllocationSitesForUnreleasedResources() { + return new ArrayList<>(allocationSites); + } + } + + private static final BiConsumer FINALIZER_CHECKER + = new BiConsumer() { + @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. + * + *

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 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 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; + } + } +} diff --git a/dalvik/test-rules/src/test/java/dalvik/system/CloseGuardSupportTest.java b/dalvik/test-rules/src/test/java/dalvik/system/CloseGuardSupportTest.java new file mode 100644 index 000000000..fe05710b2 --- /dev/null +++ b/dalvik/test-rules/src/test/java/dalvik/system/CloseGuardSupportTest.java @@ -0,0 +1,182 @@ +/* + * 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.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.JUnitCore; +import org.junit.runner.RunWith; +import org.junit.runner.notification.Failure; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(JUnit4.class) +public class CloseGuardSupportTest { + + @Test + public void testDoesReleaseResource() { + List failures = JUnitCore.runClasses(DoesReleaseResource.class).getFailures(); + assertEquals(Collections.emptyList(), failures); + } + + public static class DoesReleaseResource { + @Rule public TestRule rule = CloseGuardSupport.getRule(); + @Test public void test() { + CloseGuard closeGuard = CloseGuard.get(); + closeGuard.open("test resource"); + closeGuard.close(); + } + } + + @Test + public void testDoesReleaseResourceTwice() { + List failures = JUnitCore.runClasses(DoesReleaseResourceTwice.class).getFailures(); + assertEquals(Collections.emptyList(), failures); + } + + public static class DoesReleaseResourceTwice { + @Rule public TestRule rule = CloseGuardSupport.getRule(); + @Test public void test() { + CloseGuard closeGuard = CloseGuard.get(); + closeGuard.open("test resource"); + closeGuard.close(); + closeGuard.close(); + } + } + + @Test + public void testDoesNotReleaseResource() { + List failures = JUnitCore.runClasses(DoesNotReleaseResource.class).getFailures(); + assertEquals("Failure count", 1, failures.size()); + Failure failure = failures.get(0); + checkResourceNotReleased(failure, "Unreleased resources found in test"); + } + + public static class DoesNotReleaseResource { + @Rule public TestRule rule = CloseGuardSupport.getRule(); + @Test public void test() { + CloseGuard closeGuard = CloseGuard.get(); + closeGuard.open("test resource"); + } + } + + @Test + public void testDoesNotReleaseResourceDueToFailure() { + List failures = JUnitCore + .runClasses(DoesNotReleaseResourceDueToFailure.class) + .getFailures(); + assertEquals("Failure count", 1, failures.size()); + Failure failure = failures.get(0); + checkResourceNotReleased(failure, "failure"); + } + + public static class DoesNotReleaseResourceDueToFailure { + @Rule public TestRule rule = CloseGuardSupport.getRule(); + @Test public void test() { + CloseGuard closeGuard = CloseGuard.get(); + closeGuard.open("test resource"); + fail("failure"); + } + } + + @Test + public void testResourceOwnerDoesNotOverrideFinalize() { + List failures = JUnitCore + .runClasses(ResourceOwnerDoesNotOverrideFinalize.class) + .getFailures(); + assertEquals("Failure count", 1, failures.size()); + Failure failure = failures.get(0); + assertEquals("Class java.lang.String does not have a finalize() method", + failure.getMessage()); + } + + public static class ResourceOwnerDoesNotOverrideFinalize { + @Rule public TestRule rule = CloseGuardSupport.getRule(); + @Test + public void test() { + CloseGuardSupport.getFinalizerChecker().accept("not resource owner", 0); + } + } + + @Test + public void testResourceOwnerOverridesFinalizeButDoesNotReportLeak() { + List failures = JUnitCore + .runClasses(ResourceOwnerOverridesFinalizeButDoesNotReportLeak.class) + .getFailures(); + assertEquals("Failure count", 1, failures.size()); + Failure failure = failures.get(0); + assertEquals("Expected 1 unreleased resources, found 0;" + + " see suppressed exceptions for details", + failure.getMessage()); + } + + public static class ResourceOwnerOverridesFinalizeButDoesNotReportLeak { + @Rule public TestRule rule = CloseGuardSupport.getRule(); + @Test + public void test() { + CloseGuardSupport.getFinalizerChecker().accept(new Object() { + @Override + protected void finalize() throws Throwable { + super.finalize(); + } + }, 1); + } + } + + @Test + public void testResourceOwnerOverridesFinalizeAndReportsLeak() { + List failures = JUnitCore + .runClasses(ResourceOwnerOverridesFinalizeAndReportsLeak.class) + .getFailures(); + assertEquals("Failure count", 1, failures.size()); + Failure failure = failures.get(0); + checkResourceNotReleased(failure, "Unreleased resources found in test"); + } + + public static class ResourceOwnerOverridesFinalizeAndReportsLeak { + @Rule public TestRule rule = CloseGuardSupport.getRule(); + @Test + public void test() { + CloseGuardSupport.getFinalizerChecker().accept(new Object() { + private CloseGuard guard = CloseGuard.get(); + { + guard.open("test resource"); + } + @Override + protected void finalize() throws Throwable { + guard.warnIfOpen(); + super.finalize(); + } + }, 1); + } + } + + private void checkResourceNotReleased(Failure failure, String expectedMessage) { + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + Throwable exception = failure.getException(); + assertEquals(expectedMessage, exception.getMessage()); + Throwable[] suppressed = exception.getSuppressed(); + assertEquals("Suppressed count", 1, suppressed.length); + exception = suppressed[0]; + assertEquals("Explicit termination method 'test resource' not called", + exception.getMessage()); + } +} diff --git a/luni/src/test/java/libcore/java/lang/ProcessBuilderTest.java b/luni/src/test/java/libcore/java/lang/ProcessBuilderTest.java index bc4fe80f6..9254b8ddd 100644 --- a/luni/src/test/java/libcore/java/lang/ProcessBuilderTest.java +++ b/luni/src/test/java/libcore/java/lang/ProcessBuilderTest.java @@ -17,7 +17,6 @@ package libcore.java.lang; import android.system.Os; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; @@ -38,13 +37,13 @@ import java.util.concurrent.FutureTask; import java.util.regex.Matcher; import java.util.regex.Pattern; +import junit.framework.TestCase; import libcore.io.IoUtils; -import libcore.java.util.AbstractResourceLeakageDetectorTestCase; import static java.lang.ProcessBuilder.Redirect.INHERIT; import static java.lang.ProcessBuilder.Redirect.PIPE; -public class ProcessBuilderTest extends AbstractResourceLeakageDetectorTestCase { +public class ProcessBuilderTest extends TestCase { private static final String TAG = ProcessBuilderTest.class.getSimpleName(); private static String shell() { diff --git a/luni/src/test/java/libcore/java/util/AbstractResourceLeakageDetectorTestCase.java b/luni/src/test/java/libcore/java/util/AbstractResourceLeakageDetectorTestCase.java deleted file mode 100644 index 5ea67d396..000000000 --- a/luni/src/test/java/libcore/java/util/AbstractResourceLeakageDetectorTestCase.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2014 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 libcore.java.util; - -import junit.framework.TestCase; - -/** - * Ensures that resources used within a test are cleaned up; will detect problems with tests and - * also with runtime. - */ -public abstract class AbstractResourceLeakageDetectorTestCase extends TestCase { - /** - * The leakage detector. - */ - private ResourceLeakageDetector detector; - - @Override - protected void setUp() throws Exception { - detector = ResourceLeakageDetector.newDetector(); - } - - @Override - protected void tearDown() throws Exception { - // If available check for resource leakage. At this point it is impossible to determine - // whether the test has thrown an exception. If it has then the exception thrown by this - // could hide that test failure; it largely depends on the test runner. - if (detector != null) { - detector.checkForLeaks(); - } - } -} diff --git a/luni/src/test/java/libcore/java/util/ResourceLeakageDetector.java b/luni/src/test/java/libcore/java/util/ResourceLeakageDetector.java deleted file mode 100644 index 954665ad0..000000000 --- a/luni/src/test/java/libcore/java/util/ResourceLeakageDetector.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2014 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 libcore.java.util; - -/** - * Detects resource leakages for resources that are protected by CloseGuard mechanism. - * - *

If multiple instances of this are active at the same time, i.e. have been created but not yet - * had their {@link #checkForLeaks()} method called then while they will report all the leakages - * detected they may report the leakages caused by the code being tested by another detector. - * - *

The underlying CloseGuardMonitor is loaded using reflection to ensure that this will run, - * albeit doing nothing, on the reference implementation. - */ -public class ResourceLeakageDetector { - /** The class for the CloseGuardMonitor, null if not supported. */ - private static final Class CLOSE_GUARD_MONITOR_CLASS; - - static { - ClassLoader classLoader = ResourceLeakageDetector.class.getClassLoader(); - Class clazz; - try { - // Make sure that the CloseGuard class exists; this ensures that this is not running - // on a RI JVM. - classLoader.loadClass("dalvik.system.CloseGuard"); - - // Load the monitor class for later instantiation. - clazz = classLoader.loadClass("dalvik.system.CloseGuardMonitor"); - - } catch (ClassNotFoundException e) { - System.err.println("Resource leakage will not be detected; " - + "this is expected in the reference implementation"); - e.printStackTrace(System.err); - - // Ignore, probably running in reference implementation. - clazz = null; - } - - CLOSE_GUARD_MONITOR_CLASS = clazz; - } - - /** - * The underlying CloseGuardMonitor that will perform the post test checks for resource - * leakage. - */ - private Runnable postTestChecker; - - /** - * Create a new detector. - * - * @return The new {@link ResourceLeakageDetector}, its {@link #checkForLeaks()} method must be - * called otherwise it will not clean up properly after itself. - */ - public static ResourceLeakageDetector newDetector() - throws Exception { - return new ResourceLeakageDetector(); - } - - private ResourceLeakageDetector() - throws Exception { - if (CLOSE_GUARD_MONITOR_CLASS != null) { - postTestChecker = (Runnable) CLOSE_GUARD_MONITOR_CLASS.newInstance(); - } - } - - /** - * Detect any leaks that have arisen since this was created. - * - * @throws Exception If any leaks were detected. - */ - public void checkForLeaks() throws Exception { - // If available check for resource leakage. - if (postTestChecker != null) { - postTestChecker.run(); - } - } -} diff --git a/luni/src/test/java/libcore/java/util/ResourceLeakageDetectorTest.java b/luni/src/test/java/libcore/java/util/ResourceLeakageDetectorTest.java deleted file mode 100644 index 5509f7383..000000000 --- a/luni/src/test/java/libcore/java/util/ResourceLeakageDetectorTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2014 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 libcore.java.util; - -import dalvik.system.CloseGuard; -import junit.framework.TestCase; - -/** - * Test for {@link ResourceLeakageDetector} - */ -public class ResourceLeakageDetectorTest extends TestCase { - /** - * This test will not work on RI as it does not support the CloseGuard or similar - * mechanism. - */ - // TODO(paulduffin): b/31542223 - Work out why this is failing in CTS, fix and reenable. - public void notestDetectsUnclosedCloseGuard() throws Exception { - ResourceLeakageDetector detector = ResourceLeakageDetector.newDetector(); - try { - CloseGuard closeGuard = createCloseGuard(); - closeGuard.open("open"); - } finally { - boolean leaksDetected = true; - try { - System.logI("Checking for leaks"); - detector.checkForLeaks(); - leaksDetected = false; - } catch (AssertionError expected) { - // The leak detector should throw this error. - } - - if (!leaksDetected) { - fail("Did not detect any leaks"); - } - } - } - - public void testIgnoresClosedCloseGuard() throws Exception { - ResourceLeakageDetector detector = ResourceLeakageDetector.newDetector(); - try { - CloseGuard closeGuard = createCloseGuard(); - closeGuard.open("open"); - closeGuard.close(); - } finally { - detector.checkForLeaks(); - } - } - - /** - * Private method to ensure that the CloseGuard object is garbage collected. - */ - private CloseGuard createCloseGuard() { - final CloseGuard closeGuard = CloseGuard.get(); - new Object() { - @Override - protected void finalize() throws Throwable { - try { - closeGuard.warnIfOpen(); - } finally { - super.finalize(); - } - } - }; - - return closeGuard; - } -} diff --git a/test-rules/src/main/java/libcore/junit/junit3/TestCaseWithRules.java b/test-rules/src/main/java/libcore/junit/junit3/TestCaseWithRules.java new file mode 100644 index 000000000..b5acc824b --- /dev/null +++ b/test-rules/src/main/java/libcore/junit/junit3/TestCaseWithRules.java @@ -0,0 +1,112 @@ +/* + * 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 libcore.junit.junit3; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import junit.framework.TestCase; +import org.junit.Rule; +import org.junit.rules.MethodRule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.MultipleFailureException; +import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; + +import static org.junit.internal.runners.rules.RuleFieldValidator.RULE_VALIDATOR; + +/** + * A {@link TestCase} that supports the @Rule annotation from JUnit 4. + * + *

It supports both {@link TestRule} and {@link MethodRule} based rules when used with the + * {@code @Rule} annotation on public fields. The rules encapsulate the {@link TestCase#runBare()} + * method and so are run before the {@link TestCase#setUp()} and after the + * {@link TestCase#tearDown()} methods. + * + *

Classes that extend this must have a single no argument constructor. + */ +public abstract class TestCaseWithRules extends TestCase { + + private final TestClass testClass; + + private final List validationErrors; + + public TestCaseWithRules() { + testClass = new TestClass(getClass()); + + validationErrors = new ArrayList<>(); + RULE_VALIDATOR.validate(testClass, validationErrors); + } + + @Override + public void runBare() throws Throwable { + if (!validationErrors.isEmpty()) { + throw new MultipleFailureException(validationErrors); + } + + Statement statement = new Statement() { + @Override + public void evaluate() throws Throwable { + superRunBare(); + } + }; + + final String name = getName(); + FrameworkMethod frameworkMethod; + try { + Method method = getClass().getMethod(name, (Class[]) null); + frameworkMethod = new FrameworkMethod(method); + } catch (NoSuchMethodException e) { + frameworkMethod = new FrameworkMethod(null) { + @Override + public String getName() { + return name; + } + + @Override + public Annotation[] getAnnotations() { + return new Annotation[0]; + } + + @Override + public T getAnnotation(Class annotationType) { + return null; + } + }; + } + Description description = + Description.createTestDescription(getClass(), frameworkMethod.getName(), + frameworkMethod.getAnnotations()); + + List rules = testClass.getAnnotatedFieldValues(this, Rule.class, Object.class); + for (Object rule : rules) { + if (rule instanceof TestRule) { + statement = ((TestRule) rule).apply(statement, description); + } else { + statement = ((MethodRule) rule).apply(statement, frameworkMethod, this); + } + } + + statement.evaluate(); + } + + private void superRunBare() throws Throwable { + super.runBare(); + } +} diff --git a/test-rules/src/main/java/libcore/junit/util/ResourceLeakageDetector.java b/test-rules/src/main/java/libcore/junit/util/ResourceLeakageDetector.java new file mode 100644 index 000000000..dae7978fb --- /dev/null +++ b/test-rules/src/main/java/libcore/junit/util/ResourceLeakageDetector.java @@ -0,0 +1,150 @@ +/* + * 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 libcore.junit.util; + +import java.lang.reflect.Method; +import java.util.function.BiConsumer; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Provides support for testing classes that own resources which must not leak. + * + *

This will not detect any resource leakages in OpenJDK

+ */ +public class ResourceLeakageDetector { + private static final LeakageDetectorRule LEAKAGE_DETECTOR_RULE; + private static final BiConsumer FINALIZER_CHECKER; + + static { + LeakageDetectorRule leakageDetectorRule; + BiConsumer finalizerChecker; + try { + // Make sure that the CloseGuard class exists; this ensures that this is not + // running on a RI JVM. + Class.forName("dalvik.system.CloseGuard"); + + // Access the underlying support class using reflection in order to prevent any compile + // time dependencies on it so as to allow this to compile on OpenJDK. + Class closeGuardSupportClass = Class.forName("dalvik.system.CloseGuardSupport"); + Method method = closeGuardSupportClass.getMethod("getRule"); + leakageDetectorRule = new LeakageDetectorRule((TestRule) method.invoke(null)); + + finalizerChecker = getFinalizerChecker(closeGuardSupportClass); + + } catch (ReflectiveOperationException e) { + System.err.println("Resource leakage will not be detected; " + + "this is expected in the reference implementation"); + e.printStackTrace(System.err); + + // Could not access the class for some reason so have a rule that does nothing and a + // finalizer checker that checks nothing. This should ensure that tests work properly + // on OpenJDK even though it does not support CloseGuard. + leakageDetectorRule = new LeakageDetectorRule(RuleChain.emptyRuleChain()); + finalizerChecker = new BiConsumer() { + @Override + public void accept(Object o, Integer integer) { + // Do nothing. + } + }; + } + + LEAKAGE_DETECTOR_RULE = leakageDetectorRule; + FINALIZER_CHECKER = finalizerChecker; + } + + @SuppressWarnings("unchecked") + private static BiConsumer getFinalizerChecker(Class closeGuardSupportClass) + throws ReflectiveOperationException { + Method method = closeGuardSupportClass.getMethod("getFinalizerChecker"); + return (BiConsumer) method.invoke(null); + } + + /** + * @return the {@link LeakageDetectorRule} + */ + public static LeakageDetectorRule getRule() { + return LEAKAGE_DETECTOR_RULE; + } + + /** + * A {@link TestRule} that will fail a test if it detects any resources that were allocated + * during the test but were not released. + * + *

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. + * + *

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. + * + *

{@code
+     *     @Rule
+     *     public final TestRule ruleChain = org.junit.rules.RuleChain
+     *         .outerRule(new ...invoke caching mechanism...)
+     *         .around(CloseGuardSupport.getRule());
+     * }
+ * + * @return a {@link TestRule} that detects resource leakages, or one that does nothing if + * resource leakage detection is not supported. + */ + public static class LeakageDetectorRule implements TestRule { + + private final TestRule leakageDetectorRule; + + private LeakageDetectorRule(TestRule leakageDetectorRule) { + this.leakageDetectorRule = leakageDetectorRule; + } + + @Override + public Statement apply(Statement base, Description description) { + return leakageDetectorRule.apply(base, description); + } + + /** + * Ensure that when the supplied object is finalized that it detects the expected number of + * unreleased resources. + * + *

This helps ensure that classes which own resources protected using {@code CloseGuard} + * support leakage detection. + * + *

This must only be called from within the test currently being run otherwise it will + * fail if the resource leakage detected mechanism is disabled, e.g. in CTS. + * + *

Use as follows: + *

+         *     Object object = ...create and 'open' an object encapsulating a protected resource...;
+         *     // Check to make sure that the object reports a resource leak when it is finalized.
+         *     assertUnreleasedResourceCount(object, 1);
+         *
+         *     object = ... create, 'open' and then 'close' another object ...;
+         *     // Check to make sure that the object does not have any unreleased resources.
+         *     assertUnreleasedResourceCount(object, 0);
+         * 
+ * + * @param owner the object that owns the resource and uses {@code CloseGuard} object to detect + * when the resource is not released. + * @param expectedCount the expected number of unreleased resources. + */ + public void assertUnreleasedResourceCount(Object owner, int expectedCount) { + FINALIZER_CHECKER.accept(owner, expectedCount); + } + } +}