From 83eb3c13b2adb90ebbc5ee56883dcdc714120eac Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 13 May 2024 02:42:28 +0530 Subject: [PATCH] feat(android): add a heap growth detection test --- android/build.gradle.kts | 9 +++ android/src/androidTest/AndroidManifest.xml | 13 ++++ .../msfjarvis/claw/android/HeapGrowthCheck.kt | 59 +++++++++++++++++++ gradle/libs.versions.toml | 4 ++ 4 files changed, 85 insertions(+) create mode 100644 android/src/androidTest/AndroidManifest.xml create mode 100644 android/src/androidTest/kotlin/dev/msfjarvis/claw/android/HeapGrowthCheck.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 17bf544b4..2f1951336 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -6,6 +6,8 @@ */ @file:Suppress("UnstableApiUsage") +import dev.msfjarvis.claw.gradle.addTestDependencies + plugins { id("dev.msfjarvis.claw.android-application") id("dev.msfjarvis.claw.rename-artifacts") @@ -14,6 +16,7 @@ plugins { id("dev.msfjarvis.claw.sentry") id("dev.msfjarvis.claw.versioning-plugin") alias(libs.plugins.aboutlibraries) + alias(libs.plugins.android.junit5) alias(libs.plugins.anvil) alias(libs.plugins.modulegraphassert) alias(libs.plugins.whetstone) @@ -25,6 +28,7 @@ plugins { android { namespace = "dev.msfjarvis.claw.android" defaultConfig.applicationId = "dev.msfjarvis.claw.android" + defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildFeatures.compose = true composeOptions { useLiveLiterals = false @@ -113,4 +117,9 @@ dependencies { implementation(projects.web) kapt(libs.dagger.compiler) + + addTestDependencies(project) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.test.uiautomator) + androidTestImplementation(libs.leakcanary.android.test) } diff --git a/android/src/androidTest/AndroidManifest.xml b/android/src/androidTest/AndroidManifest.xml new file mode 100644 index 000000000..0da99d934 --- /dev/null +++ b/android/src/androidTest/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/android/src/androidTest/kotlin/dev/msfjarvis/claw/android/HeapGrowthCheck.kt b/android/src/androidTest/kotlin/dev/msfjarvis/claw/android/HeapGrowthCheck.kt new file mode 100644 index 000000000..338f72540 --- /dev/null +++ b/android/src/androidTest/kotlin/dev/msfjarvis/claw/android/HeapGrowthCheck.kt @@ -0,0 +1,59 @@ +/* + * Copyright © 2024 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.android + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.google.common.truth.Truth.assertThat +import de.mannodermaus.junit5.ActivityScenarioExtension +import leakcanary.repeatingAndroidInProcessScenario +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import shark.ObjectGrowthDetector +import shark.forAndroidHeap + +class HeapGrowthCheck { + @JvmField + @RegisterExtension + val scenarioExtension = ActivityScenarioExtension.launch() + private val detector = ObjectGrowthDetector.forAndroidHeap().repeatingAndroidInProcessScenario() + private lateinit var device: UiDevice + + @BeforeEach + fun setUp() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + device = UiDevice.getInstance(instrumentation) + } + + @Test + fun verify_heap_growth() { + val heapGrowth = detector.findRepeatedlyGrowingObjects { device.exploreScreens() } + assertThat(heapGrowth.growingObjects).isEmpty() + } + + private companion object { + fun UiDevice.exploreScreens() { + listOf("HOTTEST", "NEWEST", "SAVED").forEach { tag -> + waitForObject(By.res(tag)).click() + waitForIdle() + } + } + + private fun UiDevice.waitForObject(selector: BySelector, timeout: Long = 10_000L): UiObject2 { + if (wait(Until.hasObject(selector), timeout)) { + return findObject(selector) + } + + error("Object with selector [$selector] not found") + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a2a93a73..142d04a8f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] aboutLibraries = "11.1.4" agp = "8.5.0-beta01" +android-junit5 = "1.10.0.0" benchmark = "1.3.0-alpha04" coil = "2.6.0" # @keep used for kotlinCompilerExtensionVersion @@ -12,6 +13,7 @@ junit = "5.10.2" konvert = "3.2.0" kotlin = "1.9.24" kotlinResult = "2.0.0" +leakcanary = "3.0-alpha-4" lifecycle = "2.8.0-rc01" retrofit = "2.11.0" richtext = "1.0.0-alpha01" @@ -89,6 +91,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } +leakcanary-android-test = { module = "com.squareup.leakcanary:leakcanary-android-test", version.ref = "leakcanary" } material3-pulltorefresh = "eu.bambooapps:compose-material3-pullrefresh:1.1.1" napier = "io.github.aakira:napier:2.7.1" okhttp-bom = "com.squareup.okhttp3:okhttp-bom:4.12.0" @@ -114,6 +117,7 @@ unfurl = "me.saket.unfurl:unfurl:1.7.0" [plugins] aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" } android-lint = { id = "com.android.lint", version.ref = "agp" } +android-junit5 = { id = "de.mannodermaus.android-junit5", version.ref = "android-junit5" } android-test = { id = "com.android.test", version.ref = "agp" } anvil = "com.squareup.anvil:2.5.0-beta09" baselineprofile = { id = "androidx.baselineprofile", version.ref = "benchmark" }