diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d34c666d..1ba0c8187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog -## Unreleased +## 6.4.0-alpha.4 + +### Internal Changes 🔧 + +#### Deps + +- Update CLI to v3.3.5 by @github-actions in [#1132](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1132) +- Update CLI to v3.3.4 by @github-actions in [#1122](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1122) + +## 6.4.0-alpha.3 ### Dependencies diff --git a/plugin-build/gradle.properties b/plugin-build/gradle.properties index 7871fb815..378c4a857 100644 --- a/plugin-build/gradle.properties +++ b/plugin-build/gradle.properties @@ -7,7 +7,7 @@ org.gradle.parallel=true name = sentry-android-gradle-plugin group = io.sentry -version = 6.3.0 +version = 6.4.0-alpha.4 sdk_version = 8.37.1 # publication pom properties diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt index 711097cb8..efc48c90b 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt @@ -469,11 +469,11 @@ private fun ApplicationVariant.configureSnapshotsTasks( cliExecutable: Provider, sentryOrg: String?, sentryProject: String?, -) { +): TaskProvider { val variant = AndroidVariant74(this) val sentryProps = getPropertiesFilePath(project, variant) - SentryUploadSnapshotsTask.register( + return SentryUploadSnapshotsTask.register( project = project, extension = extension, sentryTelemetryProvider = sentryTelemetryProvider, diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTask.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTask.kt index ba3421541..0ab868f3e 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTask.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTask.kt @@ -108,6 +108,8 @@ import app.cash.paparazzi.TestName import app.cash.paparazzi.detectEnvironment import com.android.ide.common.rendering.api.SessionParams import com.android.resources.* +import java.io.File +import java.util.Locale import kotlin.math.ceil import org.junit.Rule import org.junit.Test @@ -219,6 +221,7 @@ private object PaparazziPreviewRule { return Paparazzi( environment = detectEnvironment().copy(compileSdkVersion = previewApiLevel), deviceConfig = DeviceConfigBuilder.build(preview.previewInfo), + theme = "android:Theme.Translucent.NoTitleBar", supportsRtl = true, showSystemUi = previewInfo.showSystemUi, renderingMode = when { @@ -328,7 +331,47 @@ class $CLASS_NAME( } } } + + writeSidecarMetadata(screenshotId, preview) + } + + private fun writeSidecarMetadata( + screenshotId: String, + preview: ComposablePreview, + ) { + val snapshotDir = File(System.getProperty("paparazzi.snapshot.dir")) + val imagesDir = File(snapshotDir, "images") + imagesDir.mkdirs() + val info = preview.previewInfo + val metadata = linkedMapOf( + "display_name" to screenshotId.removePrefix(preview.declaringClass + "."), + "image_file_name" to screenshotId, + "className" to preview.declaringClass, + "methodName" to preview.methodName, + ) + if (info.group.isNotBlank()) metadata["group"] = info.group + if (info.name.isNotBlank()) metadata["previewName"] = info.name + if (info.locale.isNotBlank()) metadata["locale"] = info.locale + if (info.device.isNotBlank()) metadata["device"] = info.device + metadata["nightMode"] = (info.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) + if (info.fontScale != 1f) metadata["fontScale"] = info.fontScale + if (info.apiLevel != -1) metadata["apiLevel"] = info.apiLevel + if (info.widthDp > 0) metadata["widthDp"] = info.widthDp + if (info.heightDp > 0) metadata["heightDp"] = info.heightDp + if (info.showSystemUi) metadata["showSystemUi"] = true + if (info.showBackground) metadata["showBackground"] = true + + val json = metadata.entries.joinToString(",\n ", prefix = "{\n ", postfix = "\n}") { (k, v) -> + if (v is String) "\"" + k + "\": \"" + escapeJson(v) + "\"" + else "\"" + k + "\": " + v + } + val sidecarName = "Paparazzi_Preview_Test_" + + screenshotId.lowercase(Locale.US).replace("\\s".toRegex(), "_") + File(imagesDir, "${'$'}{sidecarName}.json").writeText(json) } + + private fun escapeJson(s: String): String = + s.replace("\\", "\\\\").replace("\"", "\\\"") } """ .trimStart() diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/SentrySnapshotPlugin.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/SentrySnapshotPlugin.kt index d8ca1a091..2f9faa7bf 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/SentrySnapshotPlugin.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/SentrySnapshotPlugin.kt @@ -1,7 +1,7 @@ package io.sentry.android.gradle.snapshot import com.android.build.api.variant.ApplicationAndroidComponentsExtension -import com.android.build.api.variant.HostTestBuilder +import com.android.build.api.variant.HostTestBuilder.Companion.UNIT_TEST_TYPE import com.android.build.gradle.BaseExtension import io.sentry.android.gradle.util.AgpVersions import kotlin.jvm.java @@ -35,19 +35,21 @@ class SentrySnapshotPlugin : Plugin { // Right now it seems we only have HostTestBuilder.UNIT_TEST_TYPE as the key but we are // creating screenshot tests like HostTestBuilder.SCREENSHOT_TEST_TYPE // We should adjust this once the API is stable and documented. - variant.hostTests[HostTestBuilder.UNIT_TEST_TYPE] - // Using `sources?.kotlin` is broken so we have to use sources?.java: - // https://issuetracker.google.com/issues/268248348 - ?.sources - ?.java - ?.addGeneratedSourceDirectory(generateTask, GenerateSnapshotTestsTask::outputDir) + variant.hostTests[UNIT_TEST_TYPE]?.apply { + sources.java?.addGeneratedSourceDirectory( + generateTask, + GenerateSnapshotTestsTask::outputDir, + ) + } } else { - // `unitTest` is deprecated, the replacement above is complex @Suppress("DEPRECATION_ERROR") - variant.unitTest - ?.sources - ?.java - ?.addGeneratedSourceDirectory(generateTask, GenerateSnapshotTestsTask::outputDir) + // `unitTest` is deprecated, the replacement above is complex + variant.unitTest?.apply { + sources.java?.addGeneratedSourceDirectory( + generateTask, + GenerateSnapshotTestsTask::outputDir, + ) + } } } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/metadata/PreviewConstants.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/metadata/PreviewConstants.kt index 21d81eff4..eda2f1e9b 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/metadata/PreviewConstants.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/metadata/PreviewConstants.kt @@ -47,7 +47,8 @@ private const val SQUARE = "id:wearos_square" // region UI mode constants // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/res/Configuration.java -private const val UI_MODE_NIGHT_YES: Int = 32 +internal const val UI_MODE_NIGHT_MASK: Int = 0x30 +internal const val UI_MODE_NIGHT_YES: Int = 0x20 private const val UI_MODE_TYPE_NORMAL: Int = 1 // endregion diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTaskTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTaskTest.kt new file mode 100644 index 000000000..1f63f513b --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTaskTest.kt @@ -0,0 +1,151 @@ +package io.sentry.android.gradle.snapshot + +import java.io.File +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class GenerateSnapshotTestsTaskTest { + + @get:Rule val tmpDir = TemporaryFolder() + + @Test + fun `generates test file in correct package directory`() { + val task = createTask(packageTrees = listOf("com.example")) + + task.generate() + + val outputDir = task.outputDir.get().asFile + val expectedFile = File(outputDir, "io/sentry/snapshot/ComposablePreviewSnapshotTest.kt") + assertTrue(expectedFile.exists()) + } + + @Test + fun `cleans output directory on re-run`() { + val task = createTask(packageTrees = listOf("com.example")) + val outputDir = task.outputDir.get().asFile + + // Create a stale file in the output directory + val staleDir = File(outputDir, "io/sentry/snapshot") + staleDir.mkdirs() + val staleFile = File(staleDir, "OldFile.kt") + staleFile.writeText("old content") + assertTrue(staleFile.exists()) + + task.generate() + + assertFalse(staleFile.exists()) + val expectedFile = File(outputDir, "io/sentry/snapshot/ComposablePreviewSnapshotTest.kt") + assertTrue(expectedFile.exists()) + } + + @Test + fun `generated file contains correct package declaration`() { + val content = generateAndRead(packageTrees = listOf("com.example")) + + assertTrue(content.contains("package io.sentry.snapshot")) + } + + @Test + fun `generated file scans configured package tree`() { + val content = generateAndRead(packageTrees = listOf("com.example.app")) + + assertTrue(content.contains(".scanPackageTrees(\"com.example.app\")")) + } + + @Test + fun `generated file scans multiple package trees`() { + val content = + generateAndRead(packageTrees = listOf("com.example.feature1", "com.example.feature2")) + + assertTrue( + content.contains(".scanPackageTrees(\"com.example.feature1\", \"com.example.feature2\")") + ) + } + + @Test + fun `generated file includes private previews when enabled`() { + val content = + generateAndRead(packageTrees = listOf("com.example"), includePrivatePreviews = true) + + assertTrue(content.contains(".includePrivatePreviews()")) + } + + @Test + fun `generated file excludes private previews by default`() { + val content = generateAndRead(packageTrees = listOf("com.example")) + + assertFalse(content.contains(".includePrivatePreviews()")) + } + + @Test + fun `generated file contains parameterized test runner`() { + val content = generateAndRead(packageTrees = listOf("com.example")) + + assertTrue(content.contains("@RunWith(Parameterized::class)")) + assertTrue(content.contains("class ComposablePreviewSnapshotTest")) + } + + @Test + fun `generated sidecar metadata uses short display name`() { + val content = generateAndRead(packageTrees = listOf("com.example")) + + assertTrue( + content.contains( + "\"display_name\" to screenshotId.removePrefix(preview.declaringClass + \".\")" + ) + ) + } + + @Test + fun `generated sidecar metadata uses full screenshotId for image file name`() { + val content = generateAndRead(packageTrees = listOf("com.example")) + + assertTrue(content.contains("\"image_file_name\" to screenshotId")) + } + + @Test + fun `generated file writes sidecar json to images directory`() { + val content = generateAndRead(packageTrees = listOf("com.example")) + + assertTrue(content.contains("val imagesDir = File(snapshotDir, \"images\")")) + assertTrue(content.contains("File(imagesDir, \"\${sidecarName}.json\").writeText(json)")) + } + + @Test + fun `generated sidecar filename is lowercased to match Paparazzi image filenames`() { + val content = generateAndRead(packageTrees = listOf("com.example")) + + assertTrue( + content.contains("screenshotId.lowercase(Locale.US).replace(\"\\\\s\".toRegex(), \"_\")") + ) + } + + private fun generateAndRead( + packageTrees: List, + includePrivatePreviews: Boolean = false, + ): String { + val task = createTask(packageTrees, includePrivatePreviews) + task.generate() + val file = + File(task.outputDir.get().asFile, "io/sentry/snapshot/ComposablePreviewSnapshotTest.kt") + return file.readText() + } + + private fun createTask( + packageTrees: List, + includePrivatePreviews: Boolean = false, + ): GenerateSnapshotTestsTask { + val project = ProjectBuilder.builder().build() + return project.tasks + .register("testGenerateSnapshotTests", GenerateSnapshotTestsTask::class.java) { task -> + task.includePrivatePreviews.set(includePrivatePreviews) + task.packageTrees.set(packageTrees) + task.outputDir.set(tmpDir.newFolder("output")) + } + .get() + } +} diff --git a/sentry-kotlin-compiler-plugin/gradle.properties b/sentry-kotlin-compiler-plugin/gradle.properties index 72fb3453c..ff837adbb 100644 --- a/sentry-kotlin-compiler-plugin/gradle.properties +++ b/sentry-kotlin-compiler-plugin/gradle.properties @@ -4,7 +4,7 @@ org.gradle.parallel=true GROUP = io.sentry POM_ARTIFACT_ID = sentry-kotlin-compiler-plugin -VERSION_NAME = 6.3.0 +VERSION_NAME = 6.4.0-alpha.4 # publication pom properties POM_NAME=Sentry Kotlin Compiler Plugin