Skip to content

Commit bb29e02

Browse files
runningcodeclaude
andauthored
fix(snapshots): Generate Paparazzi 1.x compatible snapshot test code (#1142)
* fix(snapshots): Generate Paparazzi 1.x compatible snapshot test code The generated ComposablePreviewSnapshotTest called HtmlReportWriter(maxPercentDifference=...) which only compiles against Paparazzi 2.x. In 1.x, HtmlReportWriter has no maxPercentDifference parameter and accepts a no-arg constructor instead. Detect the Paparazzi version from the declared testImplementation dependencies and conditionally generate the appropriate HtmlReportWriter constructor call for each major version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(snapshots): Handle dynamic Paparazzi version strings Extract version parsing into a testable parseMajorVersion() function that takes the leading digits from the version string instead of requiring strict semver. This handles dynamic versions like "1.+" which SemVer.parse() would reject, causing the task to fail. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(snapshots): Use project compileSdk instead of hardcoded MAX_API_LEVEL When a preview doesn't specify an apiLevel, use the compileSdkVersion from detectEnvironment() (which reflects the project's actual compileSdk) instead of overriding it with a hardcoded MAX_API_LEVEL=36. The hardcoded value caused Renderer.configureBuildProperties to crash on Paparazzi 1.3.5 when its bundled layoutlib didn't support the SDK level. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8cbd27a commit bb29e02

3 files changed

Lines changed: 67 additions & 7 deletions

File tree

plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,12 +495,22 @@ private fun ApplicationVariant.configureSnapshotsTasks(
495495
"io.github.sergio-sastre.ComposablePreviewScanner:android:0.8.1",
496496
)
497497

498+
val paparazziMajorVersion =
499+
project.provider {
500+
val dep =
501+
project.configurations.findByName("testImplementation")?.allDependencies?.find {
502+
it.group == "app.cash.paparazzi" && it.name == "paparazzi"
503+
}
504+
parseMajorVersion(dep?.version)
505+
}
506+
498507
val generateTask =
499508
GenerateSnapshotTestsTask.register(
500509
project,
501510
extension.snapshots,
502511
android,
503512
this@configureSnapshotsTasks,
513+
paparazziMajorVersion,
504514
)
505515

506516
if (AgpVersions.isAGP90(AgpVersions.CURRENT)) {
@@ -537,6 +547,9 @@ private fun ApplicationVariant.configureSnapshotsTasks(
537547
}
538548
}
539549

