Convert leakcanary-android-instrumentation Java files to Kotlin
See #1205
diff --git a/leakcanary-android-instrumentation/build.gradle b/leakcanary-android-instrumentation/build.gradle
index 51396dd..9b5b017 100644
--- a/leakcanary-android-instrumentation/build.gradle
+++ b/leakcanary-android-instrumentation/build.gradle
@@ -1,8 +1,11 @@
apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
dependencies {
+ api deps.kotlin.stdLib.common
api project(':leakcanary-android')
implementation 'androidx.test:runner:1.1.1'
+ implementation deps.kotlin.reflect
}
android {
diff --git a/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationLeakDetectorTest.java b/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationLeakDetectorTest.java
index 0a2a582..6814c53 100644
--- a/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationLeakDetectorTest.java
+++ b/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationLeakDetectorTest.java
@@ -29,13 +29,13 @@
InstrumentationLeakDetector leakDetector = new InstrumentationLeakDetector();
InstrumentationLeakResults results = leakDetector.detectLeaks();
- if (results.detectedLeaks.size() != 1) {
- throw new AssertionError("Expected exactly one leak, not " + results.detectedLeaks.size());
+ if (results.getDetectedLeaks().size() != 1) {
+ throw new AssertionError("Expected exactly one leak, not " + results.getDetectedLeaks().size());
}
- InstrumentationLeakResults.Result firstResult = results.detectedLeaks.get(0);
+ InstrumentationLeakResults.Result firstResult = results.getDetectedLeaks().get(0);
- String leakingClassName = firstResult.analysisResult.getClassName();
+ String leakingClassName = firstResult.getAnalysisResult().getClassName();
if (!leakingClassName.equals(Date.class.getName())) {
throw new AssertionError("Expected a leak of Date, not " + leakingClassName);
diff --git a/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationTestApplication.java b/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationTestApplication.java
index c534699..8258e79 100644
--- a/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationTestApplication.java
+++ b/leakcanary-android-instrumentation/src/androidTest/java/com/squareup/leakcanary/InstrumentationTestApplication.java
@@ -5,7 +5,7 @@
public class InstrumentationTestApplication extends Application {
@Override public void onCreate() {
super.onCreate();
- InstrumentationLeakDetector.instrumentationRefWatcher(this)
+ InstrumentationLeakDetector.Companion.instrumentationRefWatcher(this)
.buildAndInstall();
}
}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.java b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.java
deleted file mode 100644
index 721c4be..0000000
--- a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2018 Square, Inc.
- *
- * 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 com.squareup.leakcanary;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.internal.runner.listener.InstrumentationResultPrinter;
-import java.util.List;
-import org.junit.runner.Description;
-import org.junit.runner.Result;
-import org.junit.runner.notification.Failure;
-import org.junit.runner.notification.RunListener;
-
-import static androidx.test.internal.runner.listener.InstrumentationResultPrinter.REPORT_VALUE_RESULT_FAILURE;
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-import static com.squareup.leakcanary.Preconditions.checkNotNull;
-
-/**
- * <p>A JUnit {@link RunListener} for detecting memory leaks in Android instrumentation tests. It
- * waits for the end of a test, and if the test succeeds then it will look for leaking
- * references, trigger a heap dump if needed and perform an analysis.
- * <p> {@link FailTestOnLeakRunListener} can be subclassed to override
- * {@link #skipLeakDetectionReason(Description)}, {@link #reportLeaks(InstrumentationLeakResults)}
- * or {@link #buildLeakDetectedMessage(List)}
- *
- * @see InstrumentationLeakDetector
- */
-public class FailTestOnLeakRunListener extends RunListener {
-
- private static final String SEPARATOR = "######################################\n";
- private Bundle bundle;
-
- private String skipLeakDetectionReason;
-
- @Override public final void testStarted(Description description) {
- skipLeakDetectionReason = skipLeakDetectionReason(description);
- if (skipLeakDetectionReason != null) {
- return;
- }
- String testClass = description.getClassName();
- String testName = description.getMethodName();
-
- bundle = new Bundle();
- bundle.putString(Instrumentation.REPORT_KEY_IDENTIFIER,
- FailTestOnLeakRunListener.class.getName());
- bundle.putString(InstrumentationResultPrinter.REPORT_KEY_NAME_CLASS, testClass);
- bundle.putString(InstrumentationResultPrinter.REPORT_KEY_NAME_TEST, testName);
- }
-
- /**
- * Can be overridden to skip leak detection based on the description provided when a test
- * is started. Returns null to continue leak detection, or a string describing the reason for
- * skipping otherwise.
- */
- protected @Nullable String skipLeakDetectionReason(@NonNull Description description) {
- return null;
- }
-
- @Override public final void testFailure(Failure failure) {
- skipLeakDetectionReason = "failed";
- }
-
- @Override public final void testIgnored(Description description) {
- skipLeakDetectionReason = "was ignored";
- }
-
- @Override public final void testAssumptionFailure(Failure failure) {
- skipLeakDetectionReason = "had an assumption failure";
- }
-
- @Override public final void testFinished(Description description) {
- detectLeaks();
- LeakCanary.installedRefWatcher().clearWatchedReferences();
- }
-
- @Override public final void testRunStarted(Description description) {
- }
-
- @Override public final void testRunFinished(Result result) {
- }
-
- private void detectLeaks() {
- if (skipLeakDetectionReason != null) {
- CanaryLog.d("Skipping leak detection because the test %s", skipLeakDetectionReason);
- skipLeakDetectionReason = null;
- return;
- }
-
- InstrumentationLeakDetector leakDetector = new InstrumentationLeakDetector();
- InstrumentationLeakResults results = leakDetector.detectLeaks();
-
- reportLeaks(results);
- }
-
- /** Can be overridden to report leaks in a different way or do additional reporting. */
- protected void reportLeaks(@NonNull InstrumentationLeakResults results) {
- if (!results.detectedLeaks.isEmpty()) {
- String message =
- checkNotNull(buildLeakDetectedMessage(results.detectedLeaks), "buildLeakDetectedMessage");
-
- bundle.putString(InstrumentationResultPrinter.REPORT_KEY_STACK, message);
- getInstrumentation().sendStatus(REPORT_VALUE_RESULT_FAILURE, bundle);
- }
- }
-
- /** Can be overridden to customize the failure string message. */
- protected @NonNull String buildLeakDetectedMessage(
- @NonNull List<InstrumentationLeakResults.Result> detectedLeaks) {
- StringBuilder failureMessage = new StringBuilder();
- failureMessage.append(
- "Test failed because memory leaks were detected, see leak traces below.\n");
- failureMessage.append(SEPARATOR);
-
- Context context = getInstrumentation().getContext();
- for (InstrumentationLeakResults.Result detectedLeak : detectedLeaks) {
- failureMessage.append(
- LeakCanary.leakInfo(context, detectedLeak.heapDump, detectedLeak.analysisResult, true));
- failureMessage.append(SEPARATOR);
- }
-
- return failureMessage.toString();
- }
-}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.kt b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.kt
new file mode 100644
index 0000000..6541959
--- /dev/null
+++ b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/FailTestOnLeakRunListener.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * 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 com.squareup.leakcanary
+
+import android.app.Instrumentation
+import android.os.Bundle
+import androidx.test.internal.runner.listener.InstrumentationResultPrinter
+import androidx.test.internal.runner.listener.InstrumentationResultPrinter.REPORT_VALUE_RESULT_FAILURE
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.squareup.leakcanary.Preconditions.checkNotNull
+import org.junit.runner.Description
+import org.junit.runner.Result
+import org.junit.runner.notification.Failure
+import org.junit.runner.notification.RunListener
+
+/**
+ *
+ * A JUnit [RunListener] for detecting memory leaks in Android instrumentation tests. It
+ * waits for the end of a test, and if the test succeeds then it will look for leaking
+ * references, trigger a heap dump if needed and perform an analysis.
+ *
+ * [FailTestOnLeakRunListener] can be subclassed to override
+ * [skipLeakDetectionReason], [reportLeaks]
+ * or [buildLeakDetectedMessage]
+ *
+ * @see InstrumentationLeakDetector
+ */
+open class FailTestOnLeakRunListener : RunListener() {
+ private lateinit var bundle: Bundle
+ private var skipLeakDetectionReason: String? = null
+
+ override fun testStarted(description: Description) {
+ skipLeakDetectionReason = skipLeakDetectionReason(description)
+ if (skipLeakDetectionReason != null) {
+ return
+ }
+ val testClass = description.className
+ val testName = description.methodName
+
+ bundle = Bundle()
+ bundle.putString(
+ Instrumentation.REPORT_KEY_IDENTIFIER, FailTestOnLeakRunListener::class.java.name
+ )
+ bundle.putString(InstrumentationResultPrinter.REPORT_KEY_NAME_CLASS, testClass)
+ bundle.putString(InstrumentationResultPrinter.REPORT_KEY_NAME_TEST, testName)
+ }
+
+ /**
+ * Can be overridden to skip leak detection based on the description provided when a test
+ * is started. Returns null to continue leak detection, or a string describing the reason for
+ * skipping otherwise.
+ */
+ protected open fun skipLeakDetectionReason(description: Description): String? {
+ return null
+ }
+
+ override fun testFailure(failure: Failure) {
+ skipLeakDetectionReason = "failed"
+ }
+
+ override fun testIgnored(description: Description) {
+ skipLeakDetectionReason = "was ignored"
+ }
+
+ override fun testAssumptionFailure(failure: Failure) {
+ skipLeakDetectionReason = "had an assumption failure"
+ }
+
+ override fun testFinished(description: Description) {
+ detectLeaks()
+ LeakCanary.installedRefWatcher()
+ .clearWatchedReferences()
+ }
+
+ override fun testRunStarted(description: Description) {}
+
+ override fun testRunFinished(result: Result) {}
+
+ private fun detectLeaks() {
+ if (skipLeakDetectionReason != null) {
+ CanaryLog.d("Skipping leak detection because the test %s", skipLeakDetectionReason)
+ skipLeakDetectionReason = null
+ return
+ }
+
+ val leakDetector = InstrumentationLeakDetector()
+ val results = leakDetector.detectLeaks()
+
+ reportLeaks(results)
+ }
+
+ /** Can be overridden to report leaks in a different way or do additional reporting. */
+ protected open fun reportLeaks(results: InstrumentationLeakResults) {
+ if (results.detectedLeaks.isNotEmpty()) {
+ val message =
+ checkNotNull(
+ buildLeakDetectedMessage(results.detectedLeaks), "buildLeakDetectedMessage"
+ )
+
+ bundle.putString(InstrumentationResultPrinter.REPORT_KEY_STACK, message)
+ getInstrumentation().sendStatus(REPORT_VALUE_RESULT_FAILURE, bundle)
+ }
+ }
+
+ /** Can be overridden to customize the failure string message. */
+ protected open fun buildLeakDetectedMessage(
+ detectedLeaks: List<InstrumentationLeakResults.Result>
+ ): String {
+ val failureMessage = StringBuilder()
+ failureMessage.append(
+ "Test failed because memory leaks were detected, see leak traces below.\n"
+ )
+ failureMessage.append(SEPARATOR)
+
+ val context = getInstrumentation().context
+ detectedLeaks.forEach { detectedLeak ->
+ failureMessage.append(
+ LeakCanary.leakInfo(
+ context, detectedLeak.heapDump, detectedLeak.analysisResult, true
+ )
+ )
+ failureMessage.append(SEPARATOR)
+ }
+
+ return failureMessage.toString()
+ }
+
+ companion object {
+ private const val SEPARATOR = "######################################\n"
+ }
+}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.java b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.java
deleted file mode 100644
index f1b6375..0000000
--- a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (C) 2018 Square, Inc.
- *
- * 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 com.squareup.leakcanary;
-
-import android.app.Application;
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Debug;
-import android.os.SystemClock;
-import androidx.annotation.NonNull;
-import com.squareup.leakcanary.internal.LeakCanaryInternals;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArrayList;
-import org.junit.runner.notification.RunListener;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-/**
- * <p>{@link InstrumentationLeakDetector} can be used to detect memory leaks in instrumentation
- * tests.
- *
- * <p>To use it, you need to:
- * <ul>
- * <li>Install a custom RefWatcher that will not trigger heapdumps while the tests run.</li>
- * <li>Add an instrumentation test listener (a {@link RunListener}) that will invoke
- * {@link #detectLeaks()}</li>
- * </ul>
- *
- * <h3>Installing the instrumentation RefWatcher</h3>
- *
- * <p>For {@link #detectLeaks()} to work correctly, the {@link RefWatcher} must keep track of
- * references but not trigger any heap dump until this {@link #detectLeaks()} runs, otherwise an
- * analysis in progress might prevent this listener from performing its own analysis.
- *
- * <p>Create and install the {@link RefWatcher} instance using
- * {@link #instrumentationRefWatcher(Application)} instead of
- * {@link LeakCanary#install(Application)} or {@link LeakCanary#refWatcher(Context)}.
- * <pre><code>
- * public class InstrumentationExampleApplication extends ExampleApplication {
- * {@literal @}Override protected void setupLeakCanary() {
- * InstrumentationLeakDetector.instrumentationRefWatcher(this)
- * .buildAndInstall();
- * }
- * }
- * </code></pre>
- *
- * <h3>Add an intrumentation test listener</h3>
- *
- * <p>LeakCanary provides {@link FailTestOnLeakRunListener}, but you should feel free to implement
- * your own {@link RunListener} and call {@link #detectLeaks()} directly if you need a more custom
- * behavior (for instance running it only once per test suite, or reporting to a backend).</p>
- *
- * <p>All you need to do is add the following to the defaultConfig of your build.gradle:
- *
- * <pre><code>testInstrumentationRunnerArgument "listener", "com.squareup.leakcanary.FailTestOnLeakRunListener"</code></pre>
- *
- * <p>Then you can run your instrumentation tests via Gradle as usually, and they will fail when
- * a memory leak is detected:
- *
- * <pre><code>./gradlew leakcanary-sample:connectedCheck</code></pre>
- *
- * <p>If instead you want to run UI tests via adb, add a <em>listener</em> execution argument to
- * your command line for running the UI tests:
- * <code>-e listener com.squareup.leakcanary.FailTestOnLeakRunListener</code>. The full command line
- * should look something like this:
- * <pre><code>adb shell am instrument \\
- * -w com.android.foo/android.support.test.runner.AndroidJUnitRunner \\
- * -e listener com.squareup.leakcanary.FailTestOnLeakRunListener
- * </code></pre>
- *
- * <h3>Rationale</h3>
- * Instead of using the {@link FailTestOnLeakRunListener}, one could simply enable LeakCanary in
- * instrumentation tests.
- *
- * <p>This approach would have two disadvantages:
- * <ul>
- * <li>Heap dumps freeze the VM, and the leak analysis is IO and CPU heavy. This can slow down
- * the test and introduce flakiness</li>
- * <li>The leak analysis is asynchronous by default, and happens in a separate process. This means
- * the tests could finish and the process die before the analysis is finished.</li>
- * </ul>
- *
- * <p>The approach taken here is to collect all references to watch as you run the test, but not
- * do any heap dump during the test. Then, at the end, if any of the watched objects is still in
- * memory we dump the heap and perform a blocking analysis. There is only one heap dump performed,
- * no matter the number of objects leaking, and then we iterate on the leaking references in the
- * heap dump and provide all result in a {@link InstrumentationLeakResults}.
- */
-public final class InstrumentationLeakDetector {
-
- /**
- * Returns a new {@link} AndroidRefWatcherBuilder that will create a {@link RefWatcher} suitable
- * for instrumentation tests. This {@link RefWatcher} will never trigger a heap dump. This should
- * be installed from the test application class, and should be used in combination with a
- * {@link RunListener} that calls {@link #detectLeaks()}, for instance
- * {@link FailTestOnLeakRunListener}.
- */
- public static @NonNull AndroidRefWatcherBuilder instrumentationRefWatcher(
- @NonNull Application application) {
- return LeakCanary.refWatcher(application)
- .watchExecutor(new WatchExecutor() {
- // Storing weak refs to ensure they make it to the queue.
- final List<Retryable> trackedReferences = new CopyOnWriteArrayList<>();
-
- @Override public void execute(Retryable retryable) {
- trackedReferences.add(retryable);
- }
- });
- }
-
- public @NonNull InstrumentationLeakResults detectLeaks() {
- Instrumentation instrumentation = getInstrumentation();
- Context context = instrumentation.getTargetContext();
- RefWatcher refWatcher = LeakCanary.installedRefWatcher();
- Set<String> retainedKeys = refWatcher.getRetainedKeys();
-
- if (refWatcher.isEmpty()) {
- return InstrumentationLeakResults.NONE;
- }
-
- instrumentation.waitForIdleSync();
- if (refWatcher.isEmpty()) {
- return InstrumentationLeakResults.NONE;
- }
-
- GcTrigger.DEFAULT.runGc();
- if (refWatcher.isEmpty()) {
- return InstrumentationLeakResults.NONE;
- }
-
- // Waiting for any delayed UI post (e.g. scroll) to clear. This shouldn't be needed, but
- // Android simply has way too many delayed posts that aren't canceled when views are detached.
- SystemClock.sleep(2000);
-
- if (refWatcher.isEmpty()) {
- return InstrumentationLeakResults.NONE;
- }
-
- // Aaand we wait some more.
- // 4 seconds (2+2) is greater than the 3 seconds delay for
- // FINISH_TOKEN in android.widget.Filter
- SystemClock.sleep(2000);
- GcTrigger.DEFAULT.runGc();
-
- if (refWatcher.isEmpty()) {
- return InstrumentationLeakResults.NONE;
- }
-
- // We're always reusing the same file since we only execute this once at a time.
- File heapDumpFile = new File(context.getFilesDir(), "instrumentation_tests_heapdump.hprof");
- try {
- Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
- } catch (Exception e) {
- CanaryLog.d(e, "Could not dump heap");
- return InstrumentationLeakResults.NONE;
- }
-
- HeapDump.Builder heapDumpBuilder = LeakCanaryInternals.installedHeapDumpBuilder;
- HeapAnalyzer heapAnalyzer =
- new HeapAnalyzer(heapDumpBuilder.excludedRefs, AnalyzerProgressListener.Companion.getNONE(),
- heapDumpBuilder.reachabilityInspectorClasses);
-
- List<TrackedReference> trackedReferences = heapAnalyzer.findTrackedReferences(heapDumpFile);
-
- List<InstrumentationLeakResults.Result> detectedLeaks = new ArrayList<>();
- List<InstrumentationLeakResults.Result> excludedLeaks = new ArrayList<>();
- List<InstrumentationLeakResults.Result> failures = new ArrayList<>();
-
- for (TrackedReference trackedReference : trackedReferences) {
- // Ignore any Weak Reference that this test does not care about.
- if (!retainedKeys.contains(trackedReference.getKey())) {
- continue;
- }
-
- HeapDump heapDump = HeapDump.builder()
- .heapDumpFile(heapDumpFile)
- .excludedRefs(heapDumpBuilder.excludedRefs)
- .reachabilityInspectorClasses(heapDumpBuilder.reachabilityInspectorClasses)
- .build();
-
- AnalysisResult analysisResult =
- heapAnalyzer.checkForLeak(heapDumpFile, trackedReference.getKey(), false);
-
- InstrumentationLeakResults.Result leakResult =
- new InstrumentationLeakResults.Result(heapDump, analysisResult);
-
- if (analysisResult.getLeakFound()) {
- if (!analysisResult.getExcludedLeak()) {
- detectedLeaks.add(leakResult);
- } else {
- excludedLeaks.add(leakResult);
- }
- } else if (analysisResult.getFailure() != null) {
- failures.add(leakResult);
- }
- }
-
- CanaryLog.d("Found %d proper leaks, %d excluded leaks and %d leak analysis failures",
- detectedLeaks.size(),
- excludedLeaks.size(),
- failures.size());
-
- return new InstrumentationLeakResults(detectedLeaks, excludedLeaks, failures);
- }
-}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.kt b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.kt
new file mode 100644
index 0000000..0d544a8
--- /dev/null
+++ b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakDetector.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * 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 com.squareup.leakcanary
+
+import android.app.Application
+import android.os.Debug
+import android.os.SystemClock
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.squareup.leakcanary.InstrumentationLeakDetector.Companion.instrumentationRefWatcher
+import com.squareup.leakcanary.internal.LeakCanaryInternals
+import org.junit.runner.notification.RunListener
+import java.io.File
+import java.util.concurrent.CopyOnWriteArrayList
+
+/**
+ * [InstrumentationLeakDetector] can be used to detect memory leaks in instrumentation tests.
+ *
+ * To use it, you need to:
+ *
+ * * Install a custom RefWatcher that will not trigger heapdumps while the tests run.
+ * * Add an instrumentation test listener (a [RunListener]) that will invoke
+ * [detectLeaks]
+ *
+ * ### Installing the instrumentation RefWatcher
+ *
+ * For [detectLeaks] to work correctly, the [RefWatcher] must keep track of
+ * references but not trigger any heap dump until this [detectLeaks] runs, otherwise an
+ * analysis in progress might prevent this listener from performing its own analysis.
+ *
+ * Create and install the [RefWatcher] instance using
+ * [instrumentationRefWatcher] instead of
+ * [LeakCanary.install] or [LeakCanary.refWatcher].
+ *
+ * ```
+ * public class InstrumentationExampleApplication extends ExampleApplication {
+ * @Override protected void setupLeakCanary() {
+ * InstrumentationLeakDetector.instrumentationRefWatcher(this)
+ * .buildAndInstall();
+ * }
+ * }
+ * ```
+ *
+ * ### Add an instrumentation test listener
+ *
+ * LeakCanary provides [FailTestOnLeakRunListener], but you should feel free to implement
+ * your own [RunListener] and call [.detectLeaks] directly if you need a more custom
+ * behavior (for instance running it only once per test suite, or reporting to a backend).
+ *
+ * All you need to do is add the following to the defaultConfig of your build.gradle:
+ *
+ * `testInstrumentationRunnerArgument "listener", "com.squareup.leakcanary.FailTestOnLeakRunListener"`
+ *
+ * Then you can run your instrumentation tests via Gradle as usually, and they will fail when
+ * a memory leak is detected:
+ *
+ * `./gradlew leakcanary-sample:connectedCheck`
+ *
+ * If instead you want to run UI tests via adb, add a *listener* execution argument to
+ * your command line for running the UI tests:
+ * `-e listener com.squareup.leakcanary.FailTestOnLeakRunListener`. The full command line
+ * should look something like this:
+ * ```
+ * adb shell am instrument \\
+ * -w com.android.foo/android.support.test.runner.AndroidJUnitRunner \\
+ * -e listener com.squareup.leakcanary.FailTestOnLeakRunListener
+ * ```
+ *
+ * ### Rationale
+ * Instead of using the [FailTestOnLeakRunListener], one could simply enable LeakCanary in
+ * instrumentation tests.
+ *
+ * This approach would have two disadvantages:
+ *
+ * * Heap dumps freeze the VM, and the leak analysis is IO and CPU heavy. This can slow down
+ * the test and introduce flakiness
+ * * The leak analysis is asynchronous by default, and happens in a separate process. This means
+ * the tests could finish and the process die before the analysis is finished.
+ *
+ * The approach taken here is to collect all references to watch as you run the test, but not
+ * do any heap dump during the test. Then, at the end, if any of the watched objects is still in
+ * memory we dump the heap and perform a blocking analysis. There is only one heap dump performed,
+ * no matter the number of objects leaking, and then we iterate on the leaking references in the
+ * heap dump and provide all result in a [InstrumentationLeakResults].
+ */
+class InstrumentationLeakDetector {
+
+ fun detectLeaks(): InstrumentationLeakResults {
+ val instrumentation = getInstrumentation()
+ val context = instrumentation.targetContext
+ val refWatcher = LeakCanary.installedRefWatcher()
+ val retainedKeys = refWatcher.retainedKeys
+
+ if (refWatcher.isEmpty) {
+ return InstrumentationLeakResults.NONE
+ }
+
+ instrumentation.waitForIdleSync()
+ if (refWatcher.isEmpty) {
+ return InstrumentationLeakResults.NONE
+ }
+
+ GcTrigger.DEFAULT.runGc()
+ if (refWatcher.isEmpty) {
+ return InstrumentationLeakResults.NONE
+ }
+
+ // Waiting for any delayed UI post (e.g. scroll) to clear. This shouldn't be needed, but
+ // Android simply has way too many delayed posts that aren't canceled when views are detached.
+ SystemClock.sleep(2000)
+
+ if (refWatcher.isEmpty) {
+ return InstrumentationLeakResults.NONE
+ }
+
+ // Aaand we wait some more.
+ // 4 seconds (2+2) is greater than the 3 seconds delay for
+ // FINISH_TOKEN in android.widget.Filter
+ SystemClock.sleep(2000)
+ GcTrigger.DEFAULT.runGc()
+
+ if (refWatcher.isEmpty) {
+ return InstrumentationLeakResults.NONE
+ }
+
+ // We're always reusing the same file since we only execute this once at a time.
+ val heapDumpFile = File(context.filesDir, "instrumentation_tests_heapdump.hprof")
+ try {
+ Debug.dumpHprofData(heapDumpFile.absolutePath)
+ } catch (e: Exception) {
+ CanaryLog.d(e, "Could not dump heap")
+ return InstrumentationLeakResults.NONE
+ }
+
+ val heapDumpBuilder = LeakCanaryInternals.installedHeapDumpBuilder
+ val heapAnalyzer = HeapAnalyzer(
+ heapDumpBuilder.excludedRefs, AnalyzerProgressListener.NONE,
+ heapDumpBuilder.reachabilityInspectorClasses
+ )
+
+ val trackedReferences = heapAnalyzer.findTrackedReferences(heapDumpFile)
+
+ val detectedLeaks = mutableListOf<InstrumentationLeakResults.Result>()
+ val excludedLeaks = mutableListOf<InstrumentationLeakResults.Result>()
+ val failures = mutableListOf<InstrumentationLeakResults.Result>()
+
+
+ trackedReferences
+ // Ignore any Weak Reference that this test does not care about.
+ .filter { retainedKeys.contains(it.key) }
+ .forEach { trackedReference ->
+ val heapDump = HeapDump.builder()
+ .heapDumpFile(heapDumpFile)
+ .excludedRefs(heapDumpBuilder.excludedRefs)
+ .reachabilityInspectorClasses(heapDumpBuilder.reachabilityInspectorClasses)
+ .build()
+
+ val analysisResult =
+ heapAnalyzer.checkForLeak(heapDumpFile, trackedReference.key, false)
+
+ val leakResult = InstrumentationLeakResults.Result(heapDump, analysisResult)
+
+ if (analysisResult.leakFound) {
+ if (!analysisResult.excludedLeak) {
+ detectedLeaks.add(leakResult)
+ } else {
+ excludedLeaks.add(leakResult)
+ }
+ } else if (analysisResult.failure != null) {
+ failures.add(leakResult)
+ }
+ }
+
+ CanaryLog.d(
+ "Found %d proper leaks, %d excluded leaks and %d leak analysis failures",
+ detectedLeaks.size,
+ excludedLeaks.size,
+ failures.size
+ )
+
+ return InstrumentationLeakResults(detectedLeaks, excludedLeaks, failures)
+ }
+
+ companion object {
+ /**
+ * Returns a new [AndroidRefWatcherBuilder] that will create a [RefWatcher] suitable
+ * for instrumentation tests. This [RefWatcher] will never trigger a heap dump. This should
+ * be installed from the test application class, and should be used in combination with a
+ * [RunListener] that calls [detectLeaks], for instance
+ * [FailTestOnLeakRunListener].
+ */
+ fun instrumentationRefWatcher(application: Application): AndroidRefWatcherBuilder {
+ return LeakCanary.refWatcher(application)
+ .watchExecutor(object : WatchExecutor {
+ // Storing weak refs to ensure they make it to the queue.
+ val trackedReferences: MutableList<Retryable> = CopyOnWriteArrayList()
+
+ override fun execute(retryable: Retryable) {
+ trackedReferences.add(retryable)
+ }
+ })
+ }
+ }
+}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.java b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.java
deleted file mode 100644
index e783663..0000000
--- a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2018 Square, Inc.
- *
- * 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 com.squareup.leakcanary;
-
-import androidx.annotation.NonNull;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static java.util.Collections.unmodifiableList;
-
-public final class InstrumentationLeakResults {
-
- @NonNull public static final InstrumentationLeakResults NONE =
- new InstrumentationLeakResults(Collections.<Result>emptyList(),
- Collections.<Result>emptyList(), Collections.<Result>emptyList());
-
- /** Proper leaks found during instrumentation tests. */
- @NonNull public final List<Result> detectedLeaks;
-
- /**
- * Excluded leaks found during instrumentation tests, based on {@link
- * RefWatcherBuilder#excludedRefs}
- */
- @NonNull public final List<Result> excludedLeaks;
-
- /**
- * Leak analysis failures that happened when we tried to detect leaks.
- */
- @NonNull public final List<Result> failures;
-
- public InstrumentationLeakResults(@NonNull List<Result> detectedLeaks,
- @NonNull List<Result> excludedLeaks, @NonNull List<Result> failures) {
- this.detectedLeaks = unmodifiableList(new ArrayList<>(detectedLeaks));
- this.excludedLeaks = unmodifiableList(new ArrayList<>(excludedLeaks));
- this.failures = unmodifiableList(new ArrayList<>(failures));
- }
-
- public static final class Result {
- @NonNull public final HeapDump heapDump;
- @NonNull public final AnalysisResult analysisResult;
-
- public Result(@NonNull HeapDump heapDump, @NonNull AnalysisResult analysisResult) {
- this.heapDump = heapDump;
- this.analysisResult = analysisResult;
- }
- }
-}
diff --git a/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.kt b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.kt
new file mode 100644
index 0000000..50fbb14
--- /dev/null
+++ b/leakcanary-android-instrumentation/src/main/java/com/squareup/leakcanary/InstrumentationLeakResults.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * 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 com.squareup.leakcanary
+
+/**
+ * @param detectedLeaks Proper leaks found during instrumentation tests.
+ *
+ * @param excludedLeaks Excluded leaks found during instrumentation tests,
+ * based on [RefWatcherBuilder.excludedRefs]
+ *
+ * @param failures Leak analysis failures that happened when we tried to detect leaks.
+ */
+class InstrumentationLeakResults(
+ val detectedLeaks: List<Result>,
+ val excludedLeaks: List<Result>,
+ val failures: List<Result>
+) {
+
+ class Result(
+ val heapDump: HeapDump,
+ val analysisResult: AnalysisResult
+ )
+
+ companion object {
+ val NONE = InstrumentationLeakResults(emptyList(), emptyList(), emptyList())
+ }
+}
diff --git a/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/FragmentLeakTest.java b/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/FragmentLeakTest.java
index dd5c56c..e73a2eb 100644
--- a/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/FragmentLeakTest.java
+++ b/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/FragmentLeakTest.java
@@ -6,8 +6,8 @@
import android.os.Bundle;
import android.os.Looper;
import android.os.MessageQueue;
-import androidx.test.rule.ActivityTestRule;
import android.view.View;
+import androidx.test.rule.ActivityTestRule;
import com.squareup.leakcanary.InstrumentationLeakDetector;
import com.squareup.leakcanary.InstrumentationLeakResults;
import com.squareup.leakcanary.LeakCanary;
@@ -94,20 +94,20 @@
InstrumentationLeakDetector leakDetector = new InstrumentationLeakDetector();
InstrumentationLeakResults results = leakDetector.detectLeaks();
- if (results.detectedLeaks.size() != 1) {
+ if (results.getDetectedLeaks().size() != 1) {
throw new AssertionError(
- "Expected exactly one leak, not " + results.detectedLeaks.size() + resultsAsString(
- results.detectedLeaks));
+ "Expected exactly one leak, not " + results.getDetectedLeaks().size() + resultsAsString(
+ results.getDetectedLeaks()));
}
- InstrumentationLeakResults.Result firstResult = results.detectedLeaks.get(0);
+ InstrumentationLeakResults.Result firstResult = results.getDetectedLeaks().get(0);
- String leakingClassName = firstResult.analysisResult.getClassName();
+ String leakingClassName = firstResult.getAnalysisResult().getClassName();
if (!leakingClassName.equals(expectedLeakClass.getName())) {
throw new AssertionError(
"Expected a leak of " + expectedLeakClass + ", not " + leakingClassName + resultsAsString(
- results.detectedLeaks));
+ results.getDetectedLeaks()));
}
}
@@ -117,7 +117,7 @@
message.append("\nLeaks found:\n##################\n");
for (InstrumentationLeakResults.Result detectedLeak : results) {
message.append(
- LeakCanary.leakInfo(context, detectedLeak.heapDump, detectedLeak.analysisResult,
+ LeakCanary.leakInfo(context, detectedLeak.getHeapDump(), detectedLeak.getAnalysisResult(),
false));
}
message.append("\n##################\n");
diff --git a/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/InstrumentationTestApplication.java b/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/InstrumentationTestApplication.java
index bd0321f..0674573 100644
--- a/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/InstrumentationTestApplication.java
+++ b/leakcanary-support-fragment/src/androidTest/java/com/squareup/leakcanary/tests/InstrumentationTestApplication.java
@@ -6,7 +6,7 @@
public class InstrumentationTestApplication extends Application {
@Override public void onCreate() {
super.onCreate();
- InstrumentationLeakDetector.instrumentationRefWatcher(this)
+ InstrumentationLeakDetector.Companion.instrumentationRefWatcher(this)
.buildAndInstall();
}
}