Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow updating LeakCanary config without installing #2273

Merged
merged 1 commit into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: reactivecircus/[email protected]
with:
api-level: 16
script: ./gradlew leakcanary-android-core:connectedCheck leakcanary-android-instrumentation:connectedCheck --stacktrace
script: ./gradlew leakcanary-android-core:connectedCheck leakcanary-android:connectedCheck leakcanary-android-instrumentation:connectedCheck --stacktrace

snapshot-deployment:
if: github.repository == 'square/leakcanary' && github.event_name == 'push'
Expand Down
5 changes: 5 additions & 0 deletions leakcanary-android-core/api/leakcanary-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public abstract interface class leakcanary/HeapDumper {
public abstract fun dumpHeap (Ljava/io/File;)V
}

public final class leakcanary/LazyForwardingEventListener : leakcanary/EventListener {
public fun <init> (Lkotlin/jvm/functions/Function0;)V
public fun onEvent (Lleakcanary/EventListener$Event;)V
}

public final class leakcanary/LeakCanary {
public static final field INSTANCE Lleakcanary/LeakCanary;
public final fun dumpHeap ()V
Expand Down
3 changes: 1 addition & 2 deletions leakcanary-android-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ dependencies {
testImplementation libs.kotlin.reflect
testImplementation libs.mockito
testImplementation libs.mockitoKotlin
// AppWatcher auto installer for running tests
androidTestImplementation project(':leakcanary-object-watcher-android')
androidTestImplementation libs.androidX.test.espresso
androidTestImplementation libs.androidX.test.rules
androidTestImplementation libs.androidX.test.runner
androidTestImplementation libs.assertjCore
androidTestImplementation project(':shark-hprof-test')
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package leakcanary

import android.content.Intent
import android.net.Uri
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
Expand All @@ -12,6 +10,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import com.squareup.leakcanary.core.R
import java.io.File
import leakcanary.internal.activity.LeakActivity
import leakcanary.internal.activity.db.HeapAnalysisTable
import leakcanary.internal.activity.db.LeakTable.AllLeaksProjection
Expand All @@ -30,9 +29,6 @@ import shark.LeakTraceObject
import shark.OnAnalysisProgressListener
import shark.ValueHolder.IntHolder
import shark.dump
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit.SECONDS

internal class LeakActivityTest {

Expand Down Expand Up @@ -93,27 +89,6 @@ internal class LeakActivityTest {
.check(matches(isDisplayed()))
}

@Test
fun importHeapDumpFile() = tryAndRestoreConfig {
val latch = CountDownLatch(1)
LeakCanary.config = LeakCanary.config.copy(onHeapAnalyzedListener = {
DefaultOnHeapAnalyzedListener.create().onHeapAnalyzed(it)
latch.countDown()
})
val hprof = writeHeapDump {
"Holder" clazz {
staticField["leak"] = "com.example.Leaking" watchedInstance {}
}
}
val intent = Intent(Intent.ACTION_VIEW, Uri.fromFile(hprof))
activityTestRule.launchActivity(intent)
require(latch.await(5, SECONDS))
onView(withText("1 Heap Dump")).check(matches(isDisplayed()))
onData(withItem<HeapAnalysisTable.Projection> { it.leakCount == 1 })
.perform(click())
onView(withText("1 Distinct Leak")).check(matches(isDisplayed()))
}

private fun writeHeapDump(block: HprofWriterHelper.() -> Unit): File {
val hprofFile = testFolder.newFile("temp.hprof")
hprofFile.dump {
Expand Down Expand Up @@ -163,14 +138,13 @@ internal class LeakActivityTest {
}
}
}

private fun tryAndRestoreConfig(block: () -> Unit) {
val original = LeakCanary.config
try {
block()
} finally {
LeakCanary.config = original
}
}

fun tryAndRestoreConfig(block: () -> Unit) {
val original = LeakCanary.config
try {
block()
} finally {
LeakCanary.config = original
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package leakcanary

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class NoInstallTest {

@Test fun appWatcher_is_not_installed() {
assertThat(AppWatcher.isInstalled).isFalse()
}

@Test fun can_update_LeakCanary_config_without_installing() = tryAndRestoreConfig {
LeakCanary.config = LeakCanary.config.copy(dumpHeap = false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package leakcanary

import leakcanary.EventListener.Event

/**
* Forwards events to the [EventListener] provided by lazyEventListener which
* is evaluated lazily, when the first comes in.
*/
class LazyForwardingEventListener(
lazyEventListener: () -> EventListener
) : EventListener {

private val eventListenerDelegate by lazy(lazyEventListener)

override fun onEvent(event: Event) {
eventListenerDelegate.onEvent(event)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ object LeakCanary {
val eventListeners: List<EventListener> = listOf(
LogcatEventListener,
ToastEventListener,
if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener,
LazyForwardingEventListener {
if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener
},
when {
RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath ->
RemoteWorkManagerHeapAnalyzer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ object RemoteWorkManagerHeapAnalyzer : EventListener {
}
}

private val application = InternalLeakCanary.application

override fun onEvent(event: Event) {
if (event is HeapDump) {
val application = InternalLeakCanary.application
val heapAnalysisRequest =
OneTimeWorkRequest.Builder(RemoteHeapAnalyzerWorker::class.java).apply {
val dataBuilder = Data.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import leakcanary.internal.friendly.mainHandler

object ToastEventListener : EventListener {

private val appContext = InternalLeakCanary.application

// Only accessed from the main thread
private var toastCurrentlyShown: Toast? = null

Expand All @@ -36,7 +34,9 @@ object ToastEventListener : EventListener {
}
}

@Suppress("DEPRECATION")
private fun showToastBlocking() {
val appContext = InternalLeakCanary.application
val waitingForToast = CountDownLatch(1)
mainHandler.post(Runnable {
val resumedActivity = InternalLeakCanary.resumedActivity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ object WorkManagerHeapAnalyzer : EventListener {
}
}

private val application = InternalLeakCanary.application

// setExpedited() requires WorkManager 2.7.0+
private val workManagerSupportsExpeditedRequests by lazy {
try {
Expand All @@ -49,6 +47,7 @@ object WorkManagerHeapAnalyzer : EventListener {
addExpeditedFlag()
}.build()
SharkLog.d { "Enqueuing heap analysis for ${event.file} on WorkManager remote worker" }
val application = InternalLeakCanary.application
WorkManager.getInstance(application).enqueue(heapAnalysisRequest)
}
}
Expand Down
7 changes: 7 additions & 0 deletions leakcanary-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ dependencies {
api project(':leakcanary-object-watcher-android')
// Plumber auto installer
implementation project(':plumber-android')

androidTestImplementation libs.androidX.test.espresso
androidTestImplementation libs.androidX.test.rules
androidTestImplementation libs.androidX.test.runner
androidTestImplementation libs.assertjCore
androidTestImplementation project(':shark-hprof-test')
}

android {
compileSdk versions.compileSdk
defaultConfig {
minSdk versions.minSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures.buildConfig = false
lintOptions {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package leakcanary

import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit.SECONDS
import leakcanary.EventListener.Event.HeapAnalysisDone
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import shark.HprofWriterHelper
import shark.ValueHolder.IntHolder
import shark.dump

internal class LeakActivityTest {

@get:Rule
var testFolder = TemporaryFolder()

@Suppress("UNCHECKED_CAST")
// This class is internal but ActivityTestRule requires being passed in the real class.
private val leakActivityClass = Class.forName("leakcanary.internal.activity.LeakActivity")
as Class<Activity>

@get:Rule
val activityTestRule = object : ActivityTestRule<Activity>(leakActivityClass, false, false) {
override fun getActivityIntent(): Intent {
return LeakCanary.newLeakDisplayActivityIntent()
}
}

@Test
fun importHeapDumpFile() = tryAndRestoreConfig {
val latch = CountDownLatch(1)
LeakCanary.config = LeakCanary.config.run {
copy(eventListeners = eventListeners + EventListener { event ->
if (event is HeapAnalysisDone<*>) {
latch.countDown()
}
})
}
val hprof = writeHeapDump {
"Holder" clazz {
staticField["leak"] = "com.example.Leaking" watchedInstance {}
}
}
val intent = Intent(Intent.ACTION_VIEW, Uri.fromFile(hprof))
activityTestRule.launchActivity(intent)
require(latch.await(5, SECONDS))
onView(withText("1 Heap Dump")).check(matches(isDisplayed()))
onView(withText("1 Distinct Leak")).perform(click())
onView(withText("Holder.leak")).check(matches(isDisplayed()))
}

private fun writeHeapDump(block: HprofWriterHelper.() -> Unit): File {
val hprofFile = testFolder.newFile("temp.hprof")
hprofFile.dump {
"android.os.Build" clazz {
staticField["MANUFACTURER"] = string("Samsing")
}
"android.os.Build\$VERSION" clazz {
staticField["SDK_INT"] = IntHolder(47)
}
block()
}
return hprofFile
}

private fun tryAndRestoreConfig(block: () -> Unit) {
val original = LeakCanary.config
try {
block()
} finally {
LeakCanary.config = original
}
}

}