diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePlugin.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePlugin.kt index b0f4fe2e..34c144b3 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePlugin.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePlugin.kt @@ -17,17 +17,21 @@ import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.logging.Logging import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.TaskProvider import org.gradle.process.ExecOperations import java.io.File import javax.inject.Inject +import kotlin.io.path.Path /** * Plugin adding tasks for Android projects using Datadog's SDK for Android. */ @Suppress("TooManyFunctions") class DdAndroidGradlePlugin @Inject constructor( - private val execOps: ExecOperations + private val execOps: ExecOperations, + private val providerFactory: ProviderFactory ) : Plugin { // region Plugin @@ -37,17 +41,29 @@ class DdAndroidGradlePlugin @Inject constructor( val extension = target.extensions.create(EXT_NAME, DdExtension::class.java) val apiKey = resolveApiKey(target) + // need to use withPlugin instead of afterEvaluate, because otherwise generated assets + // folder with buildId is not picked by AGP by some reason + target.pluginManager.withPlugin("com.android.application") { + val androidExtension = target.androidApplicationExtension ?: return@withPlugin + androidExtension.applicationVariants.all { variant -> + if (extension.enabled) { + configureTasksForVariant( + target, + androidExtension, + extension, + variant, + apiKey + ) + } + } + } + target.afterEvaluate { - val androidExtension = target.extensions.findByType(AppExtension::class.java) + val androidExtension = target.androidApplicationExtension if (androidExtension == null) { LOGGER.error(ERROR_NOT_ANDROID) } else if (!extension.enabled) { LOGGER.info("Datadog extension disabled, no upload task created") - } else { - androidExtension.applicationVariants.all { variant -> - configureVariantForUploadTask(target, variant, apiKey, extension) - configureVariantForSdkCheck(target, variant, extension) - } } } } @@ -56,6 +72,30 @@ class DdAndroidGradlePlugin @Inject constructor( // region Internal + internal fun configureTasksForVariant( + target: Project, + androidExtension: AppExtension, + datadogExtension: DdExtension, + variant: ApplicationVariant, + apiKey: ApiKey + ) { + if (isObfuscationEnabled(variant, datadogExtension)) { + val buildIdGenerationTask = + configureBuildIdGenerationTask(target, androidExtension, variant) + + configureVariantForUploadTask( + target, + variant, + buildIdGenerationTask, + apiKey, + datadogExtension + ) + } else { + LOGGER.info("Minifying disabled for variant ${variant.name}, no upload task created") + } + configureVariantForSdkCheck(target, variant, datadogExtension) + } + @Suppress("ReturnCount") // TODO RUMM-2382 use ProviderFactory/Provider APIs to watch changes in external environment internal fun resolveApiKey(target: Project): ApiKey { @@ -69,23 +109,52 @@ class DdAndroidGradlePlugin @Inject constructor( return apiKey ?: ApiKey.NONE } + @Suppress("StringLiteralDuplication") + internal fun configureBuildIdGenerationTask( + target: Project, + appExtension: AppExtension, + variant: ApplicationVariant + ): TaskProvider { + val buildIdDirectory = target.layout.buildDirectory + .dir(Path("generated", "datadog", "buildId", variant.name).toString()) + val buildIdGenerationTask = GenerateBuildIdTask.register(target, variant, buildIdDirectory) + + // we could generate buildIdDirectory inside GenerateBuildIdTask and read it here as + // property using flatMap, but when Gradle sync is done inside Android Studio there is an error + // Querying the mapped value of provider (java.util.Set) before task ... has completed is + // not supported, which doesn't happen when Android Studio is not used (pure Gradle build) + // so applying such workaround + // TODO RUM-0000 use new AndroidComponents API to inject generated stuff, it is more friendly + appExtension.sourceSets.getByName(variant.name).assets.srcDir(buildIdDirectory) + + val variantName = variant.name.capitalize() + listOf( + "package${variantName}Bundle", + "build${variantName}PreBundle", + "lintVitalAnalyze$variantName" + ).forEach { + target.tasks.findByName(it)?.dependsOn(buildIdGenerationTask) + } + + listOf( + variant.packageApplicationProvider, + variant.mergeAssetsProvider + ).forEach { + it.configure { it.dependsOn(buildIdGenerationTask) } + } + + return buildIdGenerationTask + } + @Suppress("DefaultLocale", "ReturnCount") internal fun configureVariantForUploadTask( target: Project, variant: ApplicationVariant, + buildIdGenerationTask: TaskProvider, apiKey: ApiKey, extension: DdExtension - ): Task? { + ): Task { val extensionConfiguration = resolveExtensionConfiguration(extension, variant) - val isDefaultObfuscationEnabled = variant.buildType.isMinifyEnabled - val isNonDefaultObfuscationEnabled = extensionConfiguration.nonDefaultObfuscation - val isObfuscationEnabled = isDefaultObfuscationEnabled || isNonDefaultObfuscationEnabled - - if (!isObfuscationEnabled) { - LOGGER.info("Minifying disabled for variant ${variant.name}, no upload task created") - return null - } - val flavorName = variant.flavorName val uploadTaskName = UPLOAD_TASK_NAME + variant.name.capitalize() // TODO RUMM-2382 use tasks.register @@ -97,6 +166,15 @@ class DdAndroidGradlePlugin @Inject constructor( configureVariantTask(uploadTask, apiKey, flavorName, extensionConfiguration, variant) + // upload task shouldn't depend on the build ID generation task, but only read its property, + // because upload task may be triggered after assemble task and we don't want to re-generate + // build ID, because it will be different then from the one which is already embedded in + // the application package + uploadTask.buildId = buildIdGenerationTask.flatMap { + it.buildIdFile.flatMap { + providerFactory.provider { it.asFile.readText().trim() } + } + } uploadTask.mappingFilePath = resolveMappingFilePath(extensionConfiguration, target, variant) uploadTask.mappingFilePackagesAliases = filterMappingFileReplacements( @@ -199,6 +277,7 @@ class DdAndroidGradlePlugin @Inject constructor( } } + @Suppress("StringLiteralDuplication") private fun resolveDatadogRepositoryFile(target: Project): File { val outputsDir = File(target.buildDir, "outputs") val reportsDir = File(outputsDir, "reports") @@ -239,6 +318,7 @@ class DdAndroidGradlePlugin @Inject constructor( uploadTask.site = extensionConfiguration.site ?: "" uploadTask.versionName = extensionConfiguration.versionName ?: variant.versionName + uploadTask.versionCode = variant.versionCode uploadTask.serviceName = extensionConfiguration.serviceName ?: variant.applicationId uploadTask.remoteRepositoryUrl = extensionConfiguration.remoteRepositoryUrl ?: "" } @@ -280,6 +360,19 @@ class DdAndroidGradlePlugin @Inject constructor( return findProperty(propertyName)?.toString() } + private fun isObfuscationEnabled( + variant: ApplicationVariant, + extension: DdExtension + ): Boolean { + val extensionConfiguration = resolveExtensionConfiguration(extension, variant) + val isDefaultObfuscationEnabled = variant.buildType.isMinifyEnabled + val isNonDefaultObfuscationEnabled = extensionConfiguration.nonDefaultObfuscation + return isDefaultObfuscationEnabled || isNonDefaultObfuscationEnabled + } + + private val Project.androidApplicationExtension: AppExtension? + get() = extensions.findByType(AppExtension::class.java) + // endregion companion object { @@ -288,11 +381,13 @@ class DdAndroidGradlePlugin @Inject constructor( internal const val DATADOG_API_KEY = "DATADOG_API_KEY" + internal const val DATADOG_TASK_GROUP = "datadog" + internal val LOGGER = Logging.getLogger("DdAndroidGradlePlugin") private const val EXT_NAME = "datadog" - private const val UPLOAD_TASK_NAME = "uploadMapping" + internal const val UPLOAD_TASK_NAME = "uploadMapping" private const val ERROR_NOT_ANDROID = "The dd-android-gradle-plugin has been applied on " + "a non android application project" diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdCheckSdkDepsTask.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdCheckSdkDepsTask.kt index 05398152..5d61f096 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdCheckSdkDepsTask.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdCheckSdkDepsTask.kt @@ -45,7 +45,7 @@ abstract class DdCheckSdkDepsTask : DefaultTask() { internal var isLastRunSuccessful: Boolean = true init { - group = "datadog" + group = DdAndroidGradlePlugin.DATADOG_TASK_GROUP description = "Checks for the Datadog SDK into your variant dependencies." outputs.upToDateWhen { it is DdCheckSdkDepsTask && it.isLastRunSuccessful } } diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdMappingFileUploadTask.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdMappingFileUploadTask.kt index 4d9c0f28..08ae7029 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdMappingFileUploadTask.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdMappingFileUploadTask.kt @@ -67,6 +67,12 @@ open class DdMappingFileUploadTask @get:Input var versionName: String = "" + /** + * The version code of the application. + */ + @get:Input + var versionCode: Int = 0 + /** * The service name of the application (by default, it is your app's package name). */ @@ -85,6 +91,12 @@ open class DdMappingFileUploadTask @get:Input var remoteRepositoryUrl: String = "" + /** + * Build ID which will be used for mapping file matching. + */ + @get:Input + var buildId: Provider = providerFactory.provider { "" } + /** * The path to the mapping file to upload. */ @@ -123,7 +135,7 @@ open class DdMappingFileUploadTask var repositoryFile: File = File("") init { - group = "datadog" + group = DdAndroidGradlePlugin.DATADOG_TASK_GROUP description = "Uploads the Proguard/R8 mapping file to Datadog" // it is never up-to-date, because request may fail outputs.upToDateWhen { false } @@ -143,7 +155,11 @@ open class DdMappingFileUploadTask validateConfiguration() check(!(apiKey.contains("\"") || apiKey.contains("'"))) { - "DD_API_KEY provided shouldn't contain quotes or apostrophes." + INVALID_API_KEY_FORMAT_ERROR + } + + check(buildId.isPresent && buildId.get().isNotEmpty()) { + MISSING_BUILD_ID_ERROR } var mappingFile = File(mappingFilePath) @@ -168,7 +184,9 @@ open class DdMappingFileUploadTask DdAppIdentifier( serviceName = serviceName, version = versionName, - variant = variantName + versionCode = versionCode, + variant = variantName, + buildId = buildId.get() ), repositories.firstOrNull(), !disableGzipOption.isPresent @@ -247,11 +265,7 @@ open class DdMappingFileUploadTask @Suppress("CheckInternal") private fun validateConfiguration() { - check(apiKey.isNotBlank()) { - "Make sure you define an API KEY to upload your mapping files to Datadog. " + - "Create a DD_API_KEY or DATADOG_API_KEY environment variable, gradle" + - " property or define it in datadog-ci.json file." - } + check(apiKey.isNotBlank()) { API_KEY_MISSING_ERROR } if (site.isBlank()) { site = DatadogSite.US1.name @@ -375,5 +389,13 @@ open class DdMappingFileUploadTask private const val DATADOG_CI_SITE_PROPERTY = "datadogSite" const val DATADOG_SITE = "DATADOG_SITE" const val DISABLE_GZIP_GRADLE_PROPERTY = "dd-disable-gzip" + + const val API_KEY_MISSING_ERROR = "Make sure you define an API KEY to upload your mapping files to Datadog. " + + "Create a DD_API_KEY or DATADOG_API_KEY environment variable, gradle" + + " property or define it in datadog-ci.json file." + const val INVALID_API_KEY_FORMAT_ERROR = + "DD_API_KEY provided shouldn't contain quotes or apostrophes." + const val MISSING_BUILD_ID_ERROR = + "Build ID is missing, you need to run upload task only after APK/AAB file is generated." } } diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/GenerateBuildIdTask.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/GenerateBuildIdTask.kt new file mode 100644 index 00000000..b232ccab --- /dev/null +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/GenerateBuildIdTask.kt @@ -0,0 +1,87 @@ +package com.datadog.gradle.plugin + +import com.android.build.gradle.api.ApplicationVariant +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import java.util.UUID + +/** + * This task generates unique Build ID which is later used to match error and mapping file. + */ +abstract class GenerateBuildIdTask : DefaultTask() { + + /** + * Directory to store build ID file. + */ + @get:OutputDirectory + abstract val buildIdDirectory: DirectoryProperty + + /** + * File containing build ID. + */ + @get:Internal + val buildIdFile: Provider = buildIdDirectory.file(BUILD_ID_FILE_NAME) + + /** + * Variant name this task is linked to. + */ + @get:Internal + abstract val variantName: Property + + init { + outputs.upToDateWhen { false } + // not a part of any group, we don't want to expose it + description = "Generates a unique build ID to associate mapping file and application." + } + + /** + * Generates unique build ID and saves it to a file. + */ + @TaskAction + fun generateBuildId() { + val buildIdDirectory = buildIdDirectory.get().asFile + buildIdDirectory.mkdirs() + + val buildId = UUID.randomUUID().toString() + logger.info("Generated buildId=$buildId for variant=${variantName.get()}") + buildIdFile.get().asFile + .writeText(buildId) + } + + companion object { + internal const val TASK_NAME = "generateBuildId" + + /** + * Name of the file containing build ID information. + */ + const val BUILD_ID_FILE_NAME = "datadog.buildId" + + /** + * Registers a new instance of [GenerateBuildIdTask] specific for the given [ApplicationVariant]. + */ + fun register( + target: Project, + variant: ApplicationVariant, + buildIdDirectory: Provider + ): TaskProvider { + val generateBuildIdTask = target.tasks.register( + TASK_NAME + variant.name.capitalize(), + GenerateBuildIdTask::class.java + ) { + it.buildIdDirectory.set(buildIdDirectory) + it.variantName.set(variant.name) + } + + return generateBuildIdTask + } + } +} diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/DdAppIdentifier.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/DdAppIdentifier.kt index 70e1e08a..84aa567e 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/DdAppIdentifier.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/DdAppIdentifier.kt @@ -9,10 +9,13 @@ package com.datadog.gradle.plugin.internal internal data class DdAppIdentifier( val serviceName: String, val version: String, - val variant: String + val versionCode: Int, + val variant: String, + val buildId: String ) { override fun toString(): String { - return "`service:$serviceName`, `version:$version`, `variant:$variant`" + return "`service:$serviceName`, `version:$version`, `versionCode:$versionCode`," + + " `variant:$variant`, `buildId:$buildId`" } } diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploader.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploader.kt index 0ba719d7..789e416a 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploader.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploader.kt @@ -103,6 +103,8 @@ internal class OkHttpUploader : Uploader { eventJson.put("version", identifier.version) eventJson.put("service", identifier.serviceName) eventJson.put("variant", identifier.variant) + eventJson.put("buildId", identifier.buildId) + eventJson.put("versionCode", identifier.versionCode) eventJson.put("type", TYPE_JVM_MAPPING_FILE) val builder = MultipartBody.Builder() diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginFunctionalTest.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginFunctionalTest.kt index 86f1af46..85e46ff2 100644 --- a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginFunctionalTest.kt +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginFunctionalTest.kt @@ -26,6 +26,8 @@ import org.junit.jupiter.api.io.TempDir import java.io.File import java.util.Locale import java.util.Properties +import java.util.UUID +import java.util.zip.ZipFile import kotlin.io.path.Path @Extensions( @@ -118,10 +120,7 @@ internal class DdAndroidGradlePluginFunctionalTest { appBuildGradleFile ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { withArguments("--info", ":samples:app:assembleRelease") } .build() // Then @@ -137,11 +136,8 @@ internal class DdAndroidGradlePluginFunctionalTest { appBuildGradleFile ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = + gradleRunner { withArguments("--info", ":samples:app:assembleRelease") }.build() // Then assertThat(result.task(":samples:app:assembleRelease")?.outcome) @@ -157,11 +153,8 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--build-cache", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = + gradleRunner { withArguments("--build-cache", ":samples:app:assembleRelease") }.build() // Then assertThat(result.task(":samples:app:assembleRelease")?.outcome) @@ -176,11 +169,7 @@ internal class DdAndroidGradlePluginFunctionalTest { appBuildGradleFile ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = gradleRunner { withArguments("--info", ":samples:app:assembleDebug") }.build() // Then assertThat(result.task(":samples:app:assembleDebug")?.outcome) @@ -195,11 +184,7 @@ internal class DdAndroidGradlePluginFunctionalTest { appBuildGradleFile ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = gradleRunner { withArguments("--info", ":samples:app:assembleDebug") }.build() // Then assertThat(result.task(":samples:app:assembleDebug")?.outcome) @@ -214,11 +199,7 @@ internal class DdAndroidGradlePluginFunctionalTest { appBuildGradleFile ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = gradleRunner { withArguments("--info", ":samples:app:assembleDebug") }.build() // Then assertThat(result.task(":samples:app:assembleDebug")?.outcome) @@ -233,16 +214,15 @@ internal class DdAndroidGradlePluginFunctionalTest { appBuildGradleFile ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = gradleRunner { withArguments("--info", ":samples:app:assembleDebug") }.build() // Then assertThat(result.task(":samples:app:assembleDebug")?.outcome) .isEqualTo(TaskOutcome.SUCCESS) assertThat(result.output).contains("Datadog extension disabled, no upload task created") + assertThat(result.tasks).noneMatch { + it.path.contains(DdAndroidGradlePlugin.UPLOAD_TASK_NAME) + } } @Test @@ -254,11 +234,8 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--build-cache", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = + gradleRunner { withArguments("--build-cache", ":samples:app:assembleDebug") }.build() // Then assertThat(result.task(":samples:app:assembleDebug")?.outcome) @@ -283,11 +260,12 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--configuration-cache", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = gradleRunner { + withArguments( + "--configuration-cache", + ":samples:app:assembleRelease" + ) + }.build() // Then assertThat(result.task(":samples:app:assembleRelease")?.outcome) @@ -311,11 +289,12 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--configuration-cache", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = gradleRunner { + withArguments( + "--configuration-cache", + ":samples:app:assembleDebug" + ) + }.build() // Then assertThat(result.task(":samples:app:assembleDebug")?.outcome) @@ -331,11 +310,12 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--configuration-cache", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = gradleRunner { + withArguments( + "--configuration-cache", + ":samples:app:assembleDebug" + ) + }.build() // Then assertThat(result.task(":samples:app:assembleDebug")?.outcome) @@ -360,11 +340,12 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--configuration-cache", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = gradleRunner { + withArguments( + "--configuration-cache", + ":samples:app:assembleRelease" + ) + }.build() // Then assertThat(result.task(":samples:app:assembleRelease")?.outcome) @@ -380,11 +361,8 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = + gradleRunner { withArguments("--info", ":samples:app:assembleRelease") }.build() // Then assertThat(result.task(":samples:app:assembleRelease")?.outcome) @@ -406,11 +384,7 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) - .build() + val result = gradleRunner { withArguments("--info", ":samples:app:assembleDebug") }.build() // Then assertThat(result.task(":samples:app:assembleDebug")?.outcome) @@ -433,10 +407,7 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) + gradleRunner { withArguments("--info", ":samples:app:assembleRelease") } .buildAndFail() } @@ -450,10 +421,7 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) + gradleRunner { withArguments("--info", ":samples:app:assembleDebug") } .buildAndFail() } @@ -467,10 +435,7 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { withArguments("--info", ":samples:app:assembleRelease") } .build() // Then @@ -488,10 +453,7 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleDebug") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { withArguments("--info", ":samples:app:assembleDebug") } .build() // Then @@ -501,6 +463,96 @@ internal class DdAndroidGradlePluginFunctionalTest { // endregion + // region BuildId + + @Test + fun `M inject build ID W assembleRelease`() { + // Given + stubGradleBuildFromResourceFile( + "build_with_datadog_dep.gradle", + appBuildGradleFile + ) + // When + val result = gradleRunner { + withArguments( + "--info", + "--configuration-cache", + ":samples:app:assembleRelease" + ) + } + .build() + + // Then + assertThat(result.task(":samples:app:assembleRelease")?.outcome) + .isEqualTo(TaskOutcome.SUCCESS) + + val apks = testProjectDir.walk() + .filter { it.isFile && it.extension == "apk" } + .map { ZipFile(it) } + .toList() + + assertThat(apks).isNotEmpty + + val buildIdFiles = apks.mapNotNull { it.getEntry(BUILD_ID_FILE_PATH_APK) } + + // each apk should contain one build ID file + assertThat(apks.size).isEqualTo(buildIdFiles.size) + + val buildIds = apks.map { + it.readBuildId(BUILD_ID_FILE_PATH_APK) + .let { UUID.fromString(it) } + } + + // all build IDs should be unique + assertThat(buildIds.toSet()).hasSize(apks.size) + } + + @Test + fun `M inject buildId W bundleRelease`() { + // Given + stubGradleBuildFromResourceFile( + "build_with_datadog_dep.gradle", + appBuildGradleFile + ) + // When + val result = gradleRunner { + withArguments( + "--info", + "--configuration-cache", + ":samples:app:bundleRelease" + ) + } + .build() + + // Then + assertThat(result.task(":samples:app:bundleRelease")?.outcome) + .isEqualTo(TaskOutcome.SUCCESS) + + val bundles = testProjectDir.walk() + .filter { + it.isFile && + !it.name.contains("intermediary") && + it.extension == "aab" + } + .map { ZipFile(it) } + .toList() + + assertThat(bundles).isNotEmpty + + val buildIdFiles = bundles.mapNotNull { it.getEntry(BUILD_ID_FILE_PATH_AAB) } + + // each bundle should contain one build ID file + assertThat(bundles.size).isEqualTo(buildIdFiles.size) + + val buildIds = bundles.map { + it.readBuildId(BUILD_ID_FILE_PATH_AAB) + .let { UUID.fromString(it) } + } + + // all build IDs should be unique + assertThat(buildIds.toSet()).hasSize(bundles.size) + } + // region Upload @Test @@ -520,25 +572,33 @@ internal class DdAndroidGradlePluginFunctionalTest { // since there is no explicit dependency between assemble and upload tasks, Gradle may // optimize the execution and run them in parallel, ignoring the order in the command // line, so we do the explicit split - GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) + gradleRunner { withArguments("--info", ":samples:app:assembleRelease") } .build() - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments(taskName, "--info", "--stacktrace", "-PDD_API_KEY=fakekey") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { + withArguments( + taskName, + "--info", + "--stacktrace", + "-PDD_API_KEY=fakekey" + ) + } .buildAndFail() // Then assertThat(result.output).contains("Creating request with GZIP encoding.") + + val buildIdInOriginFile = testProjectDir.findBuildIdInOriginFile(variant) + val buildIdInApk = testProjectDir.findBuildIdInApk(variant) + assertThat(buildIdInApk).isEqualTo(buildIdInOriginFile) + assertThat(result.output).contains( "Uploading mapping file with tags " + "`service:com.example.variants.$variantVersionName`, " + "`version:1.0-$variantVersionName`, " + - "`variant:$variant` (site=datadoghq.com):" + "`versionCode:1`, " + + "`variant:$variant`, " + + "`buildId:$buildIdInOriginFile` (site=datadoghq.com):" ) } @@ -559,25 +619,34 @@ internal class DdAndroidGradlePluginFunctionalTest { // since there is no explicit dependency between assemble and upload tasks, Gradle may // optimize the execution and run them in parallel, ignoring the order in the command // line, so we do the explicit split - GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) + gradleRunner { withArguments("--info", ":samples:app:assembleRelease") } .build() - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments(taskName, "--info", "--stacktrace", "-PDD_API_KEY=fakekey", "-Pdd-disable-gzip") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { + withArguments( + taskName, + "--info", + "--stacktrace", + "-PDD_API_KEY=fakekey", + "-Pdd-disable-gzip" + ) + } .buildAndFail() // Then assertThat(result.output).contains("Creating request without GZIP encoding.") + + val buildIdInOriginFile = testProjectDir.findBuildIdInOriginFile(variant) + val buildIdInApk = testProjectDir.findBuildIdInApk(variant) + assertThat(buildIdInApk).isEqualTo(buildIdInOriginFile) + assertThat(result.output).contains( "Uploading mapping file with tags " + "`service:com.example.variants.$variantVersionName`, " + "`version:1.0-$variantVersionName`, " + - "`variant:$variant` (site=datadoghq.com):" + "`versionCode:1`, " + + "`variant:$variant`, " + + "`buildId:$buildIdInOriginFile` (site=datadoghq.com):" ) } @@ -607,24 +676,24 @@ internal class DdAndroidGradlePluginFunctionalTest { // since there is no explicit dependency between assemble and upload tasks, Gradle may // optimize the execution and run them in parallel, ignoring the order in the command // line, so we do the explicit split - GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) + gradleRunner { withArguments("--info", ":samples:app:assembleRelease") } .build() - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments(taskName, "--info") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { withArguments(taskName, "--info") } .buildAndFail() // Then + val buildIdInOriginFile = testProjectDir.findBuildIdInOriginFile(variant) + val buildIdInApk = testProjectDir.findBuildIdInApk(variant) + assertThat(buildIdInApk).isEqualTo(buildIdInOriginFile) + assertThat(result.output).contains( "Uploading mapping file with tags " + "`service:com.example.variants.$variantVersionName`, " + "`version:1.0-$variantVersionName`, " + - "`variant:$variant` (site=datadoghq.eu):" + "`versionCode:1`, " + + "`variant:$variant`, " + + "`buildId:$buildIdInOriginFile` (site=datadoghq.eu):" ) assertThat(result.output).contains("API key found in Datadog CI config file, using it.") assertThat(result.output) @@ -645,24 +714,31 @@ internal class DdAndroidGradlePluginFunctionalTest { val taskName = resolveUploadTask(variant) // When - GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) + gradleRunner { withArguments("--info", ":samples:app:assembleRelease") } .build() - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments(taskName, "--info", "--stacktrace", "-PDD_API_KEY=fakekey") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { + withArguments( + taskName, + "--info", + "--stacktrace", + "-PDD_API_KEY=fakekey" + ) + } .buildAndFail() // Then + val buildIdInOriginFile = testProjectDir.findBuildIdInOriginFile(variant) + val buildIdInApk = testProjectDir.findBuildIdInApk(variant) + assertThat(buildIdInApk).isEqualTo(buildIdInOriginFile) + assertThat(result.output).contains( "Uploading mapping file with tags " + "`service:com.example.variants.$variantVersionName`, " + "`version:1.0-$variantVersionName`, " + - "`variant:$variant` (site=datadoghq.com):" + "`versionCode:1`, " + + "`variant:$variant`, " + + "`buildId:$buildIdInOriginFile` (site=datadoghq.com):" ) assertThat(result.output).contains( "http://github.com:fakeapp/repository.git" @@ -683,24 +759,31 @@ internal class DdAndroidGradlePluginFunctionalTest { val taskName = resolveUploadTask(variant) // When - GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) + gradleRunner { withArguments("--info", ":samples:app:assembleRelease") } .build() - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments(taskName, "--info", "--stacktrace", "-PDD_API_KEY=fakekey") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { + withArguments( + taskName, + "--info", + "--stacktrace", + "-PDD_API_KEY=fakekey" + ) + } .buildAndFail() // Then + val buildIdInOriginFile = testProjectDir.findBuildIdInOriginFile(variant) + val buildIdInApk = testProjectDir.findBuildIdInApk(variant) + assertThat(buildIdInApk).isEqualTo(buildIdInOriginFile) + assertThat(result.output).contains( "Uploading mapping file with tags " + "`service:com.example.variants.$variantVersionName`, " + "`version:1.0-$variantVersionName`, " + - "`variant:$variant` (site=datadoghq.com):" + "`versionCode:1`, " + + "`variant:$variant`, " + + "`buildId:$buildIdInOriginFile` (site=datadoghq.com):" ) val optimizedFile = Path( appRootDir.path, @@ -727,16 +810,17 @@ internal class DdAndroidGradlePluginFunctionalTest { val taskName = resolveUploadTask(variant) // When - GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:assembleRelease") - .withPluginClasspath(getTestConfigurationClasspath()) + gradleRunner { withArguments("--info", ":samples:app:assembleRelease") } .build() - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments(taskName, "--info", "--stacktrace", "-PDD_API_KEY=fakekey") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { + withArguments( + taskName, + "--info", + "--stacktrace", + "-PDD_API_KEY=fakekey" + ) + } .buildAndFail() // Then @@ -752,10 +836,7 @@ internal class DdAndroidGradlePluginFunctionalTest { ) // When - val result = GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("--info", ":samples:app:tasks") - .withPluginClasspath(getTestConfigurationClasspath()) + val result = gradleRunner { withArguments("--info", ":samples:app:tasks") } .build() // Then @@ -794,6 +875,45 @@ internal class DdAndroidGradlePluginFunctionalTest { .readImplementationClasspath("gradle-runner-classpath", properties) } + private fun gradleRunner(configure: GradleRunner.() -> Unit): GradleRunner { + return GradleRunner.create() + .withProjectDir(testProjectDir) + .withPluginClasspath(getTestConfigurationClasspath()) + .apply { + configure(this) + } + } + + private fun File.findBuildIdInOriginFile(variantName: String): String { + return walk() + .filter { + it.name == GenerateBuildIdTask.BUILD_ID_FILE_NAME && + it.path.contains(variantName) + } + .map { + it.readText() + } + .first() + } + + private fun File.findBuildIdInApk(variantName: String): String { + return walk() + .filter { + it.extension == "apk" && it.path.contains(variantName) + } + .map { + ZipFile(it).readBuildId(BUILD_ID_FILE_PATH_APK) + } + .first() + } + + private fun ZipFile.readBuildId(path: String): String { + return getInputStream(getEntry(path)) + .bufferedReader() + .readText() + .trim() + } + // endregion companion object { @@ -871,5 +991,8 @@ internal class DdAndroidGradlePluginFunctionalTest { const val OLD_AGP_VERSION = "7.1.2" const val LATEST_AGP_VERSION = "8.1.0" + + const val BUILD_ID_FILE_PATH_APK = "assets/datadog.buildId" + const val BUILD_ID_FILE_PATH_AAB = "base/assets/datadog.buildId" } } diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginTest.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginTest.kt index e5cb8e9e..d34ae193 100644 --- a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginTest.kt +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginTest.kt @@ -25,7 +25,10 @@ import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat import org.gradle.api.Project +import org.gradle.api.Transformer import org.gradle.api.artifacts.Configuration +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -35,11 +38,13 @@ import org.junit.jupiter.api.io.TempDir import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.io.File +import java.util.UUID @Extensions( ExtendWith(MockitoExtension::class), @@ -59,6 +64,8 @@ internal class DdAndroidGradlePluginTest { @Mock lateinit var mockBuildType: BuildType + lateinit var fakeBuildId: String + @Forgery lateinit var fakeExtension: DdExtension @@ -77,16 +84,17 @@ internal class DdAndroidGradlePluginTest { source = forge.aValueFrom(ApiKeySource::class.java) ) fakeFlavorNames = fakeFlavorNames.take(5) // A D F G A♭ A A♭ G F + fakeBuildId = forge.getForgery().toString() fakeProject = ProjectBuilder.builder().build() - testedPlugin = DdAndroidGradlePlugin(mock()) + testedPlugin = DdAndroidGradlePlugin(mock(), mock()) setEnv(DdAndroidGradlePlugin.DD_API_KEY, "") setEnv(DdAndroidGradlePlugin.DATADOG_API_KEY, "") } - // region configureVariant() + // region configureVariantForUploadTask() @Test - fun `𝕄 configure the upload task with the variant info 𝕎 configureVariant()`( + fun `𝕄 configure the upload task with the variant info 𝕎 configureVariantForUploadTask()`( @StringForgery(case = Case.LOWER) flavorName: String, @StringForgery(case = Case.LOWER) buildTypeName: String, @StringForgery versionName: String, @@ -109,6 +117,7 @@ internal class DdAndroidGradlePluginTest { val task = testedPlugin.configureVariantForUploadTask( fakeProject, mockVariant, + mockBuildIdGenerationTask(fakeBuildId), fakeApiKey, fakeExtension ) @@ -130,10 +139,11 @@ internal class DdAndroidGradlePluginTest { assertThat(task.mappingFilePackagesAliases) .isEqualTo(fakeExtension.mappingFilePackageAliases) assertThat(task.datadogCiFile).isNull() + assertThat(task.buildId.get()).isEqualTo(fakeBuildId) } @Test - fun `𝕄 configure the upload task with the extension info 𝕎 configureVariant()`( + fun `𝕄 configure the upload task with the extension info 𝕎 configureVariantForUploadTask()`( @StringForgery(case = Case.LOWER) flavorName: String, @StringForgery(case = Case.LOWER) buildTypeName: String, @StringForgery versionName: String, @@ -153,6 +163,7 @@ internal class DdAndroidGradlePluginTest { val task = testedPlugin.configureVariantForUploadTask( fakeProject, mockVariant, + mockBuildIdGenerationTask(fakeBuildId), fakeApiKey, fakeExtension ) @@ -176,10 +187,11 @@ internal class DdAndroidGradlePluginTest { assertThat(task.mappingFileTrimIndents) .isEqualTo(fakeExtension.mappingFileTrimIndents) assertThat(task.datadogCiFile).isNull() + assertThat(task.buildId.get()).isEqualTo(fakeBuildId) } @Test - fun `𝕄 configure the upload task with sanitized mapping aliases 𝕎 configureVariant()`( + fun `𝕄 configure the upload task with sanitized mapping aliases 𝕎 configureVariantForUploadTask()`( @StringForgery(case = Case.LOWER) flavorName: String, @StringForgery(case = Case.LOWER) buildTypeName: String, @StringForgery versionName: String, @@ -209,6 +221,7 @@ internal class DdAndroidGradlePluginTest { val task = testedPlugin.configureVariantForUploadTask( fakeProject, mockVariant, + mockBuildIdGenerationTask(fakeBuildId), fakeApiKey, fakeExtension ) @@ -232,10 +245,11 @@ internal class DdAndroidGradlePluginTest { assertThat(task.mappingFileTrimIndents) .isEqualTo(fakeExtension.mappingFileTrimIndents) assertThat(task.datadogCiFile).isNull() + assertThat(task.buildId.get()).isEqualTo(fakeBuildId) } @Test - fun `𝕄 use sensible defaults 𝕎 configureVariant() { empty config }`( + fun `𝕄 use sensible defaults 𝕎 configureVariantForUploadTask() { empty config }`( @StringForgery(case = Case.LOWER) flavorName: String, @StringForgery(case = Case.LOWER) buildTypeName: String, @StringForgery versionName: String, @@ -263,6 +277,7 @@ internal class DdAndroidGradlePluginTest { val task = testedPlugin.configureVariantForUploadTask( fakeProject, mockVariant, + mockBuildIdGenerationTask(fakeBuildId), fakeApiKey, fakeExtension ) @@ -284,10 +299,11 @@ internal class DdAndroidGradlePluginTest { assertThat(task.mappingFilePackagesAliases).isEmpty() assertThat(task.mappingFileTrimIndents).isFalse assertThat(task.datadogCiFile).isNull() + assertThat(task.buildId.get()).isEqualTo(fakeBuildId) } @Test - fun `𝕄 apply datadog CI file 𝕎 configureVariant()`( + fun `𝕄 apply datadog CI file 𝕎 configureVariantForUploadTask()`( @StringForgery(case = Case.LOWER) flavorName: String, @StringForgery(case = Case.LOWER) buildTypeName: String, @StringForgery versionName: String, @@ -319,6 +335,7 @@ internal class DdAndroidGradlePluginTest { val task = testedPlugin.configureVariantForUploadTask( fakeProject, mockVariant, + mockBuildIdGenerationTask(fakeBuildId), fakeApiKey, fakeExtension ) @@ -340,10 +357,11 @@ internal class DdAndroidGradlePluginTest { assertThat(task.mappingFilePackagesAliases).isEmpty() assertThat(task.mappingFileTrimIndents).isFalse assertThat(task.datadogCiFile).isEqualTo(fakeDatadogCiFile) + assertThat(task.buildId.get()).isEqualTo(fakeBuildId) } @Test - fun `𝕄 not apply datadog CI file 𝕎 configureVariant() { ignoreDatadogCiFileConfig }`( + fun `𝕄 not apply datadog CI file 𝕎 configureVariantForUploadTask() { ignoreDatadogCiFileConfig }`( @StringForgery(case = Case.LOWER) flavorName: String, @StringForgery(case = Case.LOWER) buildTypeName: String, @StringForgery versionName: String, @@ -375,6 +393,7 @@ internal class DdAndroidGradlePluginTest { val task = testedPlugin.configureVariantForUploadTask( fakeProject, mockVariant, + mockBuildIdGenerationTask(fakeBuildId), fakeApiKey, fakeExtension ) @@ -396,10 +415,11 @@ internal class DdAndroidGradlePluginTest { assertThat(task.mappingFilePackagesAliases).isEmpty() assertThat(task.mappingFileTrimIndents).isFalse assertThat(task.datadogCiFile).isNull() + assertThat(task.buildId.get()).isEqualTo(fakeBuildId) } @Test - fun `𝕄 do nothing 𝕎 configureVariant() { no deobfuscation }`( + fun `𝕄 not create upload and buildId tasks 𝕎 configureTasksForVariant() { no deobfuscation }`( @StringForgery(case = Case.LOWER) flavorName: String, @StringForgery(case = Case.LOWER) buildTypeName: String, @StringForgery versionName: String, @@ -415,19 +435,22 @@ internal class DdAndroidGradlePluginTest { whenever(mockBuildType.name) doReturn fakeBuildTypeName // When - val task = testedPlugin.configureVariantForUploadTask( + testedPlugin.configureTasksForVariant( fakeProject, + mock(), + fakeExtension, mockVariant, - fakeApiKey, - fakeExtension + fakeApiKey ) // Then - assertThat(task).isNull() + val allTasks = fakeProject.tasks.map { it.name } + assertThat(allTasks).allMatch { !it.startsWith(DdAndroidGradlePlugin.UPLOAD_TASK_NAME) } + assertThat(allTasks).allMatch { !it.startsWith(GenerateBuildIdTask.TASK_NAME) } } @Test - fun `𝕄 configure the upload task 𝕎 configureVariant() { non default obfuscation }`( + fun `𝕄 configure the upload task 𝕎 configureVariantForUploadTask() { non default obfuscation }`( @StringForgery(case = Case.LOWER) flavorName: String, @StringForgery(case = Case.LOWER) buildTypeName: String, @StringForgery versionName: String, @@ -448,6 +471,7 @@ internal class DdAndroidGradlePluginTest { val task = testedPlugin.configureVariantForUploadTask( fakeProject, mockVariant, + mockBuildIdGenerationTask(fakeBuildId), fakeApiKey, fakeExtension ) @@ -471,6 +495,7 @@ internal class DdAndroidGradlePluginTest { assertThat(task.mappingFileTrimIndents) .isEqualTo(fakeExtension.mappingFileTrimIndents) assertThat(task.datadogCiFile).isNull() + assertThat(task.buildId.get()).isEqualTo(fakeBuildId) } // endregion @@ -1176,5 +1201,16 @@ internal class DdAndroidGradlePluginTest { return tree } + private fun mockBuildIdGenerationTask(buildId: String): TaskProvider { + return mock>().apply { + val mockBuildIdProvider = mock>().apply { + whenever(get()) doReturn buildId + } + whenever( + flatMap(any, GenerateBuildIdTask>>()) + ) doReturn mockBuildIdProvider + } + } + // endregion } diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdMappingFileUploadTaskTest.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdMappingFileUploadTaskTest.kt index 2828df21..5e2902a9 100644 --- a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdMappingFileUploadTaskTest.kt +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdMappingFileUploadTaskTest.kt @@ -12,10 +12,12 @@ import com.datadog.gradle.plugin.internal.DdAppIdentifier import com.datadog.gradle.plugin.internal.Uploader import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat +import org.gradle.api.provider.Provider import org.gradle.testfixtures.ProjectBuilder import org.json.JSONArray import org.json.JSONObject @@ -34,11 +36,13 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.io.File +import java.util.UUID @Extensions( ExtendWith(MockitoExtension::class), @@ -62,9 +66,14 @@ internal class DdMappingFileUploadTaskTest { @StringForgery lateinit var fakeVariant: String + lateinit var fakeBuildId: String + @StringForgery lateinit var fakeVersion: String + @IntForgery(min = 0) + var fakeVersionCode: Int = 0 + @StringForgery lateinit var fakeService: String @@ -105,12 +114,18 @@ internal class DdMappingFileUploadTaskTest { value = forge.anHexadecimalString(), source = forge.aValueFrom(ApiKeySource::class.java) ) + fakeBuildId = forge.getForgery().toString() testedTask.apiKey = fakeApiKey.value testedTask.apiKeySource = fakeApiKey.source testedTask.variantName = fakeVariant testedTask.versionName = fakeVersion + testedTask.versionCode = fakeVersionCode testedTask.serviceName = fakeService testedTask.site = fakeSite.name + testedTask.buildId = mock>().apply { + whenever(isPresent) doReturn true + whenever(get()) doReturn fakeBuildId + } setEnv(DdMappingFileUploadTask.DATADOG_SITE, "") } @@ -142,7 +157,9 @@ internal class DdMappingFileUploadTaskTest { DdAppIdentifier( serviceName = fakeService, version = fakeVersion, - variant = fakeVariant + versionCode = fakeVersionCode, + variant = fakeVariant, + buildId = fakeBuildId ), fakeRepoInfo, useGzip = true @@ -188,7 +205,9 @@ internal class DdMappingFileUploadTaskTest { DdAppIdentifier( serviceName = fakeService, version = fakeVersion, - variant = fakeVariant + versionCode = fakeVersionCode, + variant = fakeVariant, + buildId = fakeBuildId ) ), eq(fakeRepoInfo), @@ -237,7 +256,9 @@ internal class DdMappingFileUploadTaskTest { DdAppIdentifier( serviceName = fakeService, version = fakeVersion, - variant = fakeVariant + versionCode = fakeVersionCode, + variant = fakeVariant, + buildId = fakeBuildId ) ), eq(fakeRepoInfo), @@ -290,7 +311,9 @@ internal class DdMappingFileUploadTaskTest { DdAppIdentifier( serviceName = fakeService, version = fakeVersion, - variant = fakeVariant + versionCode = fakeVersionCode, + variant = fakeVariant, + buildId = fakeBuildId ) ), eq(fakeRepoInfo), @@ -324,7 +347,9 @@ internal class DdMappingFileUploadTaskTest { DdAppIdentifier( serviceName = fakeService, version = fakeVersion, - variant = fakeVariant + versionCode = fakeVersionCode, + variant = fakeVariant, + buildId = fakeBuildId ), fakeRepoInfo, useGzip = true @@ -358,7 +383,9 @@ internal class DdMappingFileUploadTaskTest { DdAppIdentifier( serviceName = fakeService, version = fakeVersion, - variant = fakeVariant + versionCode = fakeVersionCode, + variant = fakeVariant, + buildId = fakeBuildId ), null, useGzip = true @@ -375,11 +402,12 @@ internal class DdMappingFileUploadTaskTest { testedTask.apiKeySource = ApiKeySource.NONE // When - assertThrows { + val exception = assertThrows { testedTask.applyTask() } // Then + assertThat(exception.message).isEqualTo(DdMappingFileUploadTask.API_KEY_MISSING_ERROR) verifyNoInteractions(mockUploader) } @@ -400,11 +428,50 @@ internal class DdMappingFileUploadTaskTest { testedTask.apiKeySource = ApiKeySource.NONE // When - assertThrows { + val exception = assertThrows { + testedTask.applyTask() + } + + // Then + assertThat(exception.message) + .isEqualTo(DdMappingFileUploadTask.INVALID_API_KEY_FORMAT_ERROR) + verifyNoInteractions(mockUploader) + } + + @Test + fun `𝕄 throw error 𝕎 applyTask() {buildId is missing}`() { + // Given + val fakeMappingFile = File(tempDir, fakeMappingFileName) + fakeMappingFile.writeText(fakeMappingFileContent) + testedTask.mappingFilePath = fakeMappingFile.path + whenever(testedTask.buildId.isPresent) doReturn false + + // When + val exception = assertThrows { + testedTask.applyTask() + } + + // Then + assertThat(exception.message).isEqualTo(DdMappingFileUploadTask.MISSING_BUILD_ID_ERROR) + verifyNoInteractions(mockUploader) + } + + @Test + fun `𝕄 throw error 𝕎 applyTask() {buildId is empty string}`() { + // Given + val fakeMappingFile = File(tempDir, fakeMappingFileName) + fakeMappingFile.writeText(fakeMappingFileContent) + testedTask.mappingFilePath = fakeMappingFile.path + whenever(testedTask.buildId.isPresent) doReturn true + whenever(testedTask.buildId.get()) doReturn "" + + // When + val exception = assertThrows { testedTask.applyTask() } // Then + assertThat(exception.message).isEqualTo(DdMappingFileUploadTask.MISSING_BUILD_ID_ERROR) verifyNoInteractions(mockUploader) } @@ -453,7 +520,9 @@ internal class DdMappingFileUploadTaskTest { DdAppIdentifier( serviceName = fakeService, version = fakeVersion, - variant = fakeVariant + versionCode = fakeVersionCode, + variant = fakeVariant, + buildId = fakeBuildId ), fakeRepoInfo, useGzip = true diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/IdentifierForgeryFactory.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/IdentifierForgeryFactory.kt index 5a8fc708..2cd388cb 100644 --- a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/IdentifierForgeryFactory.kt +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/IdentifierForgeryFactory.kt @@ -9,13 +9,16 @@ package com.datadog.gradle.plugin import com.datadog.gradle.plugin.internal.DdAppIdentifier import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory +import java.util.UUID internal class IdentifierForgeryFactory : ForgeryFactory { override fun getForgery(forge: Forge): DdAppIdentifier { return DdAppIdentifier( serviceName = forge.aStringMatching("[a-z]{3}(\\.[a-z]{5,10}){2,4}"), version = forge.aStringMatching("\\d\\.\\d{1,2}\\.\\d{1,3}"), - variant = forge.anAlphabeticalString() + versionCode = forge.aPositiveInt(), + variant = forge.anAlphabeticalString(), + buildId = forge.getForgery().toString() ) } } diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploaderTest.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploaderTest.kt index 0eb73ff5..bad782c5 100644 --- a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploaderTest.kt +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploaderTest.kt @@ -171,8 +171,10 @@ internal class OkHttpUploaderTest { "event", "{\"service\":\"${fakeIdentifier.serviceName}\"," + "\"variant\":\"${fakeIdentifier.variant}\"," + + "\"buildId\":\"${fakeIdentifier.buildId}\"," + "\"type\":\"${OkHttpUploader.TYPE_JVM_MAPPING_FILE}\"," + - "\"version\":\"${fakeIdentifier.version}\"}", + "\"version\":\"${fakeIdentifier.version}\"," + + "\"versionCode\":${fakeIdentifier.versionCode}}", "application/json; charset=utf-8" ) .containsMultipartFile( @@ -219,8 +221,10 @@ internal class OkHttpUploaderTest { "event", "{\"service\":\"${fakeIdentifier.serviceName}\"," + "\"variant\":\"${fakeIdentifier.variant}\"," + + "\"buildId\":\"${fakeIdentifier.buildId}\"," + "\"type\":\"${OkHttpUploader.TYPE_JVM_MAPPING_FILE}\"," + - "\"version\":\"${fakeIdentifier.version}\"}", + "\"version\":\"${fakeIdentifier.version}\"," + + "\"versionCode\":${fakeIdentifier.versionCode}}", "application/json; charset=utf-8" ) .containsMultipartFile( @@ -265,8 +269,10 @@ internal class OkHttpUploaderTest { "event", "{\"service\":\"${fakeIdentifier.serviceName}\"," + "\"variant\":\"${fakeIdentifier.variant}\"," + + "\"buildId\":\"${fakeIdentifier.buildId}\"," + "\"type\":\"${OkHttpUploader.TYPE_JVM_MAPPING_FILE}\"," + - "\"version\":\"${fakeIdentifier.version}\"}", + "\"version\":\"${fakeIdentifier.version}\"," + + "\"versionCode\":${fakeIdentifier.versionCode}}", "application/json; charset=utf-8" ) .containsMultipartFile( @@ -312,8 +318,10 @@ internal class OkHttpUploaderTest { "event", "{\"service\":\"${fakeIdentifier.serviceName}\"," + "\"variant\":\"${fakeIdentifier.variant}\"," + + "\"buildId\":\"${fakeIdentifier.buildId}\"," + "\"type\":\"${OkHttpUploader.TYPE_JVM_MAPPING_FILE}\"," + - "\"version\":\"${fakeIdentifier.version}\"}", + "\"version\":\"${fakeIdentifier.version}\"," + + "\"versionCode\":${fakeIdentifier.versionCode}}", "application/json; charset=utf-8" ) .containsMultipartFile( @@ -368,8 +376,10 @@ internal class OkHttpUploaderTest { "event", "{\"service\":\"${fakeIdentifier.serviceName}\"," + "\"variant\":\"${fakeIdentifier.variant}\"," + + "\"buildId\":\"${fakeIdentifier.buildId}\"," + "\"type\":\"${OkHttpUploader.TYPE_JVM_MAPPING_FILE}\"," + - "\"version\":\"${fakeIdentifier.version}\"}", + "\"version\":\"${fakeIdentifier.version}\"," + + "\"versionCode\":${fakeIdentifier.versionCode}}", "application/json; charset=utf-8" ) .containsMultipartFile( @@ -421,8 +431,10 @@ internal class OkHttpUploaderTest { "event", "{\"service\":\"${fakeIdentifier.serviceName}\"," + "\"variant\":\"${fakeIdentifier.variant}\"," + + "\"buildId\":\"${fakeIdentifier.buildId}\"," + "\"type\":\"${OkHttpUploader.TYPE_JVM_MAPPING_FILE}\"," + - "\"version\":\"${fakeIdentifier.version}\"}", + "\"version\":\"${fakeIdentifier.version}\"," + + "\"versionCode\":${fakeIdentifier.versionCode}}", "application/json; charset=utf-8" ) .containsMultipartFile( @@ -490,8 +502,10 @@ internal class OkHttpUploaderTest { "event", "{\"service\":\"${fakeIdentifier.serviceName}\"," + "\"variant\":\"${fakeIdentifier.variant}\"," + + "\"buildId\":\"${fakeIdentifier.buildId}\"," + "\"type\":\"${OkHttpUploader.TYPE_JVM_MAPPING_FILE}\"," + - "\"version\":\"${fakeIdentifier.version}\"}", + "\"version\":\"${fakeIdentifier.version}\"," + + "\"versionCode\":${fakeIdentifier.versionCode}}", "application/json; charset=utf-8" ) .containsMultipartFile( @@ -547,8 +561,10 @@ internal class OkHttpUploaderTest { "event", "{\"service\":\"${fakeIdentifier.serviceName}\"," + "\"variant\":\"${fakeIdentifier.variant}\"," + + "\"buildId\":\"${fakeIdentifier.buildId}\"," + "\"type\":\"${OkHttpUploader.TYPE_JVM_MAPPING_FILE}\"," + - "\"version\":\"${fakeIdentifier.version}\"}", + "\"version\":\"${fakeIdentifier.version}\"," + + "\"versionCode\":${fakeIdentifier.versionCode}}", "application/json; charset=utf-8" ) .containsMultipartFile( @@ -603,8 +619,10 @@ internal class OkHttpUploaderTest { "event", "{\"service\":\"${fakeIdentifier.serviceName}\"," + "\"variant\":\"${fakeIdentifier.variant}\"," + + "\"buildId\":\"${fakeIdentifier.buildId}\"," + "\"type\":\"${OkHttpUploader.TYPE_JVM_MAPPING_FILE}\"," + - "\"version\":\"${fakeIdentifier.version}\"}", + "\"version\":\"${fakeIdentifier.version}\"," + + "\"versionCode\":${fakeIdentifier.versionCode}}", "application/json; charset=utf-8" ) .containsMultipartFile(