diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index e360263a..0234b8d7 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -60,4 +60,19 @@ object Samples { const val compiler = "androidx.room:room-compiler:${version}" const val rxjava = "androidx.room:room-rxjava2:${version}" } + + object OkHttp { + private const val version = "4.9.3" + const val okhttp = "com.squareup.okhttp3:okhttp:${version}" + } + + object Timber { + private const val version = "5.0.1" + const val timber = "com.jakewharton.timber:timber:${version}" + } + + object Fragment { + private const val version = "1.3.5" + const val fragmentKtx = "androidx.fragment:fragment-ktx:${version}" + } } diff --git a/examples/android-instrumentation-sample/build.gradle.kts b/examples/android-instrumentation-sample/build.gradle.kts index 8830b973..46945177 100644 --- a/examples/android-instrumentation-sample/build.gradle.kts +++ b/examples/android-instrumentation-sample/build.gradle.kts @@ -50,8 +50,6 @@ android { // } dependencies { - implementation(Libs.SENTRY_ANDROID) - implementation(Samples.AndroidX.recyclerView) implementation(Samples.AndroidX.lifecycle) implementation(Samples.AndroidX.appcompat) @@ -63,6 +61,8 @@ dependencies { implementation(Samples.Room.ktx) implementation(Samples.Room.rxjava) + implementation(Samples.Timber.timber) + implementation(Samples.Fragment.fragmentKtx) implementation(project(":examples:android-room-lib")) kapt(Samples.Room.compiler) diff --git a/examples/android-room-lib/build.gradle.kts b/examples/android-room-lib/build.gradle.kts index 43d1f584..a7168ada 100644 --- a/examples/android-room-lib/build.gradle.kts +++ b/examples/android-room-lib/build.gradle.kts @@ -21,4 +21,8 @@ dependencies { implementation(Samples.Room.runtime) implementation(Samples.Room.ktx) + + // this is here for test purposes, to ensure that transitive dependencies are also recognized + // by our auto-installation + implementation(Samples.OkHttp.okhttp) } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt index b63689a6..9b56c06b 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt @@ -16,6 +16,8 @@ import io.sentry.android.gradle.SentryTasksProvider.getPackageBundleTask import io.sentry.android.gradle.SentryTasksProvider.getPackageProvider import io.sentry.android.gradle.SentryTasksProvider.getPreBundleTask import io.sentry.android.gradle.SentryTasksProvider.getTransformerTask +import io.sentry.android.gradle.autoinstall.installDependencies +import io.sentry.android.gradle.extensions.SentryPluginExtension import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory import io.sentry.android.gradle.services.SentrySdkStateHolder import io.sentry.android.gradle.tasks.SentryGenerateProguardUuidTask @@ -290,6 +292,8 @@ class SentryPlugin : Plugin { project.logger.info { "uploadSentryNativeSymbols won't be executed" } } } + + project.installDependencies(extension) } } @@ -307,6 +311,7 @@ class SentryPlugin : Plugin { companion object { const val SENTRY_ORG_PARAMETER = "sentryOrg" const val SENTRY_PROJECT_PARAMETER = "sentryProject" + internal const val SENTRY_SDK_VERSION = "5.6.1" internal val sep = File.separator diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AbstractInstallStrategy.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AbstractInstallStrategy.kt new file mode 100644 index 00000000..06e6101d --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AbstractInstallStrategy.kt @@ -0,0 +1,48 @@ +package io.sentry.android.gradle.autoinstall + +import io.sentry.android.gradle.util.SemVer +import io.sentry.android.gradle.util.info +import io.sentry.android.gradle.util.warn +import org.gradle.api.artifacts.ComponentMetadataContext +import org.gradle.api.artifacts.ComponentMetadataRule +import org.slf4j.Logger + +abstract class AbstractInstallStrategy : ComponentMetadataRule { + + protected lateinit var logger: Logger + + protected abstract val moduleId: String + + protected abstract val shouldInstallModule: Boolean + + protected open val minSupportedVersion: SemVer = SemVer(0, 0, 0) + + override fun execute(context: ComponentMetadataContext) { + val autoInstallState = AutoInstallState.getInstance() + if (!shouldInstallModule) { + logger.info { + "$moduleId won't be installed because it was already installed directly" + } + return + } + val semVer = SemVer.parse(context.details.id.version) + if (semVer < minSupportedVersion) { + logger.warn { + "$moduleId won't be installed because the current version is " + + "lower than the minimum supported version ($minSupportedVersion)" + } + return + } + + context.details.allVariants { metadata -> + metadata.withDependencies { dependencies -> + val sentryVersion = autoInstallState.sentryVersion + dependencies.add("$SENTRY_GROUP:$moduleId:$sentryVersion") + + logger.info { + "$moduleId was successfully installed with version: $sentryVersion" + } + } + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstall.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstall.kt new file mode 100644 index 00000000..0119a959 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstall.kt @@ -0,0 +1,75 @@ +package io.sentry.android.gradle.autoinstall + +import io.sentry.android.gradle.autoinstall.fragment.FragmentInstallStrategy +import io.sentry.android.gradle.autoinstall.fragment.FragmentInstallStrategy.Registrar.SENTRY_FRAGMENT_ID +import io.sentry.android.gradle.autoinstall.okhttp.OkHttpInstallStrategy +import io.sentry.android.gradle.autoinstall.okhttp.OkHttpInstallStrategy.Registrar.SENTRY_OKHTTP_ID +import io.sentry.android.gradle.autoinstall.timber.TimberInstallStrategy +import io.sentry.android.gradle.autoinstall.timber.TimberInstallStrategy.Registrar.SENTRY_TIMBER_ID +import io.sentry.android.gradle.extensions.SentryPluginExtension +import io.sentry.android.gradle.util.info +import org.gradle.api.Project +import org.gradle.api.artifacts.DependencySet + +internal const val SENTRY_GROUP = "io.sentry" +private const val SENTRY_ANDROID_ID = "sentry-android" +private const val SENTRY_ANDROID_CORE_ID = "sentry-android-core" + +private val strategies = listOf( + OkHttpInstallStrategy.Registrar, + TimberInstallStrategy.Registrar, + FragmentInstallStrategy.Registrar +) + +fun Project.installDependencies(extension: SentryPluginExtension) { + configurations.named("implementation").configure { configuration -> + configuration.withDependencies { dependencies -> + // if autoInstallation is disabled, the autoInstallState will contain initial values + // which all default to false, hence, the integrations won't be installed as well + if (extension.autoInstallation.enabled.get()) { + val sentryVersion = dependencies.findSentryAndroidVersion() + with(AutoInstallState.getInstance(gradle)) { + this.sentryVersion = installSentrySdk(sentryVersion, dependencies, extension) + + installOkHttp = !dependencies.isModuleAvailable(SENTRY_OKHTTP_ID) + installTimber = !dependencies.isModuleAvailable(SENTRY_TIMBER_ID) + installFragment = !dependencies.isModuleAvailable(SENTRY_FRAGMENT_ID) + } + } + } + } + project.dependencies.components { component -> + strategies.forEach { it.register(component) } + } +} + +private fun Project.installSentrySdk( + sentryVersion: String?, + dependencies: DependencySet, + extension: SentryPluginExtension +): String { + return if (sentryVersion == null) { + val userDefinedVersion = extension.autoInstallation.sentryVersion.get() + val sentryAndroidDep = + this.dependencies.create("$SENTRY_GROUP:$SENTRY_ANDROID_ID:$userDefinedVersion") + dependencies.add(sentryAndroidDep) + logger.info { + "sentry-android was successfully installed with version: $userDefinedVersion" + } + userDefinedVersion + } else { + logger.info { + "sentry-android won't be installed because it was already installed directly" + } + sentryVersion + } +} + +private fun DependencySet.findSentryAndroidVersion(): String? = + find { + it.group == SENTRY_GROUP && + (it.name == SENTRY_ANDROID_ID || it.name == SENTRY_ANDROID_CORE_ID) + }?.version + +private fun DependencySet.isModuleAvailable(id: String): Boolean = + any { it.group == SENTRY_GROUP && it.name == id } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstallState.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstallState.kt new file mode 100644 index 00000000..123fbb88 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstallState.kt @@ -0,0 +1,77 @@ +package io.sentry.android.gradle.autoinstall + +import io.sentry.android.gradle.SentryPlugin.Companion.SENTRY_SDK_VERSION +import java.io.Serializable +import org.gradle.api.invocation.Gradle + +class AutoInstallState private constructor() : Serializable { + + @get:Synchronized + @set:Synchronized + var sentryVersion: String = SENTRY_SDK_VERSION + + @get:Synchronized + @set:Synchronized + var installOkHttp: Boolean = false + + @get:Synchronized + @set:Synchronized + var installFragment: Boolean = false + + @get:Synchronized + @set:Synchronized + var installTimber: Boolean = false + + override fun toString(): String { + return "AutoInstallState(sentryVersion='$sentryVersion', " + + "installOkHttp=$installOkHttp, " + + "installFragment=$installFragment, " + + "installTimber=$installTimber)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AutoInstallState + + if (sentryVersion != other.sentryVersion) return false + if (installOkHttp != other.installOkHttp) return false + if (installFragment != other.installFragment) return false + if (installTimber != other.installTimber) return false + + return true + } + + override fun hashCode(): Int { + var result = sentryVersion.hashCode() + result = 31 * result + installOkHttp.hashCode() + result = 31 * result + installFragment.hashCode() + result = 31 * result + installTimber.hashCode() + return result + } + + // We can't use Kotlin object because we need new instance on each Gradle rebuild + // But if we're inside Gradle daemon, Kotlin object will be shared between builds + companion object { + @field:Volatile + private var instance: AutoInstallState? = null + + @JvmStatic + @Synchronized + fun getInstance(gradle: Gradle? = null): AutoInstallState { + if (instance != null) { + return instance!! + } + + val state = AutoInstallState() + instance = state + + if (gradle != null) { + BuildFinishedListenerService.getInstance(gradle).onClose { instance = null } + } + + return state + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/BuildFinishedListenerService.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/BuildFinishedListenerService.kt new file mode 100644 index 00000000..423052c5 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/BuildFinishedListenerService.kt @@ -0,0 +1,35 @@ +@file:Suppress("UnstableApiUsage") + +package io.sentry.android.gradle.autoinstall + +import io.sentry.android.gradle.util.getBuildServiceName +import org.gradle.api.invocation.Gradle +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +abstract class BuildFinishedListenerService : + BuildService, + AutoCloseable { + + private val actionsOnClose = mutableListOf<() -> Unit>() + + fun onClose(action: () -> Unit) { + actionsOnClose.add(action) + } + + override fun close() { + for (action in actionsOnClose) { + action() + } + actionsOnClose.clear() + } + + companion object { + fun getInstance(gradle: Gradle): BuildFinishedListenerService { + return gradle.sharedServices.registerIfAbsent( + getBuildServiceName(BuildFinishedListenerService::class.java), + BuildFinishedListenerService::class.java + ) {}.get() + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/InstallStrategyRegistrar.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/InstallStrategyRegistrar.kt new file mode 100644 index 00000000..63863322 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/InstallStrategyRegistrar.kt @@ -0,0 +1,7 @@ +package io.sentry.android.gradle.autoinstall + +import org.gradle.api.artifacts.dsl.ComponentMetadataHandler + +interface InstallStrategyRegistrar { + fun register(component: ComponentMetadataHandler) +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategy.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategy.kt new file mode 100644 index 00000000..122d4c82 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategy.kt @@ -0,0 +1,38 @@ +package io.sentry.android.gradle.autoinstall.fragment + +import io.sentry.android.gradle.SentryPlugin +import io.sentry.android.gradle.autoinstall.AbstractInstallStrategy +import io.sentry.android.gradle.autoinstall.AutoInstallState +import io.sentry.android.gradle.autoinstall.InstallStrategyRegistrar +import javax.inject.Inject +import org.gradle.api.artifacts.dsl.ComponentMetadataHandler +import org.slf4j.Logger + +// @CacheableRule // TODO: make it cacheable somehow (probably depends on parameters) +abstract class FragmentInstallStrategy : AbstractInstallStrategy { + + constructor(logger: Logger) : super() { + this.logger = logger + } + + @Suppress("unused") // used by Gradle + @Inject // inject is needed to avoid Gradle error + constructor() : this(SentryPlugin.logger) + + override val moduleId: String get() = SENTRY_FRAGMENT_ID + + override val shouldInstallModule: Boolean get() = AutoInstallState.getInstance().installFragment + + companion object Registrar : InstallStrategyRegistrar { + private const val FRAGMENT_GROUP = "androidx.fragment" + private const val FRAGMENT_ID = "fragment" + internal const val SENTRY_FRAGMENT_ID = "sentry-android-fragment" + + override fun register(component: ComponentMetadataHandler) { + component.withModule( + "$FRAGMENT_GROUP:$FRAGMENT_ID", + FragmentInstallStrategy::class.java + ) {} + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategy.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategy.kt new file mode 100644 index 00000000..87c98be3 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategy.kt @@ -0,0 +1,43 @@ +package io.sentry.android.gradle.autoinstall.okhttp + +import io.sentry.android.gradle.SentryPlugin +import io.sentry.android.gradle.autoinstall.AbstractInstallStrategy +import io.sentry.android.gradle.autoinstall.AutoInstallState +import io.sentry.android.gradle.autoinstall.InstallStrategyRegistrar +import io.sentry.android.gradle.util.SemVer +import javax.inject.Inject +import org.gradle.api.artifacts.dsl.ComponentMetadataHandler +import org.slf4j.Logger + +// @CacheableRule +abstract class OkHttpInstallStrategy : AbstractInstallStrategy { + + constructor(logger: Logger) : super() { + this.logger = logger + } + + @Suppress("unused") // used by Gradle + @Inject // inject is needed to avoid Gradle error + constructor() : this(SentryPlugin.logger) + + override val moduleId: String get() = SENTRY_OKHTTP_ID + + override val shouldInstallModule: Boolean get() = AutoInstallState.getInstance().installOkHttp + + override val minSupportedVersion: SemVer get() = MIN_SUPPORTED_VERSION + + companion object Registrar : InstallStrategyRegistrar { + private const val OKHTTP_GROUP = "com.squareup.okhttp3" + private const val OKHTTP_ID = "okhttp" + internal const val SENTRY_OKHTTP_ID = "sentry-android-okhttp" + + private val MIN_SUPPORTED_VERSION = SemVer(3, 13, 0) + + override fun register(component: ComponentMetadataHandler) { + component.withModule( + "$OKHTTP_GROUP:$OKHTTP_ID", + OkHttpInstallStrategy::class.java + ) {} + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategy.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategy.kt new file mode 100644 index 00000000..12ea60a5 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategy.kt @@ -0,0 +1,42 @@ +package io.sentry.android.gradle.autoinstall.timber + +import io.sentry.android.gradle.SentryPlugin +import io.sentry.android.gradle.autoinstall.AbstractInstallStrategy +import io.sentry.android.gradle.autoinstall.AutoInstallState +import io.sentry.android.gradle.autoinstall.InstallStrategyRegistrar +import io.sentry.android.gradle.util.SemVer +import javax.inject.Inject +import org.gradle.api.artifacts.dsl.ComponentMetadataHandler +import org.slf4j.Logger + +// @CacheableRule +abstract class TimberInstallStrategy : AbstractInstallStrategy { + + constructor(logger: Logger) : super() { + this.logger = logger + } + + @Suppress("unused") // used by Gradle + @Inject // inject is needed to avoid Gradle error + constructor() : this(SentryPlugin.logger) + + override val moduleId: String get() = SENTRY_TIMBER_ID + + override val shouldInstallModule: Boolean get() = AutoInstallState.getInstance().installTimber + + override val minSupportedVersion: SemVer get() = MIN_SUPPORTED_VERSION + + companion object Registrar : InstallStrategyRegistrar { + private const val TIMBER_GROUP = "com.jakewharton.timber" + private const val TIMBER_ID = "timber" + internal const val SENTRY_TIMBER_ID = "sentry-android-timber" + private val MIN_SUPPORTED_VERSION = SemVer(4, 6, 0) + + override fun register(component: ComponentMetadataHandler) { + component.withModule( + "$TIMBER_GROUP:$TIMBER_ID", + TimberInstallStrategy::class.java + ) {} + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/AutoInstallExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/AutoInstallExtension.kt new file mode 100644 index 00000000..ccb79fc1 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/AutoInstallExtension.kt @@ -0,0 +1,23 @@ +package io.sentry.android.gradle.extensions + +import io.sentry.android.gradle.SentryPlugin.Companion.SENTRY_SDK_VERSION +import javax.inject.Inject +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +open class AutoInstallExtension @Inject constructor(objects: ObjectFactory) { + /** + * Enable auto-installation of Sentry components (sentry-android SDK and okhttp, timber and + * fragment integrations). + * Defaults to true. + */ + val enabled: Property = objects.property(Boolean::class.java) + .convention(true) + + /** + * Overrides default (bundled with plugin) or inherited (from user's buildscript) sentry version. + * Defaults to the latest published sentry version. + */ + val sentryVersion: Property = objects.property(String::class.java) + .convention(SENTRY_SDK_VERSION) +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPluginExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt similarity index 91% rename from plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPluginExtension.kt rename to plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt index 92dd4516..0a662187 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPluginExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt @@ -1,4 +1,4 @@ -package io.sentry.android.gradle +package io.sentry.android.gradle.extensions import javax.inject.Inject import org.gradle.api.Action @@ -106,4 +106,17 @@ abstract class SentryPluginExtension @Inject constructor(project: Project) { ) { tracingInstrumentationAction.execute(tracingInstrumentation) } + + val autoInstallation: AutoInstallExtension = objects.newInstance( + AutoInstallExtension::class.java + ) + + /** + * Configure the auto installation feature. + */ + fun autoInstallation( + autoInstallationAction: Action + ) { + autoInstallationAction.execute(autoInstallation) + } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/TracingInstrumentationExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt similarity index 98% rename from plugin-build/src/main/kotlin/io/sentry/android/gradle/TracingInstrumentationExtension.kt rename to plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt index 0027b6a8..ecc4a78d 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/TracingInstrumentationExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt @@ -1,4 +1,4 @@ -package io.sentry.android.gradle +package io.sentry.android.gradle.extensions import javax.inject.Inject import org.gradle.api.model.ObjectFactory diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt index 59664f2f..12c8ba20 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt @@ -4,8 +4,8 @@ import com.android.build.api.instrumentation.AsmClassVisitorFactory import com.android.build.api.instrumentation.ClassContext import com.android.build.api.instrumentation.ClassData import com.android.build.api.instrumentation.InstrumentationParameters -import io.sentry.android.gradle.InstrumentationFeature import io.sentry.android.gradle.SentryPlugin +import io.sentry.android.gradle.extensions.InstrumentationFeature import io.sentry.android.gradle.instrumentation.androidx.room.AndroidXRoomDao import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.AndroidXSQLiteDatabase import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginAutoInstallTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginAutoInstallTest.kt new file mode 100644 index 00000000..d1b6b471 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginAutoInstallTest.kt @@ -0,0 +1,138 @@ +package io.sentry.android.gradle + +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class SentryPluginAutoInstallTest( + androidGradlePluginVersion: String, + gradleVersion: String +) : BaseSentryPluginTest(androidGradlePluginVersion, gradleVersion) { + + @Test + fun `adds sentry-android dependency`() { + appBuildFile.writeText( + // language=Groovy + """ + plugins { + id "com.android.application" + id "io.sentry.android.gradle" + } + + sentry.includeProguardMapping = false + """.trimIndent() + ) + + val result = runner + .appendArguments("app:dependencies") + .appendArguments("--configuration") + .appendArguments("debugRuntimeClasspath") + .build() + assertTrue { + "io.sentry:sentry-android:5.6.1" in result.output + } + } + + @Test + fun `adds integrations`() { + appBuildFile.writeText( + // language=Groovy + """ + plugins { + id "com.android.application" + id "io.sentry.android.gradle" + } + + dependencies { + // sentry-android shouldn't be installed, since sentry-android-core is present + implementation 'io.sentry:sentry-android-core:5.1.0' + implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'androidx.fragment:fragment:1.3.5' + // our plugin shouldn't install okhttp, since it's a direct dep + implementation 'io.sentry:sentry-android-okhttp:5.4.0' + } + sentry.includeProguardMapping = false + """.trimIndent() + ) + + val result = runner + .appendArguments("app:dependencies") + .appendArguments("--configuration") + .appendArguments("debugRuntimeClasspath") + .build() + assertFalse { "io.sentry:sentry-android:5.1.0" in result.output } + assertTrue { "io.sentry:sentry-android-timber:5.1.0" in result.output } + assertTrue { "io.sentry:sentry-android-fragment:5.1.0" in result.output } + assertFalse { "io.sentry:sentry-android-okhttp:5.1.0" in result.output } + assertTrue { "io.sentry:sentry-android-okhttp:5.4.0" in result.output } + } + + @Test + fun `does not do anything when autoinstall is disabled`() { + appBuildFile.writeText( + // language=Groovy + """ + plugins { + id "com.android.application" + id "io.sentry.android.gradle" + } + + dependencies { + implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'androidx.fragment:fragment:1.3.5' + implementation 'com.squareup.okhttp3:okhttp:4.9.2' + } + + sentry.autoInstallation.enabled = false + sentry.includeProguardMapping = false + """.trimIndent() + ) + + val result = runner + .appendArguments("app:dependencies") + .appendArguments("--configuration") + .appendArguments("debugRuntimeClasspath") + .build() + print(result.output) + assertFalse { "io.sentry:sentry-android:5.6.1" in result.output } + assertFalse { "io.sentry:sentry-android-timber:5.6.1" in result.output } + assertFalse { "io.sentry:sentry-android-fragment:5.6.1" in result.output } + assertFalse { "io.sentry:sentry-android-okhttp:5.6.1" in result.output } + } + + @Test + fun `uses user-provided sentryVersion when sentry-android is not available in direct deps`() { + appBuildFile.writeText( + // language=Groovy + """ + plugins { + id "com.android.application" + id "io.sentry.android.gradle" + } + + dependencies { + implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'com.squareup.okhttp3:okhttp:4.9.2' + // the fragment integration should stay as it is, the version shouldn't be overridden + implementation 'io.sentry:sentry-android-fragment:5.4.0' + } + + sentry.autoInstallation.sentryVersion = "5.1.2" + sentry.includeProguardMapping = false + """.trimIndent() + ) + + val result = runner + .appendArguments("app:dependencies") + .appendArguments("--configuration") + .appendArguments("debugRuntimeClasspath") + .build() + assertTrue { "io.sentry:sentry-android:5.1.2" in result.output } + assertTrue { "io.sentry:sentry-android-timber:5.1.2" in result.output } + assertTrue { "io.sentry:sentry-android-okhttp:5.1.2" in result.output } + assertTrue { "io.sentry:sentry-android-fragment:5.4.0" in result.output } + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginMRJarTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginMRJarTest.kt index f243ba43..e8ab0174 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginMRJarTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginMRJarTest.kt @@ -2,14 +2,9 @@ package io.sentry.android.gradle import kotlin.test.assertTrue import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -class SentryPluginMRJarTest( - androidGradlePluginVersion: String, - gradleVersion: String -) : BaseSentryPluginTest(androidGradlePluginVersion, gradleVersion) { + +class SentryPluginMRJarTest : + BaseSentryPluginTest(androidGradlePluginVersion = "7.0.4", gradleVersion = "7.1.1") { @Test fun `does not break when there is a MR-JAR dependency with unsupported java version`() { diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginTest.kt index c6502e66..df292297 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginTest.kt @@ -1,5 +1,6 @@ package io.sentry.android.gradle +import io.sentry.android.gradle.extensions.InstrumentationFeature import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertTrue diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategyTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategyTest.kt new file mode 100644 index 00000000..b3618711 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategyTest.kt @@ -0,0 +1,88 @@ +package io.sentry.android.gradle.autoinstall.fragment + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.doAnswer +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.android.gradle.autoinstall.AutoInstallState +import io.sentry.android.gradle.instrumentation.fakes.CapturingTestLogger +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.gradle.api.Action +import org.gradle.api.artifacts.ComponentMetadataContext +import org.gradle.api.artifacts.ComponentMetadataDetails +import org.gradle.api.artifacts.DirectDependenciesMetadata +import org.gradle.api.artifacts.ModuleVersionIdentifier +import org.gradle.api.artifacts.VariantMetadata +import org.junit.Test +import org.slf4j.Logger + +class FragmentInstallStrategyTest { + class Fixture { + val logger = CapturingTestLogger() + val dependencies = mock() + val metadataDetails = mock() + val metadataContext = mock { + whenever(it.details).thenReturn(metadataDetails) + val metadata = mock() + doAnswer { + (it.arguments[0] as Action).execute(dependencies) + }.whenever(metadata).withDependencies(any>()) + + doAnswer { + // trigger the callback registered in tests + (it.arguments[0] as Action).execute(metadata) + }.whenever(metadataDetails).allVariants(any>()) + } + + fun getSut(installFragment: Boolean = true): FragmentInstallStrategy { + val id = mock { + whenever(it.version).doReturn("1.3.5") + } + whenever(metadataDetails.id).thenReturn(id) + + with(AutoInstallState.getInstance()) { + this.installFragment = installFragment + this.sentryVersion = "5.6.1" + } + return FragmentInstallStrategyImpl(logger) + } + } + + private val fixture = Fixture() + + @Test + fun `when sentry-android-fragment is a direct dependency logs a message and does nothing`() { + val sut = fixture.getSut(installFragment = false) + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-android-fragment won't be installed because it was already " + + "installed directly" + } + verify(fixture.metadataContext, never()).details + } + + @Test + fun `installs sentry-android-fragment with info message`() { + val sut = fixture.getSut() + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-android-fragment was successfully installed with version: 5.6.1" + } + verify(fixture.dependencies).add( + check { + assertEquals("io.sentry:sentry-android-fragment:5.6.1", it) + } + ) + } + + private class FragmentInstallStrategyImpl(logger: Logger) : FragmentInstallStrategy(logger) +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategyTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategyTest.kt new file mode 100644 index 00000000..9bcd1467 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategyTest.kt @@ -0,0 +1,104 @@ +package io.sentry.android.gradle.autoinstall.okhttp + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.doAnswer +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.android.gradle.autoinstall.AutoInstallState +import io.sentry.android.gradle.instrumentation.fakes.CapturingTestLogger +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.gradle.api.Action +import org.gradle.api.artifacts.ComponentMetadataContext +import org.gradle.api.artifacts.ComponentMetadataDetails +import org.gradle.api.artifacts.DirectDependenciesMetadata +import org.gradle.api.artifacts.ModuleVersionIdentifier +import org.gradle.api.artifacts.VariantMetadata +import org.junit.Test +import org.slf4j.Logger + +class OkHttpInstallStrategyTest { + class Fixture { + val logger = CapturingTestLogger() + val dependencies = mock() + val metadataDetails = mock() + val metadataContext = mock { + whenever(it.details).thenReturn(metadataDetails) + val metadata = mock() + doAnswer { + (it.arguments[0] as Action).execute(dependencies) + }.whenever(metadata).withDependencies(any>()) + + doAnswer { + // trigger the callback registered in tests + (it.arguments[0] as Action).execute(metadata) + }.whenever(metadataDetails).allVariants(any>()) + } + + fun getSut( + installOkHttp: Boolean = true, + okHttpVersion: String = "4.9.3" + ): OkHttpInstallStrategy { + val id = mock { + whenever(it.version).doReturn(okHttpVersion) + } + whenever(metadataDetails.id).thenReturn(id) + + with(AutoInstallState.getInstance()) { + this.installOkHttp = installOkHttp + this.sentryVersion = "5.6.1" + } + return OkHttpInstallStrategyImpl(logger) + } + } + + private val fixture = Fixture() + + @Test + fun `when sentry-android-okhttp is a direct dependency logs a message and does nothing`() { + val sut = fixture.getSut(installOkHttp = false) + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-android-okhttp won't be installed because it was already " + + "installed directly" + } + verify(fixture.metadataContext, never()).details + } + + @Test + fun `when okhttp version is unsupported logs a message and does nothing`() { + val sut = fixture.getSut(okHttpVersion = "3.11.0") + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-android-okhttp won't be installed because the current " + + "version is lower than the minimum supported version (3.13.0)" + } + verify(fixture.metadataDetails, never()).allVariants(any()) + } + + @Test + fun `installs sentry-android-okhttp with info message`() { + val sut = fixture.getSut() + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-android-okhttp was successfully installed with version: 5.6.1" + } + verify(fixture.dependencies).add( + check { + assertEquals("io.sentry:sentry-android-okhttp:5.6.1", it) + } + ) + } + + private class OkHttpInstallStrategyImpl(logger: Logger) : OkHttpInstallStrategy(logger) +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategyTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategyTest.kt new file mode 100644 index 00000000..d3b10f50 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategyTest.kt @@ -0,0 +1,104 @@ +package io.sentry.android.gradle.autoinstall.timber + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.doAnswer +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.android.gradle.autoinstall.AutoInstallState +import io.sentry.android.gradle.instrumentation.fakes.CapturingTestLogger +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.gradle.api.Action +import org.gradle.api.artifacts.ComponentMetadataContext +import org.gradle.api.artifacts.ComponentMetadataDetails +import org.gradle.api.artifacts.DirectDependenciesMetadata +import org.gradle.api.artifacts.ModuleVersionIdentifier +import org.gradle.api.artifacts.VariantMetadata +import org.junit.Test +import org.slf4j.Logger + +class TimberInstallStrategyTest { + class Fixture { + val logger = CapturingTestLogger() + val dependencies = mock() + val metadataDetails = mock() + val metadataContext = mock { + whenever(it.details).thenReturn(metadataDetails) + val metadata = mock() + doAnswer { + (it.arguments[0] as Action).execute(dependencies) + }.whenever(metadata).withDependencies(any>()) + + doAnswer { + // trigger the callback registered in tests + (it.arguments[0] as Action).execute(metadata) + }.whenever(metadataDetails).allVariants(any>()) + } + + fun getSut( + installTimber: Boolean = true, + timberVersion: String = "4.7.1" + ): TimberInstallStrategy { + val id = mock { + whenever(it.version).doReturn(timberVersion) + } + whenever(metadataDetails.id).thenReturn(id) + + with(AutoInstallState.getInstance()) { + this.installTimber = installTimber + this.sentryVersion = "5.6.1" + } + return TimberInstallStrategyImpl(logger) + } + } + + private val fixture = Fixture() + + @Test + fun `when sentry-android-timber is a direct dependency logs a message and does nothing`() { + val sut = fixture.getSut(installTimber = false) + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-android-timber won't be installed because it was already " + + "installed directly" + } + verify(fixture.metadataContext, never()).details + } + + @Test + fun `when timber version is unsupported logs a message and does nothing`() { + val sut = fixture.getSut(timberVersion = "4.5.0") + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-android-timber won't be installed because the current " + + "version is lower than the minimum supported version (4.6.0)" + } + verify(fixture.metadataDetails, never()).allVariants(any()) + } + + @Test + fun `installs sentry-android-timber with info message`() { + val sut = fixture.getSut() + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-android-timber was successfully installed with version: 5.6.1" + } + verify(fixture.dependencies).add( + check { + assertEquals("io.sentry:sentry-android-timber:5.6.1", it) + } + ) + } + + private class TimberInstallStrategyImpl(logger: Logger) : TimberInstallStrategy(logger) +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt index ac2e5ef3..4e9f2381 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt @@ -1,6 +1,6 @@ package io.sentry.android.gradle.instrumentation.fakes -import io.sentry.android.gradle.InstrumentationFeature +import io.sentry.android.gradle.extensions.InstrumentationFeature import io.sentry.android.gradle.instrumentation.ClassInstrumentable import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory import io.sentry.android.gradle.services.SentrySdkStateHolder diff --git a/scripts/benchmark/duckduckgo/add-sentry-to-duckduckgo.patch b/scripts/benchmark/duckduckgo/add-sentry-to-duckduckgo.patch index f49ae227..2491dc60 100644 --- a/scripts/benchmark/duckduckgo/add-sentry-to-duckduckgo.patch +++ b/scripts/benchmark/duckduckgo/add-sentry-to-duckduckgo.patch @@ -59,6 +59,16 @@ index 5773597d..288a82b1 100644 org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+UseParallelGC org.gradle.caching=true +diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties +index 35022c85..4f3e5389 100644 +--- a/gradle/wrapper/gradle-wrapper.properties ++++ b/gradle/wrapper/gradle-wrapper.properties +@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME + distributionPath=wrapper/dists + zipStoreBase=GRADLE_USER_HOME + zipStorePath=wrapper/dists +-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip ++distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip diff --git a/settings.gradle b/settings.gradle index 98cd4fc7..0b3e76d7 100644 --- a/settings.gradle