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 4ec0ba2
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 26 deletions.
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"
}
}
18 changes: 14 additions & 4 deletions paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import javax.imageio.ImageIO
*/
public class HtmlReportWriter @JvmOverloads constructor(
private val runName: String = defaultRunName(),
private val fileNameProvider: FileNameProvider = DefaultFileNameProvider(),
private val rootDirectory: File = File(System.getProperty("paparazzi.report.dir")),
snapshotRootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir"))
) : SnapshotHandler {
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)
} 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,6 +23,7 @@ import javax.imageio.ImageIO

public class SnapshotVerifier @JvmOverloads constructor(
private val maxPercentDifference: Double,
private val fileNameProvider: FileNameProvider = DefaultFileNameProvider(),
rootDirectory: File = File(System.getProperty("paparazzi.snapshot.dir"))
) : SnapshotHandler {
private val imagesDirectory: File = File(rootDirectory, "images")
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
47 changes: 44 additions & 3 deletions paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class HtmlReportWriterTest {

@Test
fun happyPath() {
val htmlReportWriter = HtmlReportWriter("run_one", reportRoot.root)
val htmlReportWriter = HtmlReportWriter("run_one", rootDirectory = reportRoot.root)
htmlReportWriter.use {
val frameHandler = htmlReportWriter.newFrameHandler(
Snapshot(
Expand Down Expand Up @@ -91,7 +91,7 @@ class HtmlReportWriterTest {

@Test
fun noSnapshotOnFailure() {
val htmlReportWriter = HtmlReportWriter("run_one", reportRoot.root)
val htmlReportWriter = HtmlReportWriter("run_one", rootDirectory = reportRoot.root)
htmlReportWriter.use {
val frameHandler = htmlReportWriter.newFrameHandler(
snapshot = Snapshot(
Expand All @@ -116,7 +116,11 @@ class HtmlReportWriterTest {
// set record mode
System.setProperty("paparazzi.test.record", "true")

val htmlReportWriter = HtmlReportWriter("record_run", reportRoot.root, snapshotRoot.root)
val htmlReportWriter = HtmlReportWriter(
"record_run",
rootDirectory = reportRoot.root,
snapshotRootDirectory = snapshotRoot.root
)
htmlReportWriter.use {
val now = Instant.parse("2021-02-23T10:27:43Z")
val snapshot = Snapshot(
Expand Down Expand Up @@ -158,6 +162,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 4ec0ba2

Please sign in to comment.