Skip to content

Commit 43229ed

Browse files
committed
Add a way to provide a file name for Snapshots
Adds a FileNameProvider that can be implemented in any way to specify file names for a recorded Snapshot. Closes feature request #549
1 parent 4594b7a commit 43229ed

File tree

7 files changed

+108
-33
lines changed

7 files changed

+108
-33
lines changed

paparazzi/api/paparazzi.api

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ public final class app/cash/paparazzi/EnvironmentKt {
8585
public static final fun detectEnvironment ()Lapp/cash/paparazzi/Environment;
8686
}
8787

88+
public abstract interface class app/cash/paparazzi/FileNameProvider {
89+
public abstract fun snapshotFileName (Lapp/cash/paparazzi/Snapshot;Ljava/lang/String;)Ljava/lang/String;
90+
}
91+
8892
public final class app/cash/paparazzi/Flags {
8993
public static final field $stable I
9094
public static final field DEBUG_LINKED_OBJECTS Ljava/lang/String;
@@ -97,7 +101,8 @@ public final class app/cash/paparazzi/HtmlReportWriter : app/cash/paparazzi/Snap
97101
public fun <init> (Ljava/lang/String;)V
98102
public fun <init> (Ljava/lang/String;Ljava/io/File;)V
99103
public fun <init> (Ljava/lang/String;Ljava/io/File;Ljava/io/File;)V
100-
public synthetic fun <init> (Ljava/lang/String;Ljava/io/File;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
104+
public fun <init> (Ljava/lang/String;Ljava/io/File;Ljava/io/File;Lapp/cash/paparazzi/FileNameProvider;)V
105+
public synthetic fun <init> (Ljava/lang/String;Ljava/io/File;Ljava/io/File;Lapp/cash/paparazzi/FileNameProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
101106
public fun close ()V
102107
public fun newFrameHandler (Lapp/cash/paparazzi/Snapshot;II)Lapp/cash/paparazzi/SnapshotHandler$FrameHandler;
103108
}
@@ -117,12 +122,13 @@ public final class app/cash/paparazzi/Paparazzi : org/junit/rules/TestRule {
117122
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;)V
118123
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;Z)V
119124
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZD)V
120-
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/SnapshotHandler;)V
121-
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;)V
122-
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;Z)V
123-
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;ZZ)V
124-
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;ZZZ)V
125-
public synthetic fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
125+
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/FileNameProvider;)V
126+
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/FileNameProvider;Lapp/cash/paparazzi/SnapshotHandler;)V
127+
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/FileNameProvider;Lapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;)V
128+
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/FileNameProvider;Lapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;Z)V
129+
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/FileNameProvider;Lapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;ZZ)V
130+
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/FileNameProvider;Lapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;ZZZ)V
131+
public synthetic fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZDLapp/cash/paparazzi/FileNameProvider;Lapp/cash/paparazzi/SnapshotHandler;Ljava/util/Set;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
126132
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
127133
public final fun close ()V
128134
public final fun getContext ()Landroid/content/Context;
@@ -178,7 +184,8 @@ public final class app/cash/paparazzi/SnapshotVerifier : app/cash/paparazzi/Snap
178184
public static final field $stable I
179185
public fun <init> (D)V
180186
public fun <init> (DLjava/io/File;)V
181-
public synthetic fun <init> (DLjava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
187+
public fun <init> (DLjava/io/File;Lapp/cash/paparazzi/FileNameProvider;)V
188+
public synthetic fun <init> (DLjava/io/File;Lapp/cash/paparazzi/FileNameProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
182189
public fun close ()V
183190
public fun newFrameHandler (Lapp/cash/paparazzi/Snapshot;II)Lapp/cash/paparazzi/SnapshotHandler$FrameHandler;
184191
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package app.cash.paparazzi
2+
3+
import java.util.Locale
4+
5+
public interface FileNameProvider {
6+
public fun snapshotFileName(snapshot: Snapshot, extension: String): String
7+
}
8+
9+
internal class DefaultFileNameProvider(
10+
private val delimiter: String = "_"
11+
) : FileNameProvider {
12+
13+
override fun snapshotFileName(snapshot: Snapshot, extension: String): String {
14+
val name = snapshot.name
15+
val formattedLabel = if (name != null) {
16+
"$delimiter${name.lowercase(Locale.US).replace("\\s".toRegex(), delimiter)}"
17+
} else {
18+
""
19+
}
20+
21+
val testName = snapshot.testName
22+
return "${testName.packageName}${delimiter}${testName.className}${delimiter}${testName.methodName}$formattedLabel.$extension"
23+
}
24+
}

paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ import javax.imageio.ImageIO
6060
public class HtmlReportWriter @JvmOverloads constructor(
6161
private val runName: String = defaultRunName(),
6262
private val rootDirectory: File = File(System.getProperty("paparazzi.report.dir")),
63-
snapshotRootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir"))
63+
snapshotRootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir")),
64+
private val fileNameProvider: FileNameProvider = DefaultFileNameProvider()
6465
) : SnapshotHandler {
6566
private val runsDirectory: File = File(rootDirectory, "runs")
6667
private val imagesDirectory: File = File(rootDirectory, "images")
@@ -101,7 +102,10 @@ public class HtmlReportWriter @JvmOverloads constructor(
101102
val shot = if (hashes.size == 1) {
102103
val original = File(imagesDirectory, "${hashes[0]}.png")
103104
if (isRecording) {
104-
val goldenFile = File(goldenImagesDirectory, snapshot.toFileName("_", "png"))
105+
val goldenFile = File(
106+
goldenImagesDirectory,
107+
fileNameProvider.snapshotFileName(snapshot, extension = "png")
108+
)
105109
original.copyTo(goldenFile, overwrite = true)
106110
}
107111
snapshot.copy(file = original.toJsonPath())
@@ -112,15 +116,21 @@ public class HtmlReportWriter @JvmOverloads constructor(
112116
for ((index, frameHash) in hashes.withIndex()) {
113117
val originalFrame = File(imagesDirectory, "$frameHash.png")
114118
val frameSnapshot = snapshot.copy(name = "${snapshot.name} $index")
115-
val goldenFile = File(goldenImagesDirectory, frameSnapshot.toFileName("_", "png"))
119+
val goldenFile = File(
120+
goldenImagesDirectory,
121+
fileNameProvider.snapshotFileName(frameSnapshot, extension = "png")
122+
)
116123
if (!goldenFile.exists()) {
117124
originalFrame.copyTo(goldenFile)
118125
}
119126
}
120127
}
121128
val original = File(videosDirectory, "$hash.mov")
122129
if (isRecording) {
123-
val goldenFile = File(goldenVideosDirectory, snapshot.toFileName("_", "mov"))
130+
val goldenFile = File(
131+
goldenVideosDirectory,
132+
fileNameProvider.snapshotFileName(snapshot, extension = "mov")
133+
)
124134
if (!goldenFile.exists()) {
125135
original.copyTo(goldenFile)
126136
}
@@ -290,5 +300,5 @@ internal val filenameSafeChars = CharMatcher.inRange('a', 'z')
290300
.or(CharMatcher.anyOf("_-.~@^()[]{}:;,"))
291301

292302
internal fun String.sanitizeForFilename(): String? {
293-
return filenameSafeChars.negate().replaceFrom(toLowerCase(Locale.US), '_')
303+
return filenameSafeChars.negate().replaceFrom(lowercase(Locale.US), '_')
294304
}

paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,11 @@ public class Paparazzi @JvmOverloads constructor(
9595
private val renderingMode: RenderingMode = RenderingMode.NORMAL,
9696
private val appCompatEnabled: Boolean = true,
9797
private val maxPercentDifference: Double = 0.1,
98-
private val snapshotHandler: SnapshotHandler = determineHandler(maxPercentDifference),
98+
private val fileNameProvider: FileNameProvider = DefaultFileNameProvider(),
99+
private val snapshotHandler: SnapshotHandler = determineHandler(
100+
maxPercentDifference,
101+
fileNameProvider
102+
),
99103
private val renderExtensions: Set<RenderExtension> = setOf(),
100104
private val supportsRtl: Boolean = false,
101105
private val showSystemUi: Boolean = false,
@@ -675,11 +679,15 @@ public class Paparazzi @JvmOverloads constructor(
675679
}
676680
}
677681

678-
private fun determineHandler(maxPercentDifference: Double): SnapshotHandler =
679-
if (isVerifying) {
680-
SnapshotVerifier(maxPercentDifference)
682+
private fun determineHandler(
683+
maxPercentDifference: Double,
684+
fileNameProvider: FileNameProvider
685+
): SnapshotHandler {
686+
return if (isVerifying) {
687+
SnapshotVerifier(maxPercentDifference, fileNameProvider = fileNameProvider)
681688
} else {
682-
HtmlReportWriter()
689+
HtmlReportWriter(fileNameProvider = fileNameProvider)
683690
}
691+
}
684692
}
685693
}

paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package app.cash.paparazzi
1717

1818
import dev.drewhamilton.poko.Poko
1919
import java.util.Date
20-
import java.util.Locale
2120

2221
@Poko
2322
public class Snapshot(
@@ -35,15 +34,3 @@ public class Snapshot(
3534
file: String? = this.file
3635
): Snapshot = Snapshot(name, testName, timestamp, tags, file)
3736
}
38-
39-
internal fun Snapshot.toFileName(
40-
delimiter: String = "_",
41-
extension: String
42-
): String {
43-
val formattedLabel = if (name != null) {
44-
"$delimiter${name.toLowerCase(Locale.US).replace("\\s".toRegex(), delimiter)}"
45-
} else {
46-
""
47-
}
48-
return "${testName.packageName}${delimiter}${testName.className}${delimiter}${testName.methodName}$formattedLabel.$extension"
49-
}

paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import javax.imageio.ImageIO
2323

2424
public class SnapshotVerifier @JvmOverloads constructor(
2525
private val maxPercentDifference: Double,
26-
rootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir"))
26+
rootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir")),
27+
private val fileNameProvider: FileNameProvider = DefaultFileNameProvider()
2728
) : SnapshotHandler {
2829
private val imagesDirectory: File = File(rootDirectory, "images")
2930
private val videosDirectory: File = File(rootDirectory, "videos")
@@ -41,7 +42,8 @@ public class SnapshotVerifier @JvmOverloads constructor(
4142
return object : FrameHandler {
4243
override fun handle(image: BufferedImage) {
4344
// Note: does not handle videos or its frames at the moment
44-
val expected = File(imagesDirectory, snapshot.toFileName(extension = "png"))
45+
val expected =
46+
File(imagesDirectory, fileNameProvider.snapshotFileName(snapshot, extension = "png"))
4547
if (!expected.exists()) {
4648
throw AssertionError("File $expected does not exist")
4749
}

paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,43 @@ class HtmlReportWriterTest {
158158
}
159159
}
160160

161+
@Test
162+
fun useFileNameProvider() {
163+
// set record mode
164+
System.setProperty("paparazzi.test.record", "true")
165+
166+
val htmlReportWriter = HtmlReportWriter(
167+
"record_run",
168+
fileNameProvider = object : FileNameProvider {
169+
override fun snapshotFileName(snapshot: Snapshot, extension: String): String {
170+
return "${snapshot.name}.$extension"
171+
}
172+
},
173+
rootDirectory = reportRoot.root,
174+
snapshotRootDirectory = snapshotRoot.root
175+
)
176+
htmlReportWriter.use {
177+
val snapshot = Snapshot(
178+
name = "test",
179+
testName = TestName("app.cash.paparazzi", "HomeView", "testSettings"),
180+
timestamp = Instant.parse("2021-02-23T10:27:43Z").toDate()
181+
)
182+
val golden = File("${snapshotRoot.root}/images/test.png")
183+
184+
// precondition
185+
assertThat(golden).doesNotExist()
186+
187+
// take 1
188+
val frameHandler1 = htmlReportWriter.newFrameHandler(
189+
snapshot = snapshot,
190+
frameCount = 1,
191+
fps = -1
192+
)
193+
frameHandler1.use { frameHandler1.handle(anyImage) }
194+
assertThat(golden).exists()
195+
}
196+
}
197+
161198
private fun Instant.toDate() = Date(toEpochMilli())
162199

163200
private fun File.lastModifiedTime(): FileTime {

0 commit comments

Comments
 (0)