From 4ec0ba221d05ca1cdd4f06cbd30e5458bcc7b14e Mon Sep 17 00:00:00 2001 From: Magnus von Scheele Date: Wed, 14 Feb 2024 11:56:02 +0100 Subject: [PATCH] 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 --- .../app/cash/paparazzi/FileNameProvider.kt | 24 ++++++++++ .../app/cash/paparazzi/HtmlReportWriter.kt | 18 +++++-- .../main/java/app/cash/paparazzi/Paparazzi.kt | 18 +++++-- .../main/java/app/cash/paparazzi/Snapshot.kt | 13 ----- .../app/cash/paparazzi/SnapshotVerifier.kt | 4 +- .../cash/paparazzi/HtmlReportWriterTest.kt | 47 +++++++++++++++++-- 6 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 paparazzi/src/main/java/app/cash/paparazzi/FileNameProvider.kt diff --git a/paparazzi/src/main/java/app/cash/paparazzi/FileNameProvider.kt b/paparazzi/src/main/java/app/cash/paparazzi/FileNameProvider.kt new file mode 100644 index 0000000000..fb99f6e4f4 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/FileNameProvider.kt @@ -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" + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt b/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt index 81eb5df0fe..d763bba143 100644 --- a/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt @@ -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 { @@ -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()) @@ -112,7 +116,10 @@ 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) } @@ -120,7 +127,10 @@ public class HtmlReportWriter @JvmOverloads constructor( } 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) } @@ -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), '_') } diff --git a/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt b/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt index 655f87c3c6..c30ff4dd80 100644 --- a/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt @@ -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 = setOf(), private val supportsRtl: Boolean = false, private val showSystemUi: Boolean = false, @@ -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) } + } } } diff --git a/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt b/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt index f49f3b9bad..2d27928a48 100644 --- a/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt @@ -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( @@ -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" -} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt b/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt index b5744bf494..5111cd9b5b 100644 --- a/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt @@ -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") @@ -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") } diff --git a/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt index e6694b82e2..93639df2d9 100644 --- a/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt +++ b/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt @@ -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( @@ -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( @@ -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( @@ -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 {