Skip to content

Commit

Permalink
Add a way to provide a file name for Snapshots
Browse files Browse the repository at this point in the history
Adds a FileNameProvider that can be implemented in any way to specify file names for a recorded Snapshot.
Closes feature request cashapp#549
  • Loading branch information
magnusvs committed Feb 14, 2024
1 parent 4594b7a commit 43229ed
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 33 deletions.
23 changes: 15 additions & 8 deletions paparazzi/api/paparazzi.api
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ public final class app/cash/paparazzi/EnvironmentKt {
public static final fun detectEnvironment ()Lapp/cash/paparazzi/Environment;
}

public abstract interface class app/cash/paparazzi/FileNameProvider {
public abstract fun snapshotFileName (Lapp/cash/paparazzi/Snapshot;Ljava/lang/String;)Ljava/lang/String;
}

public final class app/cash/paparazzi/Flags {
public static final field $stable I
public static final field DEBUG_LINKED_OBJECTS Ljava/lang/String;
Expand All @@ -97,7 +101,8 @@ public final class app/cash/paparazzi/HtmlReportWriter : app/cash/paparazzi/Snap
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/io/File;)V
public fun <init> (Ljava/lang/String;Ljava/io/File;Ljava/io/File;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/io/File;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/io/File;Ljava/io/File;Lapp/cash/paparazzi/FileNameProvider;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/io/File;Ljava/io/File;Lapp/cash/paparazzi/FileNameProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public fun newFrameHandler (Lapp/cash/paparazzi/Snapshot;II)Lapp/cash/paparazzi/SnapshotHandler$FrameHandler;
}
Expand All @@ -117,12 +122,13 @@ public final class app/cash/paparazzi/Paparazzi : org/junit/rules/TestRule {
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;)V
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;Z)V
public fun <init> (Lapp/cash/paparazzi/Environment;Lapp/cash/paparazzi/DeviceConfig;Ljava/lang/String;Lcom/android/ide/common/rendering/api/SessionParams$RenderingMode;ZD)V
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
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
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
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
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
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
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
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
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
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
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
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
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
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
public final fun close ()V
public final fun getContext ()Landroid/content/Context;
Expand Down Expand Up @@ -178,7 +184,8 @@ public final class app/cash/paparazzi/SnapshotVerifier : app/cash/paparazzi/Snap
public static final field $stable I
public fun <init> (D)V
public fun <init> (DLjava/io/File;)V
public synthetic fun <init> (DLjava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (DLjava/io/File;Lapp/cash/paparazzi/FileNameProvider;)V
public synthetic fun <init> (DLjava/io/File;Lapp/cash/paparazzi/FileNameProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public fun newFrameHandler (Lapp/cash/paparazzi/Snapshot;II)Lapp/cash/paparazzi/SnapshotHandler$FrameHandler;
}
Expand Down
24 changes: 24 additions & 0 deletions paparazzi/src/main/java/app/cash/paparazzi/FileNameProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package app.cash.paparazzi

import java.util.Locale

public interface FileNameProvider {
public fun snapshotFileName(snapshot: Snapshot, extension: String): String
}

internal class DefaultFileNameProvider(
private val delimiter: String = "_"
) : FileNameProvider {

override fun snapshotFileName(snapshot: Snapshot, extension: String): String {
val name = snapshot.name
val formattedLabel = if (name != null) {
"$delimiter${name.lowercase(Locale.US).replace("\\s".toRegex(), delimiter)}"
} else {
""
}

val testName = snapshot.testName
return "${testName.packageName}${delimiter}${testName.className}${delimiter}${testName.methodName}$formattedLabel.$extension"
}
}
20 changes: 15 additions & 5 deletions paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ import javax.imageio.ImageIO
public class HtmlReportWriter @JvmOverloads constructor(
private val runName: String = defaultRunName(),
private val rootDirectory: File = File(System.getProperty("paparazzi.report.dir")),
snapshotRootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir"))
snapshotRootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir")),
private val fileNameProvider: FileNameProvider = DefaultFileNameProvider()
) : SnapshotHandler {
private val runsDirectory: File = File(rootDirectory, "runs")
private val imagesDirectory: File = File(rootDirectory, "images")
Expand Down Expand Up @@ -101,7 +102,10 @@ public class HtmlReportWriter @JvmOverloads constructor(
val shot = if (hashes.size == 1) {
val original = File(imagesDirectory, "${hashes[0]}.png")
if (isRecording) {
val goldenFile = File(goldenImagesDirectory, snapshot.toFileName("_", "png"))
val goldenFile = File(
goldenImagesDirectory,
fileNameProvider.snapshotFileName(snapshot, extension = "png")
)
original.copyTo(goldenFile, overwrite = true)
}
snapshot.copy(file = original.toJsonPath())
Expand All @@ -112,15 +116,21 @@ public class HtmlReportWriter @JvmOverloads constructor(
for ((index, frameHash) in hashes.withIndex()) {
val originalFrame = File(imagesDirectory, "$frameHash.png")
val frameSnapshot = snapshot.copy(name = "${snapshot.name} $index")
val goldenFile = File(goldenImagesDirectory, frameSnapshot.toFileName("_", "png"))
val goldenFile = File(
goldenImagesDirectory,
fileNameProvider.snapshotFileName(frameSnapshot, extension = "png")
)
if (!goldenFile.exists()) {
originalFrame.copyTo(goldenFile)
}
}
}
val original = File(videosDirectory, "$hash.mov")
if (isRecording) {
val goldenFile = File(goldenVideosDirectory, snapshot.toFileName("_", "mov"))
val goldenFile = File(
goldenVideosDirectory,
fileNameProvider.snapshotFileName(snapshot, extension = "mov")
)
if (!goldenFile.exists()) {
original.copyTo(goldenFile)
}
Expand Down Expand Up @@ -290,5 +300,5 @@ internal val filenameSafeChars = CharMatcher.inRange('a', 'z')
.or(CharMatcher.anyOf("_-.~@^()[]{}:;,"))

internal fun String.sanitizeForFilename(): String? {
return filenameSafeChars.negate().replaceFrom(toLowerCase(Locale.US), '_')
return filenameSafeChars.negate().replaceFrom(lowercase(Locale.US), '_')
}
18 changes: 13 additions & 5 deletions paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ public class Paparazzi @JvmOverloads constructor(
private val renderingMode: RenderingMode = RenderingMode.NORMAL,
private val appCompatEnabled: Boolean = true,
private val maxPercentDifference: Double = 0.1,
private val snapshotHandler: SnapshotHandler = determineHandler(maxPercentDifference),
private val fileNameProvider: FileNameProvider = DefaultFileNameProvider(),
private val snapshotHandler: SnapshotHandler = determineHandler(
maxPercentDifference,
fileNameProvider
),
private val renderExtensions: Set<RenderExtension> = setOf(),
private val supportsRtl: Boolean = false,
private val showSystemUi: Boolean = false,
Expand Down Expand Up @@ -675,11 +679,15 @@ public class Paparazzi @JvmOverloads constructor(
}
}

private fun determineHandler(maxPercentDifference: Double): SnapshotHandler =
if (isVerifying) {
SnapshotVerifier(maxPercentDifference)
private fun determineHandler(
maxPercentDifference: Double,
fileNameProvider: FileNameProvider
): SnapshotHandler {
return if (isVerifying) {
SnapshotVerifier(maxPercentDifference, fileNameProvider = fileNameProvider)
} else {
HtmlReportWriter()
HtmlReportWriter(fileNameProvider = fileNameProvider)
}
}
}
}
13 changes: 0 additions & 13 deletions paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package app.cash.paparazzi

import dev.drewhamilton.poko.Poko
import java.util.Date
import java.util.Locale

@Poko
public class Snapshot(
Expand All @@ -35,15 +34,3 @@ public class Snapshot(
file: String? = this.file
): Snapshot = Snapshot(name, testName, timestamp, tags, file)
}

internal fun Snapshot.toFileName(
delimiter: String = "_",
extension: String
): String {
val formattedLabel = if (name != null) {
"$delimiter${name.toLowerCase(Locale.US).replace("\\s".toRegex(), delimiter)}"
} else {
""
}
return "${testName.packageName}${delimiter}${testName.className}${delimiter}${testName.methodName}$formattedLabel.$extension"
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import javax.imageio.ImageIO

public class SnapshotVerifier @JvmOverloads constructor(
private val maxPercentDifference: Double,
rootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir"))
rootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir")),
private val fileNameProvider: FileNameProvider = DefaultFileNameProvider()
) : SnapshotHandler {
private val imagesDirectory: File = File(rootDirectory, "images")
private val videosDirectory: File = File(rootDirectory, "videos")
Expand All @@ -41,7 +42,8 @@ public class SnapshotVerifier @JvmOverloads constructor(
return object : FrameHandler {
override fun handle(image: BufferedImage) {
// Note: does not handle videos or its frames at the moment
val expected = File(imagesDirectory, snapshot.toFileName(extension = "png"))
val expected =
File(imagesDirectory, fileNameProvider.snapshotFileName(snapshot, extension = "png"))
if (!expected.exists()) {
throw AssertionError("File $expected does not exist")
}
Expand Down
37 changes: 37 additions & 0 deletions paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,43 @@ class HtmlReportWriterTest {
}
}

@Test
fun useFileNameProvider() {
// set record mode
System.setProperty("paparazzi.test.record", "true")

val htmlReportWriter = HtmlReportWriter(
"record_run",
fileNameProvider = object : FileNameProvider {
override fun snapshotFileName(snapshot: Snapshot, extension: String): String {
return "${snapshot.name}.$extension"
}
},
rootDirectory = reportRoot.root,
snapshotRootDirectory = snapshotRoot.root
)
htmlReportWriter.use {
val snapshot = Snapshot(
name = "test",
testName = TestName("app.cash.paparazzi", "HomeView", "testSettings"),
timestamp = Instant.parse("2021-02-23T10:27:43Z").toDate()
)
val golden = File("${snapshotRoot.root}/images/test.png")

// precondition
assertThat(golden).doesNotExist()

// take 1
val frameHandler1 = htmlReportWriter.newFrameHandler(
snapshot = snapshot,
frameCount = 1,
fps = -1
)
frameHandler1.use { frameHandler1.handle(anyImage) }
assertThat(golden).exists()
}
}

private fun Instant.toDate() = Date(toEpochMilli())

private fun File.lastModifiedTime(): FileTime {
Expand Down

0 comments on commit 43229ed

Please sign in to comment.