Skip to content

Commit ec8a132

Browse files
runningcodeclaude
andcommitted
feat(snapshots): Emit diff_threshold in snapshot sidecar (EME-1055)
The generated Paparazzi test now reflects on each @Preview to read an optional @SentrySnapshot(diffThreshold) annotation and writes the value as "diff_threshold" in the per-snapshot sidecar JSON, but only when the annotation is present and the threshold differs from the 0f default. Changes SentrySnapshot's retention from BINARY to RUNTIME so the JVM retains it for reflection. Java's Method.getAnnotation cannot see CLASS/BINARY-retained annotations. The lookup is guarded by runCatching so a missing runtime jar or a class-loading failure degrades to "field omitted" rather than a test failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0d949ec commit ec8a132

4 files changed

Lines changed: 33 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Add `@SentrySnapshot` runtime annotation for configuring the per-snapshot diff threshold, published as `io.sentry:sentry-snapshots-runtime` ([EME-1055](https://linear.app/getsentry/issue/EME-1055))
8+
- Emit `diff_threshold` in the snapshot sidecar JSON when a `@Preview` is also annotated with `@SentrySnapshot(diffThreshold = ...)` ([EME-1055](https://linear.app/getsentry/issue/EME-1055))
89

910
### Dependencies
1011

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,17 @@ class $CLASS_NAME(
376376
if (info.showSystemUi) metadata["showSystemUi"] = true
377377
if (info.showBackground) metadata["showBackground"] = true
378378
379+
val diffThreshold: Float? = runCatching {
380+
val declaring = Class.forName(preview.declaringClass)
381+
val method = declaring.declaredMethods.firstOrNull { it.name == preview.methodName }
382+
?: return@runCatching null
383+
@Suppress("UNCHECKED_CAST")
384+
val annClass = Class.forName("io.sentry.snapshots.runtime.SentrySnapshot") as Class<out Annotation>
385+
val ann = method.getAnnotation(annClass) ?: return@runCatching null
386+
annClass.getDeclaredMethod("diffThreshold").invoke(ann) as? Float
387+
}.getOrNull()
388+
if (diffThreshold != null && diffThreshold != 0f) metadata["diff_threshold"] = diffThreshold
389+
379390
val json = metadata.entries.joinToString(",\n ", prefix = "{\n ", postfix = "\n}") { (k, v) ->
380391
if (v is String) "\"" + k + "\": \"" + escapeJson(v) + "\""
381392
else "\"" + k + "\": " + v

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,26 @@ class GenerateSnapshotTestsTaskTest {
126126
)
127127
}
128128

129+
@Test
130+
fun `generated sidecar reads SentrySnapshot annotation via reflection`() {
131+
val content = generateAndRead(packageTrees = listOf("com.example"))
132+
133+
assertTrue(content.contains("Class.forName(preview.declaringClass)"))
134+
assertTrue(content.contains("\"io.sentry.snapshots.runtime.SentrySnapshot\""))
135+
assertTrue(content.contains("getDeclaredMethod(\"diffThreshold\")"))
136+
}
137+
138+
@Test
139+
fun `generated sidecar emits diff_threshold only when non-default`() {
140+
val content = generateAndRead(packageTrees = listOf("com.example"))
141+
142+
assertTrue(
143+
content.contains(
144+
"if (diffThreshold != null && diffThreshold != 0f) metadata[\"diff_threshold\"] = diffThreshold"
145+
)
146+
)
147+
}
148+
129149
@Test
130150
fun `parseMajorVersion extracts major from standard semver`() {
131151
assertEquals(1, parseMajorVersion("1.3.5"))

sentry-snapshots-runtime/src/main/kotlin/io/sentry/snapshots/runtime/SentrySnapshot.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import androidx.annotation.FloatRange
88
* Apply alongside `@Preview` on a composable to override the global diff threshold on a
99
* per-snapshot basis.
1010
*/
11-
@Retention(AnnotationRetention.BINARY)
11+
@Retention(AnnotationRetention.RUNTIME)
1212
@Target(AnnotationTarget.FUNCTION)
1313
annotation class SentrySnapshot(
1414
/**

0 commit comments

Comments
 (0)