550+
internal fun parseMajorVersion(version: String?, defaultVersion: Int = 2): Int =
551+
version?.trimStart()?.takeWhile { it.isDigit() }?.toIntOrNull() ?: defaultVersion
552+
540553
/**
541554
* Configure the upload AAB and APK tasks and set them up as finalizers on the respective producer
542555
* tasks

plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTask.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.gradle.api.Project
1010
import org.gradle.api.file.DirectoryProperty
1111
import org.gradle.api.provider.ListProperty
1212
import org.gradle.api.provider.Property
13+
import org.gradle.api.provider.Provider
1314
import org.gradle.api.tasks.CacheableTask
1415
import org.gradle.api.tasks.Input
1516
import org.gradle.api.tasks.Optional
@@ -31,6 +32,8 @@ abstract class GenerateSnapshotTestsTask : DefaultTask() {
3132

3233
@get:Input @get:Optional abstract val theme: Property<String>
3334

35+
@get:Input abstract val paparazziMajorVersion: Property<Int>
36+
3437
@get:OutputDirectory abstract val outputDir: DirectoryProperty
3538

3639
@TaskAction
@@ -48,6 +51,7 @@ abstract class GenerateSnapshotTestsTask : DefaultTask() {
4851
includePrivatePreviews = includePrivatePreviews.get(),
4952
packageTrees = packageTrees.get(),
5053
theme = theme.orNull,
54+
paparazziMajorVersion = paparazziMajorVersion.get(),
5155
)
5256
File(packageDir, "$CLASS_NAME.kt").writeText(content)
5357
logger.lifecycle("Generated snapshot test: ${packageDir.absolutePath}/$CLASS_NAME.kt")
@@ -62,13 +66,15 @@ abstract class GenerateSnapshotTestsTask : DefaultTask() {
6266
extension: SnapshotsExtension,
6367
android: BaseExtension,
6468
variant: ApplicationVariant,
69+
paparazziMajorVersion: Provider<Int>,
6570
): TaskProvider<GenerateSnapshotTestsTask> {
6671
return project.tasks.register(
6772
"sentryGenerateSnapshotsTests${variant.name.capitalized}",
6873
GenerateSnapshotTestsTask::class.java,
6974
) { task ->
7075
task.includePrivatePreviews.set(extension.includePrivatePreviews)
7176
task.theme.set(extension.theme)
77+
task.paparazziMajorVersion.value(paparazziMajorVersion)
7278
// Fall back to the Android namespace when the user doesn't configure packageTrees
7379
// TODO do we actually need this?
7480
task.packageTrees.set(
@@ -87,6 +93,7 @@ abstract class GenerateSnapshotTestsTask : DefaultTask() {
8793
includePrivatePreviews: Boolean,
8894
packageTrees: List<String>,
8995
theme: String? = null,
96+
paparazziMajorVersion: Int = 2,
9097
): String {
9198
val includePrivateExpr =
9299
if (includePrivatePreviews) "\n .includePrivatePreviews()" else ""
@@ -217,17 +224,17 @@ private class TestNameOverrideHandler(
217224
218225
private object PaparazziPreviewRule {
219226
const val UNDEFINED_API_LEVEL = -1
220-
const val MAX_API_LEVEL = 36
221227
222228
fun createFor(preview: ComposablePreview<AndroidPreviewInfo>): Paparazzi {
223229
val previewInfo = preview.previewInfo
224-
val previewApiLevel = when (previewInfo.apiLevel == UNDEFINED_API_LEVEL) {
225-
true -> MAX_API_LEVEL
226-
false -> previewInfo.apiLevel
230+
val env = detectEnvironment()
231+
val environment = when (previewInfo.apiLevel == UNDEFINED_API_LEVEL) {
232+
true -> env
233+
false -> env.copy(compileSdkVersion = previewInfo.apiLevel)
227234
}
228235
val tolerance = 0.0
229236
return Paparazzi(
230-
environment = detectEnvironment().copy(compileSdkVersion = previewApiLevel),
237+
environment = environment,
231238
deviceConfig = DeviceConfigBuilder.build(preview.previewInfo),
232239
${if (theme != null) "theme = \"$theme\"," else ""}
233240
supportsRtl = true,
@@ -240,7 +247,7 @@ private object PaparazziPreviewRule {
240247
snapshotHandler = TestNameOverrideHandler(
241248
when (System.getProperty("paparazzi.test.verify")?.toBoolean() == true) {
242249
true -> SnapshotVerifier(maxPercentDifference = tolerance)
243-
false -> HtmlReportWriter(maxPercentDifference = tolerance)
250+
false -> ${if (paparazziMajorVersion >= 2) "HtmlReportWriter(maxPercentDifference = tolerance)" else "HtmlReportWriter()"}
244251
}
245252
),
246253
maxPercentDifference = tolerance,

plugin-build/src/test/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTaskTest.kt

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.sentry.android.gradle.snapshot
22

3+
import io.sentry.android.gradle.parseMajorVersion
34
import java.io.File
5+
import kotlin.test.assertEquals
46
import kotlin.test.assertFalse
57
import kotlin.test.assertTrue
68
import org.gradle.testfixtures.ProjectBuilder
@@ -124,11 +126,47 @@ class GenerateSnapshotTestsTaskTest {
124126
)
125127
}
126128

129+
@Test
130+
fun `parseMajorVersion extracts major from standard semver`() {
131+
assertEquals(1, parseMajorVersion("1.3.5"))
132+
assertEquals(2, parseMajorVersion("2.0.0-alpha01"))
133+
}
134+
135+
@Test
136+
fun `parseMajorVersion extracts major from dynamic versions`() {
137+
assertEquals(1, parseMajorVersion("1.+"))
138+
assertEquals(2, parseMajorVersion("2.+"))
139+
}
140+
141+
@Test
142+
fun `parseMajorVersion returns default for unparseable versions`() {
143+
assertEquals(2, parseMajorVersion("latest.release"))
144+
assertEquals(2, parseMajorVersion(null))
145+
assertEquals(2, parseMajorVersion("+"))
146+
}
147+
148+
@Test
149+
fun `generated file uses HtmlReportWriter without maxPercentDifference for paparazzi 1`() {
150+
val content = generateAndRead(packageTrees = listOf("com.example"), paparazziMajorVersion = 1)
151+
152+
assertTrue(content.contains("HtmlReportWriter()"))
153+
assertFalse(content.contains("HtmlReportWriter(maxPercentDifference"))
154+
}
155+
156+
@Test
157+
fun `generated file uses HtmlReportWriter with maxPercentDifference for paparazzi 2`() {
158+
val content = generateAndRead(packageTrees = listOf("com.example"), paparazziMajorVersion = 2)
159+
160+
assertTrue(content.contains("HtmlReportWriter(maxPercentDifference = tolerance)"))
161+
assertFalse(content.contains("HtmlReportWriter()"))
162+
}
163+
127164
private fun generateAndRead(
128165
packageTrees: List<String>,
129166
includePrivatePreviews: Boolean = false,
167+
paparazziMajorVersion: Int = 2,
130168
): String {
131-
val task = createTask(packageTrees, includePrivatePreviews)
169+
val task = createTask(packageTrees, includePrivatePreviews, paparazziMajorVersion)
132170
task.generate()
133171
val file =
134172
File(task.outputDir.get().asFile, "io/sentry/snapshot/ComposablePreviewSnapshotTest.kt")
@@ -138,12 +176,14 @@ class GenerateSnapshotTestsTaskTest {
138176
private fun createTask(
139177
packageTrees: List<String>,
140178
includePrivatePreviews: Boolean = false,
179+
paparazziMajorVersion: Int = 2,
141180
): GenerateSnapshotTestsTask {
142181
val project = ProjectBuilder.builder().build()
143182
return project.tasks
144183
.register("testGenerateSnapshotTests", GenerateSnapshotTestsTask::class.java) { task ->
145184
task.includePrivatePreviews.set(includePrivatePreviews)
146185
task.packageTrees.set(packageTrees)
186+
task.paparazziMajorVersion.set(paparazziMajorVersion)
147187
task.outputDir.set(tmpDir.newFolder("output"))
148188
}
149189
.get()

0 commit comments

Comments
 (0)