diff --git a/CHANGELOG.md b/CHANGELOG.md index 907a67aa0..dd0baca53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Features + +- Auto-wrap SQLiteDriver with SentrySQLiteDriver for Room users ([#1285](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1285)) + - Gated on i) `sentry-android-sqlite` >= 8.44.0, ii) `androidx.room:room-runtime` >= 2.7.0-alpha01 or `androidx.room3:room3-runtime` >= 3.0.0-alpha01, and iii) the existing `tracingInstrumentation` `DATABASE` feature + - For users of the `androidx.sqlite.driver.SupportSQLiteDriver` bridge, the `SupportSQLiteOpenHelper` consumed by the bridge continues to be auto-wrapped as before rather than the bridge itself being wrapped + ### Fixes - Resolve the sentry-cli path as a task input instead of memoizing it in a static field, fixing stale-path build failures when switching branches with the configuration cache enabled ([#1264](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1264)) diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt index d51ca26f4..f8f4e633c 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt @@ -75,9 +75,19 @@ open class TracingInstrumentationExtension @Inject constructor(objects: ObjectFa enum class InstrumentationFeature(val integrationName: String) { /** - * When enabled the SDK will create spans for any CRUD operation performed by - * 'androidx.sqlite.db.SupportSQLiteOpenHelper' and 'androidx.room'. This feature uses bytecode - * manipulation. + * When enabled the SDK will create spans for database operations at two levels: + * + * **SQL execution** (`db.sql.query` spans): wraps the low-level driver so each individual SQL + * statement produces a span. Two mutually exclusive paths: + * - `androidx.sqlite.db.SupportSQLiteOpenHelper` via any `SupportSQLiteOpenHelper.Factory` + * (open-helper path) + * - `androidx.sqlite.SQLiteDriver` via `RoomDatabase.Builder.setDriver` (driver path) + * + * **DAO method** (`db.sql.room` spans): wraps each public method on Room's generated `@Dao` + * `_Impl` classes, measuring the full DAO call end-to-end (transaction management, query + * execution, and cursor processing). Pre-Room 2.7 only. + * + * This feature uses bytecode manipulation. */ DATABASE("DatabaseInstrumentation"), 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 fd4699f9c..cc69154cd 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 @@ -10,6 +10,7 @@ import io.sentry.android.gradle.instrumentation.androidx.compose.ComposeNavigati import io.sentry.android.gradle.instrumentation.androidx.room.AndroidXRoomDao import io.sentry.android.gradle.instrumentation.androidx.sqlite.AndroidXSQLiteOpenHelper import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.AndroidXSQLiteDatabase +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.AndroidXSQLiteDriver import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement import io.sentry.android.gradle.instrumentation.appstart.Application import io.sentry.android.gradle.instrumentation.appstart.ContentProvider @@ -90,6 +91,7 @@ abstract class SpanAddingClassVisitorFactory : ChainedInstrumentable( listOfNotNull( AndroidXSQLiteOpenHelper().takeIf { sentryModulesService.isNewDatabaseInstrEnabled() }, + AndroidXSQLiteDriver().takeIf { sentryModulesService.isSQLiteDriverInstrEnabled() }, AndroidXSQLiteDatabase().takeIf { sentryModulesService.isOldDatabaseInstrEnabled() }, AndroidXSQLiteStatement(androidXSqliteFrameWorkVersion).takeIf { sentryModulesService.isOldDatabaseInstrEnabled() diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt new file mode 100644 index 000000000..dd68a9d93 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt @@ -0,0 +1,74 @@ +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver + +import com.android.build.api.instrumentation.ClassContext +import io.sentry.android.gradle.instrumentation.ClassInstrumentable +import io.sentry.android.gradle.instrumentation.CommonClassVisitor +import io.sentry.android.gradle.instrumentation.MethodContext +import io.sentry.android.gradle.instrumentation.MethodInstrumentable +import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor.SetDriverMethodVisitor +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +/** + * Auto-instruments `SQLiteDriver` for all Room users by wrapping the driver parameter in + * `RoomDatabase.Builder.setDriver(SQLiteDriver)`. + * + * Note: As of this writing, SQLDelight doesn't support `SQLiteDriver` + * ([link](https://github.com/sqldelight/sqldelight/issues/6072)), and developers who use the driver + * directly are expected to wrap it themselves. + * + * The SDK protects against duplicate wrappings, allowing the visitor to wrap the driver + * unconditionally. + */ +class AndroidXSQLiteDriver : ClassInstrumentable { + + override fun getVisitor( + instrumentableContext: ClassContext, + apiVersion: Int, + originalVisitor: ClassVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters, + ): ClassVisitor { + val currentClassName = instrumentableContext.currentClassData.className + + return CommonClassVisitor( + apiVersion = apiVersion, + classVisitor = originalVisitor, + className = currentClassName.substringAfterLast('.'), + methodInstrumentables = listOf(SetDriverMethodInstrumentable()), + parameters = parameters, + ) + } + + // Instrument RoomDatabase.Builder.setDriver() in room-runtime and room3 directly. + override fun isInstrumentable(data: ClassContext): Boolean = + data.currentClassData.className in TARGET_CLASSES + + companion object { + + /** Currently covers Room 2 and Room 3 packages. Update as needed. */ + internal val TARGET_CLASSES = + setOf("androidx.room.RoomDatabase\$Builder", "androidx.room3.RoomDatabase\$Builder") + } +} + +class SetDriverMethodInstrumentable : MethodInstrumentable { + + override val fqName: String + get() = SET_DRIVER + + override fun getVisitor( + instrumentableContext: MethodContext, + apiVersion: Int, + originalVisitor: MethodVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters, + ): MethodVisitor = SetDriverMethodVisitor(apiVersion, originalVisitor, instrumentableContext) + + override fun isInstrumentable(data: MethodContext): Boolean = + data.name == SET_DRIVER && data.descriptor?.startsWith(SET_DRIVER_DESCRIPTOR_PREFIX) == true + + companion object { + private const val SET_DRIVER = "setDriver" + private const val SET_DRIVER_DESCRIPTOR_PREFIX = "(Landroidx/sqlite/SQLiteDriver;)" + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt new file mode 100644 index 000000000..3926f1f3a --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt @@ -0,0 +1,34 @@ +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor + +import io.sentry.android.gradle.instrumentation.MethodContext +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Type +import org.objectweb.asm.commons.AdviceAdapter +import org.objectweb.asm.commons.Method + +class SetDriverMethodVisitor( + apiVersion: Int, + originalVisitor: MethodVisitor, + instrumentableContext: MethodContext, +) : + AdviceAdapter( + apiVersion, + originalVisitor, + instrumentableContext.access, + instrumentableContext.name, + instrumentableContext.descriptor, + ) { + + override fun onMethodEnter() { + loadArg(0) + invokeStatic(Type.getType(SENTRY_SQLITE_DRIVER_TYPE), Method(CREATE, SENTRY_CREATE_DESCRIPTOR)) + storeArg(0) + } + + companion object { + private const val SENTRY_SQLITE_DRIVER_TYPE = "Lio/sentry/sqlite/SentrySQLiteDriver;" + private const val CREATE = "create" + private const val SENTRY_CREATE_DESCRIPTOR = + "(Landroidx/sqlite/SQLiteDriver;)Landroidx/sqlite/SQLiteDriver;" + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt index 29ba71bb3..2eb711d22 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt @@ -4,6 +4,8 @@ package io.sentry.android.gradle.services import com.android.build.gradle.internal.utils.setDisallowChanges import io.sentry.android.gradle.extensions.InstrumentationFeature +import io.sentry.android.gradle.util.ExternalModules +import io.sentry.android.gradle.util.ExternalVersions import io.sentry.android.gradle.util.SemVer import io.sentry.android.gradle.util.SentryModules import io.sentry.android.gradle.util.SentryVersions @@ -52,6 +54,10 @@ abstract class SentryModulesService : features.add("DexGuard") } + if (isSQLiteDriverInstrEnabled()) { + features.add("SQLiteDriver") + } + return project.provider { features } } @@ -72,6 +78,30 @@ abstract class SentryModulesService : sentryModules.isAtLeast(SentryModules.SENTRY_ANDROID_SQLITE, SentryVersions.VERSION_SQLITE) && parameters.features.get().contains(InstrumentationFeature.DATABASE) + fun isSQLiteDriverInstrEnabled(): Boolean = + isSQLiteDriverSentryGateEnabled() && isSQLiteDriverRoomGateEnabled() + + /** + * Returns true if the owning app uses a version of sentry-android-sqlite that contains the + * `SentrySQLiteDriver`. + */ + private fun isSQLiteDriverSentryGateEnabled(): Boolean = + sentryModules.isAtLeast( + SentryModules.SENTRY_ANDROID_SQLITE, + SentryVersions.VERSION_SQLITE_DRIVER, + ) && parameters.features.get().contains(InstrumentationFeature.DATABASE) + + /** Returns true if the owning app uses on a version of Room that supports `SQLiteDriver`. */ + private fun isSQLiteDriverRoomGateEnabled(): Boolean = + externalModules.isAtLeastMinor( + ExternalModules.ROOM2_RUNTIME, + ExternalVersions.ROOM2_SQLITE_DRIVER_VERSION, + ) || + externalModules.isAtLeastMinor( + ExternalModules.ROOM3_RUNTIME, + ExternalVersions.ROOM3_SQLITE_DRIVER_VERSION, + ) + fun isOldDatabaseInstrEnabled(): Boolean = !isNewDatabaseInstrEnabled() && sentryModules.isAtLeast( @@ -121,6 +151,21 @@ abstract class SentryModulesService : minVersion: SemVer, ): Boolean = getOrDefault(module, SentryVersions.VERSION_DEFAULT) >= minVersion + /** + * External-library gate on major.minor only, irrespective of patch or pre-release. Pre-releases + * on the floor line pass (e.g. Room `2.7.0-alpha12` satisfies a `2.7.0` floor). + * + * Not equivalent to `>=`, as full [SemVer] ordering ranks pre-releases below the release. + */ + private fun Map.isAtLeastMinor( + module: ModuleIdentifier, + minVersion: SemVer, + ): Boolean { + val version = getOrDefault(module, SentryVersions.VERSION_DEFAULT) + return version.major > minVersion.major || + (version.major == minVersion.major && version.minor >= minVersion.minor) + } + companion object { fun register( project: Project, diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt index 8485bde93..df6c32739 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt @@ -33,10 +33,21 @@ internal object SentryVersions { internal val VERSION_LOGCAT = SemVer(6, 17, 0) internal val VERSION_APP_START = SemVer(7, 1, 0) internal val VERSION_SQLITE = SemVer(6, 21, 0) + internal val VERSION_SQLITE_DRIVER = SemVer(8, 44, 0) internal val VERSION_ANDROID_OKHTTP_LISTENER = SemVer(6, 20, 0) internal val VERSION_OKHTTP = SemVer(7, 0, 0) } +internal object ExternalVersions { + // Room 2.7.0 introduced RoomDatabase.Builder.setDriver(SQLiteDriver) in the androidx.room + // package. + internal val ROOM2_SQLITE_DRIVER_VERSION = SemVer(2, 7, 0) + + // Room 3.0 moved the RoomDatabase.Builder.setDriver(SQLiteDriver) to the androidx.room3 + // package. + internal val ROOM3_SQLITE_DRIVER_VERSION = SemVer(3, 0, 0) +} + internal object SentryModules { internal val SENTRY = DefaultModuleIdentifier.newId("io.sentry", "sentry") internal val SENTRY_ANDROID = DefaultModuleIdentifier.newId("io.sentry", "sentry-android") @@ -80,3 +91,8 @@ internal object SentryModules { internal val SENTRY_OPENTELEMETRY_AGENTLESS_SPRING = DefaultModuleIdentifier.newId("io.sentry", "sentry-opentelemetry-agentless-spring") } + +internal object ExternalModules { + internal val ROOM2_RUNTIME = DefaultModuleIdentifier.newId("androidx.room", "room-runtime") + internal val ROOM3_RUNTIME = DefaultModuleIdentifier.newId("androidx.room3", "room3-runtime") +} diff --git a/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt b/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt new file mode 100644 index 000000000..187e5e6a8 --- /dev/null +++ b/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt @@ -0,0 +1,3 @@ +package androidx.sqlite + +interface SQLiteDriver diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt index 56ba455bf..0038afb8d 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt @@ -5,6 +5,7 @@ import io.sentry.android.gradle.instrumentation.androidx.compose.ComposeNavigati import io.sentry.android.gradle.instrumentation.androidx.room.AndroidXRoomDao import io.sentry.android.gradle.instrumentation.androidx.sqlite.AndroidXSQLiteOpenHelper import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.AndroidXSQLiteDatabase +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.AndroidXSQLiteDriver import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement import io.sentry.android.gradle.instrumentation.appstart.Application import io.sentry.android.gradle.instrumentation.appstart.ContentProvider @@ -122,6 +123,18 @@ class VisitorTest( AndroidXSQLiteStatement(SemVer(2, 3, 0)), null, ), + arrayOf( + "androidxRoom", + "RoomDatabase\$Builder", + AndroidXSQLiteDriver(), + TestClassContext("androidx.room.RoomDatabase\$Builder"), + ), + arrayOf( + "androidxRoom", + "RoomDatabase3\$Builder", + AndroidXSQLiteDriver(), + TestClassContext("androidx.room3.RoomDatabase\$Builder"), + ), roomDaoTestParameters("DeleteAndReturnUnit"), roomDaoTestParameters("InsertAndReturnLong"), roomDaoTestParameters("InsertAndReturnUnit"), diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt new file mode 100644 index 000000000..bcc844eeb --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt @@ -0,0 +1,71 @@ +@file:Suppress("UnstableApiUsage") + +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver + +import io.sentry.android.gradle.instrumentation.ChainedInstrumentable +import io.sentry.android.gradle.instrumentation.fakes.TestClassContext +import io.sentry.android.gradle.instrumentation.fakes.TestSpanAddingParameters +import java.io.FileInputStream +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes + +class AndroidXSQLiteDriverTest { + + @get:Rule val tmpDir = TemporaryFolder() + + private val instrumentable = AndroidXSQLiteDriver() + + @Test + fun `isInstrumentable returns true for RoomDatabase Builder classes`() { + assertTrue( + instrumentable.isInstrumentable(TestClassContext("androidx.room.RoomDatabase\$Builder")) + ) + assertTrue( + instrumentable.isInstrumentable(TestClassContext("androidx.room3.RoomDatabase\$Builder")) + ) + } + + @Test + fun `isInstrumentable returns false for unrelated classes`() { + assertFalse(instrumentable.isInstrumentable(TestClassContext("com.example.RoomConfig"))) + assertFalse(instrumentable.isInstrumentable(TestClassContext("io.sentry.Sentry"))) + assertFalse(instrumentable.isInstrumentable(TestClassContext("com.example.FakeSetDriver"))) + } + + @Test + fun `ChainedInstrumentable does not instrument unrelated classes`() { + val className = "com.example.NoSetDriver" + val originalBytes = loadNoSetDriverFixtureBytes() + val instrumentedBytes = instrumentThroughChain(className, originalBytes) + + assertEquals(0, SQLiteDriverBytecodeTestUtil.countWrapCalls(instrumentedBytes)) + } + + private fun instrumentThroughChain(className: String, bytes: ByteArray): ByteArray { + val classReader = ClassReader(bytes) + val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) + val classVisitor = + ChainedInstrumentable(listOf(instrumentable)) + .getVisitor( + TestClassContext(className), + Opcodes.ASM9, + classWriter, + parameters = TestSpanAddingParameters(inMemoryDir = tmpDir.root), + ) + classReader.accept(classVisitor, ClassReader.SKIP_FRAMES) + return classWriter.toByteArray() + } + + private fun loadNoSetDriverFixtureBytes(): ByteArray = + FileInputStream( + "src/test/resources/testFixtures/instrumentation/androidxSqliteDriver/NoSetDriver.class" + ) + .use { it.readBytes() } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SQLiteDriverBytecodeTestUtil.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SQLiteDriverBytecodeTestUtil.kt new file mode 100644 index 000000000..36149e084 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SQLiteDriverBytecodeTestUtil.kt @@ -0,0 +1,57 @@ +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver + +import java.io.FileInputStream +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode + +internal object SQLiteDriverBytecodeTestUtil { + + private const val FIXTURES_ROOT = "src/test/resources/testFixtures/instrumentation/androidxRoom" + private const val SENTRY_SQLITE_DRIVER = "io/sentry/sqlite/SentrySQLiteDriver" + private const val CREATE = "create" + + const val SENTRY_CREATE_DESCRIPTOR = + "(Landroidx/sqlite/SQLiteDriver;)Landroidx/sqlite/SQLiteDriver;" + const val SET_DRIVER_DESCRIPTOR_PREFIX = "(Landroidx/sqlite/SQLiteDriver;)" + const val SET_DRIVER = "setDriver" + + private val CLASS_NAME_TO_FIXTURE = + mapOf( + "androidx.room.RoomDatabase\$Builder" to "RoomDatabase\$Builder", + "androidx.room3.RoomDatabase\$Builder" to "RoomDatabase3\$Builder", + ) + + fun loadRoomBuilderFixture(className: String): ByteArray { + val fixtureName = + CLASS_NAME_TO_FIXTURE[className] ?: error("No committed fixture for class $className") + return FileInputStream("$FIXTURES_ROOT/$fixtureName.class").use { it.readBytes() } + } + + fun isWrapCall(insn: MethodInsnNode): Boolean = + insn.opcode == Opcodes.INVOKESTATIC && + insn.owner == SENTRY_SQLITE_DRIVER && + insn.name == CREATE && + insn.desc == SENTRY_CREATE_DESCRIPTOR + + fun isSetDriverDescriptor(descriptor: String): Boolean = + descriptor.startsWith(SET_DRIVER_DESCRIPTOR_PREFIX) + + fun countWrapCalls(bytes: ByteArray): Int { + val classNode = ClassNode().also { ClassReader(bytes).accept(it, 0) } + return classNode.methods.sumOf(::countWrapCalls) + } + + fun countWrapCalls(method: MethodNode): Int { + var count = 0 + var insn: AbstractInsnNode? = method.instructions.first + while (insn != null) { + if (insn is MethodInsnNode && isWrapCall(insn)) count++ + insn = insn.next + } + return count + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SetDriverMethodInstrumentableTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SetDriverMethodInstrumentableTest.kt new file mode 100644 index 000000000..253273742 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SetDriverMethodInstrumentableTest.kt @@ -0,0 +1,48 @@ +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver + +import io.sentry.android.gradle.instrumentation.MethodContext +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test +import org.objectweb.asm.Opcodes + +class SetDriverMethodInstrumentableTest { + + private val instrumentable = SetDriverMethodInstrumentable() + + @Test + fun `isInstrumentable returns true for setDriver with SQLiteDriver parameter`() { + assertTrue( + instrumentable.isInstrumentable( + methodContext( + SQLiteDriverBytecodeTestUtil.SET_DRIVER, + "${SQLiteDriverBytecodeTestUtil.SET_DRIVER_DESCRIPTOR_PREFIX}Landroidx/room/RoomDatabase\$Builder;", + ) + ) + ) + } + + @Test + fun `isInstrumentable returns false for unrelated method names`() { + assertFalse(instrumentable.isInstrumentable(methodContext("build", "()V"))) + } + + @Test + fun `isInstrumentable returns false for setDriver with non-SQLiteDriver descriptor`() { + assertFalse( + instrumentable.isInstrumentable( + methodContext(SQLiteDriverBytecodeTestUtil.SET_DRIVER, "(Ljava/lang/Object;)V") + ) + ) + } + + @Test + fun `isInstrumentable returns false when descriptor is null`() { + assertFalse( + instrumentable.isInstrumentable(methodContext(SQLiteDriverBytecodeTestUtil.SET_DRIVER, null)) + ) + } + + private fun methodContext(name: String, descriptor: String?) = + MethodContext(Opcodes.ACC_PUBLIC, name, descriptor, null, null) +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt new file mode 100644 index 000000000..890983b44 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt @@ -0,0 +1,83 @@ +@file:Suppress("UnstableApiUsage") + +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor + +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.AndroidXSQLiteDriver +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.SQLiteDriverBytecodeTestUtil +import io.sentry.android.gradle.instrumentation.fakes.TestClassContext +import io.sentry.android.gradle.instrumentation.fakes.TestSpanAddingParameters +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode + +class SetDriverMethodVisitorTest { + + @get:Rule val tmpDir = TemporaryFolder() + + @Test + fun `wraps the driver parameter at the start of Room 2_x Builder setDriver`() { + assertSetDriverWrappedOnce("androidx.room.RoomDatabase\$Builder") + } + + @Test + fun `wraps the driver parameter at the start of Room 3_x Builder setDriver`() { + assertSetDriverWrappedOnce("androidx.room3.RoomDatabase\$Builder") + } + + private fun assertSetDriverWrappedOnce(className: String) { + val instrumentedBytes = instrument(className) + val setDriverMethod = findSetDriverMethod(instrumentedBytes) + + assertEquals( + 1, + SQLiteDriverBytecodeTestUtil.countWrapCalls(setDriverMethod), + "setDriver should contain exactly one wrap", + ) + assertTrue( + wrapPrecedesOriginalBody(setDriverMethod), + "SentrySQLiteDriver.create() must run before the original setDriver body", + ) + } + + private fun instrument(className: String): ByteArray { + val bytes = SQLiteDriverBytecodeTestUtil.loadRoomBuilderFixture(className) + val classReader = ClassReader(bytes) + val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) + val classVisitor = + AndroidXSQLiteDriver() + .getVisitor( + TestClassContext(className), + Opcodes.ASM9, + classWriter, + parameters = TestSpanAddingParameters(inMemoryDir = tmpDir.root), + ) + classReader.accept(classVisitor, ClassReader.SKIP_FRAMES) + return classWriter.toByteArray() + } + + private fun findSetDriverMethod(bytes: ByteArray): MethodNode { + val classNode = ClassNode().also { ClassReader(bytes).accept(it, 0) } + return classNode.methods.first { + it.name == SQLiteDriverBytecodeTestUtil.SET_DRIVER && + SQLiteDriverBytecodeTestUtil.isSetDriverDescriptor(it.desc) + } + } + + private fun wrapPrecedesOriginalBody(method: MethodNode): Boolean { + val realInsns = method.instructions.toArray().filter { it.opcode >= 0 } + val wrapIndex = + realInsns.indexOfFirst { it is MethodInsnNode && SQLiteDriverBytecodeTestUtil.isWrapCall(it) } + assertTrue(wrapIndex >= 0, "setDriver has no SentrySQLiteDriver.create call") + val returnIndex = + realInsns.indexOfFirst { it.opcode == Opcodes.ARETURN || it.opcode == Opcodes.RETURN } + return wrapIndex < returnIndex + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt index 50c8c5b0e..a8885cd99 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt @@ -16,11 +16,13 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertTrue +import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.TaskOutcome import org.gradle.util.GradleVersion import org.hamcrest.CoreMatchers.`is` import org.junit.Assert.assertThrows import org.junit.Assume.assumeThat +import org.junit.Ignore import org.junit.Test class SentryPluginTest : @@ -529,6 +531,73 @@ class SentryPluginTest : } } + @Ignore(SQLITE_DRIVER_IGNORE_REASON) + @Test + fun `applies sqliteDriver instrumentable when both gates pass with room2, open helper enabled and old path off`() { + val build = + buildDatabaseInstrumentation(SQLITE, ROOM2_AT_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_DRIVER) + + assertInstrumentableChain("AndroidXSQLiteOpenHelper, AndroidXSQLiteDriver, AndroidXRoomDao)") + val chainLine = instrumentableChainLine(build.output) + assertTrue(chainLine != null) + assertFalse("AndroidXSQLiteDatabase" in chainLine!!) + assertFalse("AndroidXSQLiteStatement" in chainLine) + } + + @Ignore(SQLITE_DRIVER_IGNORE_REASON) + @Test + fun `applies sqliteDriver instrumentable when both gates pass with room3`() { + buildDatabaseInstrumentation(SQLITE, ROOM3_AT_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_DRIVER) + + assertInstrumentableChain("AndroidXSQLiteOpenHelper, AndroidXSQLiteDriver, AndroidXRoomDao)") + } + + @Test + fun `does not apply sqliteDriver instrumentable when both gates fail, open helper remains enabled without room on classpath`() { + buildDatabaseInstrumentation(SQLITE_OLD, SENTRY_ANDROID_SQLITE_OPEN_HELPER, minSdk = null) + + assertInstrumentableChain("AndroidXSQLiteOpenHelper, AndroidXRoomDao)") + assertSQLiteDriverInstrumentableAbsent() + } + + @Test + fun `does not apply sqliteDriver instrumentable when sentry gate fails and room2 gate passes`() { + buildDatabaseInstrumentation(SQLITE, ROOM2_AT_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_OPEN_HELPER) + + assertInstrumentableChain("AndroidXSQLiteOpenHelper, AndroidXRoomDao)") + assertSQLiteDriverInstrumentableAbsent() + } + + @Test + fun `does not apply sqliteDriver instrumentable when sentry gate fails and room3 gate passes`() { + buildDatabaseInstrumentation(SQLITE, ROOM3_AT_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_OPEN_HELPER) + + assertInstrumentableChain("AndroidXSQLiteOpenHelper, AndroidXRoomDao)") + assertSQLiteDriverInstrumentableAbsent() + } + + @Ignore(SQLITE_DRIVER_IGNORE_REASON) + @Test + fun `does not apply sqliteDriver instrumentable when sentry gate passes and room is absent from classpath`() { + buildDatabaseInstrumentation(SQLITE, SENTRY_ANDROID_SQLITE_DRIVER) + + assertInstrumentableChain("AndroidXSQLiteOpenHelper, AndroidXRoomDao)") + assertSQLiteDriverInstrumentableAbsent() + } + + // Note: Room 3 can't be below the driver floor b/c our floor is 3.0.0-alpha01, so we don't + // bother to test that scenario. + @Ignore(SQLITE_DRIVER_IGNORE_REASON) + @Test + fun `does not apply sqliteDriver instrumentable when sentry gate passes and room2 is below driver floor`() { + buildDatabaseInstrumentation(SQLITE, ROOM2_BELOW_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_DRIVER) + + assertInstrumentableChain("AndroidXSQLiteOpenHelper, AndroidXRoomDao)") + assertSQLiteDriverInstrumentableAbsent() + } + + // Database path when sentry-android-sqlite is absent (old AndroidXSQLiteDatabase/Statement). + // Orthogonal to the SQLiteDriver gate matrix above. @Test fun `apply old Database instrumentable when app does not depend on sentry-android-sqlite`() { applyTracingInstrumentation( @@ -1106,6 +1175,7 @@ class SentryPluginTest : excludes: Set = emptySet(), sdkVersion: String = "7.1.0", forceInstrumentDependencies: Boolean = true, + minSdk: Int? = null, ) { appBuildFile.appendText( // language=Groovy @@ -1131,8 +1201,68 @@ class SentryPluginTest : excludes = ["${excludes.joinToString()}"] } } + ${ + minSdk?.let { + """ + android { + defaultConfig { + minSdkVersion $it + } + } + """ + } ?: "" + } """ .trimIndent() ) } + + private var sqliteDriverBuildOutput: String = "" + + private fun buildDatabaseInstrumentation( + vararg dependencies: String, + minSdk: Int? = DRIVER_PATH_MIN_SDK, + ): BuildResult { + applyTracingInstrumentation( + features = setOf(InstrumentationFeature.DATABASE), + dependencies = dependencies.toSet(), + appStart = false, + logcat = false, + minSdk = minSdk, + ) + val build = runner.appendArguments(":app:assembleDebug", "--info").build() + sqliteDriverBuildOutput = build.output + return build + } + + private fun assertInstrumentableChain(expectedInstrumentables: String) { + assertTrue { + "[sentry] Instrumentable: ChainedInstrumentable(instrumentables=$expectedInstrumentables" in + sqliteDriverBuildOutput + } + } + + private fun assertSQLiteDriverInstrumentableAbsent() { + assertFalse("AndroidXSQLiteDriver" in sqliteDriverBuildOutput) + } + + private fun instrumentableChainLine(output: String): String? = + output.lines().firstOrNull { + it.contains("[sentry] Instrumentable: ChainedInstrumentable(instrumentables=") + } + + companion object { + private const val SQLITE_DRIVER_IGNORE_REASON = + "Placeholder version VERSION_SQLITE_DRIVER not yet on Maven" + + private const val SQLITE = "androidx.sqlite:sqlite:2.6.2" + private const val SQLITE_OLD = "androidx.sqlite:sqlite:2.0.0" + private const val SENTRY_ANDROID_SQLITE_OPEN_HELPER = "io.sentry:sentry-android-sqlite:6.21.0" + private const val SENTRY_ANDROID_SQLITE_DRIVER = "io.sentry:sentry-android-sqlite:8.44.0" + private const val ROOM2_AT_DRIVER_FLOOR = "androidx.room:room-runtime:2.7.0" + private const val ROOM2_BELOW_DRIVER_FLOOR = "androidx.room:room-runtime:2.6.1" + private const val ROOM3_AT_DRIVER_FLOOR = "androidx.room3:room3-runtime:3.0.0-alpha06" + /** androidx.sqlite 2.6.x and room3-runtime both require minSdk 23 in the test fixture. */ + private const val DRIVER_PATH_MIN_SDK = 23 + } } diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt index c2f728d81..e55431502 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt @@ -198,6 +198,32 @@ class SentryModulesCollectorTest { assertTrue { fixture.getExternalModules()[moduleIdentifier]!! == SemVer.parse(version) } } + @Test + fun `room3-runtime dependency is collected under ROOM3_RUNTIME module identifier`() { + val group = "androidx.room3" + val name = "room3-runtime" + val version = "3.0.0-alpha06" + val moduleIdentifier = DefaultModuleIdentifier.newId(group, name) + + val room3RuntimeDep = + mock { + whenever(mock.moduleVersion) + .thenReturn(DefaultModuleVersionIdentifier.newId(group, name, version)) + } + + val project = fixture.getSut(testProjectDir.root, dependencies = setOf(room3RuntimeDep)) + project.collectModules( + fixture.configurationName, + fixture.variantName, + fixture.sentryModulesServiceProvider, + ) + + assertTrue { + fixture.getExternalModules()[ExternalModules.ROOM3_RUNTIME] == SemVer.parse(version) + } + assertTrue { fixture.getExternalModules()[moduleIdentifier] == SemVer.parse(version) } + } + @Test fun `sentry-android transitive - logs a info and persists both modules in build service`() { val firstLevelDep = diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt new file mode 100644 index 000000000..590c0dc65 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt @@ -0,0 +1,249 @@ +package io.sentry.android.gradle.util + +import io.sentry.android.gradle.extensions.InstrumentationFeature +import io.sentry.android.gradle.services.SentryModulesService +import java.io.File +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.gradle.api.artifacts.ModuleIdentifier +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class SentryModulesServiceTest { + + class Fixture { + + data class Sut(val service: SentryModulesService, val project: org.gradle.api.Project) + + fun getSut( + tmpDir: File, + features: Set = emptySet(), + sentryModules: Map = emptyMap(), + externalModules: Map = emptyMap(), + ): Sut { + val fakeProject = ProjectBuilder.builder().withProjectDir(tmpDir).build() + + val featureProvider = fakeProject.provider { features } + val logcatEnabled = fakeProject.provider { true } + val sourceContextEnabled = fakeProject.provider { false } + val dexguardEnabled = fakeProject.provider { false } + val appStartEnabled = fakeProject.provider { false } + + val serviceProvider = + SentryModulesService.register( + fakeProject, + featureProvider, + logcatEnabled, + sourceContextEnabled, + dexguardEnabled, + appStartEnabled, + ) + val service = serviceProvider.get() + service.sentryModules = sentryModules + service.externalModules = externalModules + return Sut(service, fakeProject) + } + } + + @get:Rule val testProjectDir = TemporaryFolder() + + private val fixture = Fixture() + + private fun sqliteDriverSentryModules(): Map = + mapOf(SentryModules.SENTRY_ANDROID_SQLITE to SentryVersions.VERSION_SQLITE_DRIVER) + + private fun sqliteDriverExternalModules( + roomVersion: SemVer = ExternalVersions.ROOM2_SQLITE_DRIVER_VERSION + ): Map = mapOf(ExternalModules.ROOM2_RUNTIME to roomVersion) + + @Test + fun `isSQLiteDriverInstrEnabled is true when sentry-android-sqlite meets threshold, DATABASE is enabled, and room-runtime is 2_7 pre-release`() { + val preRelease = SemVer(2, 7, 0, "alpha12") + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = sqliteDriverSentryModules(), + externalModules = sqliteDriverExternalModules(roomVersion = preRelease), + ) + + assertTrue(service.isSQLiteDriverInstrEnabled()) + // Minor-line gating accepts pre-releases on the floor line; full SemVer ordering does not. + assertFalse(preRelease >= ExternalVersions.ROOM2_SQLITE_DRIVER_VERSION) + } + + @Test + fun `isSQLiteDriverInstrEnabled is true when sentry-android-sqlite meets threshold, DATABASE is enabled, and room-runtime is 2_7`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = sqliteDriverSentryModules(), + externalModules = sqliteDriverExternalModules(), + ) + + assertTrue(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is true when sentry-android-sqlite meets threshold, DATABASE is enabled, and room3-runtime is 3_0 pre-release`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = sqliteDriverSentryModules(), + externalModules = mapOf(ExternalModules.ROOM3_RUNTIME to SemVer(3, 0, 0, "alpha01")), + ) + + assertTrue(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is true when sentry-android-sqlite meets threshold, DATABASE is enabled, and only room3-runtime is 3_0`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = sqliteDriverSentryModules(), + externalModules = + mapOf(ExternalModules.ROOM3_RUNTIME to ExternalVersions.ROOM3_SQLITE_DRIVER_VERSION), + ) + + assertTrue(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is false when sentry-android-sqlite is absent from classpath`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = emptyMap(), + externalModules = sqliteDriverExternalModules(), + ) + + assertFalse(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is false when sentry-android-sqlite is below VERSION_SQLITE`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = mapOf(SentryModules.SENTRY_ANDROID_SQLITE to SentryVersions.VERSION_SQLITE), + externalModules = sqliteDriverExternalModules(), + ) + + assertFalse(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is false when sentry-android-sqlite is one minor below VERSION_SQLITE_DRIVER`() { + val belowThreshold = + SemVer( + SentryVersions.VERSION_SQLITE_DRIVER.major, + SentryVersions.VERSION_SQLITE_DRIVER.minor - 1, + SentryVersions.VERSION_SQLITE_DRIVER.patch, + ) + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = mapOf(SentryModules.SENTRY_ANDROID_SQLITE to belowThreshold), + externalModules = sqliteDriverExternalModules(), + ) + + assertFalse(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is false when DATABASE is disabled`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = emptySet(), + sentryModules = sqliteDriverSentryModules(), + externalModules = sqliteDriverExternalModules(), + ) + + assertFalse(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is false when room-runtime is below 2_7`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = sqliteDriverSentryModules(), + externalModules = sqliteDriverExternalModules(roomVersion = SemVer(2, 6, 1)), + ) + + assertFalse(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `retrieveEnabledInstrumentationFeatures includes SQLiteDriver when gate passes`() { + val (service, project) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = sqliteDriverSentryModules(), + externalModules = sqliteDriverExternalModules(), + ) + + val features = service.retrieveEnabledInstrumentationFeatures(project).get() + + assertTrue("SQLiteDriver" in features) + assertTrue(InstrumentationFeature.DATABASE.integrationName in features) + } + + @Test + fun `VERSION_SQLITE_DRIVER is greater than or equal to VERSION_SQLITE`() { + // Gating relies on the presence of the open helper whenever the driver is present (shared + // sentry-android-sqlite module): both instrumentables fire together and we rely on + // SentrySQLiteDriver.create to dedup the SupportSQLiteDriver bridge case. + assertTrue(SentryVersions.VERSION_SQLITE_DRIVER >= SentryVersions.VERSION_SQLITE) + } + + @Test + fun `between VERSION_SQLITE and VERSION_SQLITE_DRIVER only the open-helper path is on`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = + mapOf( + SentryModules.SENTRY_ANDROID_SQLITE to SentryVersions.VERSION_SQLITE, + SentryModules.SENTRY_ANDROID_CORE to SentryVersions.VERSION_PERFORMANCE, + ), + externalModules = sqliteDriverExternalModules(), + ) + + assertTrue(service.isNewDatabaseInstrEnabled()) + assertFalse(service.isSQLiteDriverInstrEnabled()) + assertFalse(service.isOldDatabaseInstrEnabled()) + } + + @Test + fun `at VERSION_SQLITE_DRIVER the open-helper path is also on and the old path is off`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = + mapOf( + SentryModules.SENTRY_ANDROID_SQLITE to SentryVersions.VERSION_SQLITE_DRIVER, + SentryModules.SENTRY_ANDROID_CORE to SentryVersions.VERSION_PERFORMANCE, + ), + externalModules = sqliteDriverExternalModules(), + ) + + assertTrue(service.isSQLiteDriverInstrEnabled()) + assertTrue(service.isNewDatabaseInstrEnabled()) // superset relationship + assertFalse(service.isOldDatabaseInstrEnabled()) // suppressed by !isNewDatabaseInstrEnabled() + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt b/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt new file mode 100644 index 000000000..7cd08376b --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt @@ -0,0 +1,9 @@ +package io.sentry.sqlite + +import androidx.sqlite.SQLiteDriver + +class SentrySQLiteDriver private constructor(private val delegate: SQLiteDriver) : SQLiteDriver { + companion object { + @JvmStatic fun create(delegate: SQLiteDriver): SQLiteDriver = SentrySQLiteDriver(delegate) + } +} diff --git a/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase$Builder.class b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase$Builder.class new file mode 100644 index 000000000..8660b5726 Binary files /dev/null and b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase$Builder.class differ diff --git a/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase3$Builder.class b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase3$Builder.class new file mode 100644 index 000000000..33b9d0369 Binary files /dev/null and b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase3$Builder.class differ diff --git a/plugin-build/src/test/resources/testFixtures/instrumentation/androidxSqliteDriver/NoSetDriver.class b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxSqliteDriver/NoSetDriver.class new file mode 100644 index 000000000..7d3748a1e Binary files /dev/null and b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxSqliteDriver/NoSetDriver.class differ