From 2052eb2bae045e6ff70900d34452385b06beac29 Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Mon, 16 Sep 2024 16:15:28 +0200 Subject: [PATCH 1/6] Allow promoting selected suites based on filter --- .../siriusxm/snapshot/Snapshot4sPlugin.scala | 38 ++++++++++++++----- .../sbt-snapshot4s/promote-filter/build.sbt | 10 +++++ .../promote-filter/project/plugins.sbt | 6 +++ .../src/test/resources/snapshot/existing-file | 1 + .../snapshot/nested-directory/nested-file | 1 + .../src/test/scala/FilterTest.scala | 29 ++++++++++++++ .../sbt-snapshot4s/promote-filter/test | 20 ++++++++++ 7 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/build.sbt create mode 100644 modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/project/plugins.sbt create mode 100644 modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/resources/snapshot/existing-file create mode 100644 modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/resources/snapshot/nested-directory/nested-file create mode 100644 modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/scala/FilterTest.scala create mode 100644 modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test diff --git a/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala b/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala index b6e9b07..2d5a324 100644 --- a/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala +++ b/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala @@ -18,6 +18,7 @@ package snapshot4s import sbt.* import sbt.Keys.* +import sbt.complete.DefaultParsers.* import sbt.util.Logger object Snapshot4sPlugin extends AutoPlugin { @@ -29,7 +30,7 @@ object Snapshot4sPlugin extends AutoPlugin { settingKey[File]("The directory in which snapshot4s results are stored prior to promotion.") val snapshot4sSourceGenerator = taskKey[Seq[File]]("Generate source files for snapshot4s testing.") - val snapshot4sPromote = taskKey[Unit]("Update failing snapshot4s snapshot files.") + val snapshot4sPromote = inputKey[Unit]("Update failing snapshot4s snapshot files.") } import autoImport.* @@ -65,17 +66,29 @@ object generated { }, snapshot4sPromote := { val log = streams.value.log + val arguments = + spaceDelimited("") + .examples("*MySuite*", "*MySuite.scala") + .parsed + + val filter = makeFilter(arguments) applyResourcePatches(log)( snapshot4sDirectory.value / "resource-patch", - snapshot4sResourceDirectory.value + snapshot4sResourceDirectory.value, + filter ) applyInlinePatches(log)( snapshot4sDirectory.value / "inline-patch", - sourceBaseDirectory((Test / sourceDirectories).value) + sourceBaseDirectory((Test / sourceDirectories).value), + filter ) } ) + private def makeFilter(arguments: Seq[String]): NameFilter = + if (arguments.isEmpty) AllPassFilter + else arguments.map(GlobFilter(_)).reduce(_ | _) + private def sourceBaseDirectory(sourceDirectories: Seq[File]): File = { def sharedParent(dirA: File, dirB: File): File = { if (dirB.getAbsolutePath().startsWith(dirA.getAbsolutePath())) dirA @@ -85,9 +98,12 @@ object generated { sourceDirectories.reduce(sharedParent) } - private def applyResourcePatches(log: Logger)(resourcePatchDir: File, resourceDir: File) = { - val patches = (resourcePatchDir ** (-DirectoryFilter)).get - patches.foreach { patchFile => + private def applyResourcePatches( + log: Logger + )(resourcePatchDir: File, resourceDir: File, filter: NameFilter) = { + val patches = (resourcePatchDir ** (-DirectoryFilter)).get + val filteredPatches = patches.filter(file => filter.accept(file.getParent)) + filteredPatches.foreach { patchFile => val patchContents = IO.read(patchFile) val relativeSourceFile = IO.relativize(resourcePatchDir, patchFile).get val sourceFile = resourceDir / relativeSourceFile @@ -97,10 +113,12 @@ object generated { } } - private def applyInlinePatches(log: Logger)(inlinePatchDir: File, sourceDir: File) = { - val allChangeFiles = (inlinePatchDir ** (-DirectoryFilter)).get - - allChangeFiles.groupBy(_.getParent).foreach { case (parentDir, changeFiles) => + private def applyInlinePatches( + log: Logger + )(inlinePatchDir: File, sourceDir: File, filter: NameFilter) = { + val patchDirectories = (inlinePatchDir ** (-DirectoryFilter)).get + val dirsByParent = patchDirectories.groupBy(_.getParent).filterKeys(filter.accept) + dirsByParent.foreach { case (parentDir, changeFiles) => val relativeSourceFile = IO.relativize(inlinePatchDir, new File(parentDir)).get val sourceFile = sourceDir / relativeSourceFile val sourceContents = IO.read(sourceFile) diff --git a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/build.sbt b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/build.sbt new file mode 100644 index 0000000..027c579 --- /dev/null +++ b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/build.sbt @@ -0,0 +1,10 @@ +import snapshot4s.BuildInfo.snapshot4sVersion + +lazy val root = (project in file(".")) + .settings( + scalaVersion := "3.3.1", + scalacOptions += "-Xsource:3", + crossScalaVersions := Seq("3.3.1", "2.12.20", "2.13.14"), + libraryDependencies += "com.siriusxm" %% "snapshot4s-weaver" % snapshot4sVersion % Test + ) + .enablePlugins(Snapshot4sPlugin) diff --git a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/project/plugins.sbt b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/project/plugins.sbt new file mode 100644 index 0000000..8c23346 --- /dev/null +++ b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/project/plugins.sbt @@ -0,0 +1,6 @@ +sys.props.get("plugin.version") match { + case Some(x) => + addSbtPlugin("com.siriusxm" % "sbt-snapshot4s" % x) + case _ => sys.error("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +} diff --git a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/resources/snapshot/existing-file b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/resources/snapshot/existing-file new file mode 100644 index 0000000..e33be70 --- /dev/null +++ b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/resources/snapshot/existing-file @@ -0,0 +1 @@ +old-contents diff --git a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/resources/snapshot/nested-directory/nested-file b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/resources/snapshot/nested-directory/nested-file new file mode 100644 index 0000000..e33be70 --- /dev/null +++ b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/resources/snapshot/nested-directory/nested-file @@ -0,0 +1 @@ +old-contents diff --git a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/scala/FilterTest.scala b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/scala/FilterTest.scala new file mode 100644 index 0000000..34130e7 --- /dev/null +++ b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/scala/FilterTest.scala @@ -0,0 +1,29 @@ +package simple + +import weaver.* +import snapshot4s.weaver.SnapshotExpectations +import snapshot4s.generated.snapshotConfig + +object FilterTest extends SimpleIOSuite with SnapshotExpectations { + + test("inline") { + assertInlineSnapshot(1, 2) + } + + test("new inline snapshot") { + assertInlineSnapshot(1, ???) + } + + test("file that doesn't exist") { + assertFileSnapshot("contents", "nonexistent-file") + } + + test("existing file") { + assertFileSnapshot("contents", "existing-file") + } + + test("file in nested directory") { + assertFileSnapshot("contents", "nested-directory/nested-file") + } + +} diff --git a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test new file mode 100644 index 0000000..e89dadf --- /dev/null +++ b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test @@ -0,0 +1,20 @@ +# Generated code should compile +> ++ 2.12 compile +> ++ 2.13 compile +> ++ 3 compile +# # Tests should fail as snapshots are out of date +-> ++ 2.12 test +-> ++ 2.13 test +-> ++ 3 test +# Not update anything with invalid filter +> snapshot4sPromote *IDontExist* +# Tests should fail as snapshots are out of date +-> ++ 2.12 test +-> ++ 2.13 test +-> ++ 3 test +# # Update snapshots with correct filter +> snapshot4sPromote *FilterTest* +# # Tests should succeed with updated snapshots +> ++ 2.12 test +> ++ 2.13 test +> ++ 3 test From 5ee6963ad642ba3327fa8125a30def25035cdcaf Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Tue, 17 Sep 2024 17:13:40 +0200 Subject: [PATCH 2/6] implement selective promote for file snapshots --- .../snapshot4s/AssertFileSnapshotMacro.scala | 71 +++++++++++++++++++ .../snapshot4s/AssertFileSnapshotMacro.scala | 61 ++++++++++++++++ .../siriusxm/snapshot4s/FileSnapshot.scala | 8 ++- .../siriusxm/snapshot4s/InlineSnapshot.scala | 13 +--- .../com/siriusxm/snapshot4s/Locations.scala | 35 +++++++++ .../snapshot4s/SnapshotAssertions.scala | 20 +----- ...SnapshotSpec.scala => LocationsSpec.scala} | 13 +++- .../snapshot4s/FileSnapshotSpec.scala | 2 +- .../siriusxm/snapshot/Snapshot4sPlugin.scala | 13 +++- .../sbt-snapshot4s/promote-filter/test | 10 +-- 10 files changed, 202 insertions(+), 44 deletions(-) create mode 100644 modules/core/src/main/scala-2/com/siriusxm/snapshot4s/AssertFileSnapshotMacro.scala create mode 100644 modules/core/src/main/scala-3/com/siriusxm/snapshot4s/AssertFileSnapshotMacro.scala create mode 100644 modules/core/src/main/scala/com/siriusxm/snapshot4s/Locations.scala rename modules/core/src/test/scala/com/siriusxm/snapshot4s/{InlineSnapshotSpec.scala => LocationsSpec.scala} (81%) diff --git a/modules/core/src/main/scala-2/com/siriusxm/snapshot4s/AssertFileSnapshotMacro.scala b/modules/core/src/main/scala-2/com/siriusxm/snapshot4s/AssertFileSnapshotMacro.scala new file mode 100644 index 0000000..c406272 --- /dev/null +++ b/modules/core/src/main/scala-2/com/siriusxm/snapshot4s/AssertFileSnapshotMacro.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2024 SiriusXM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package snapshot4s + +import scala.reflect.macros.blackbox + +private[snapshot4s] trait AssertFileSnapshotMacro[R] { + + /** Assert that a found value is equal to a previously snapshotted value. + * + * If the assertion fails, the file can be recreated with the found contents using `snapshot4sPromote`. + * @see https://siriusxm.github.io/snapshot4s/file-snapshots + * + * @param found The found value. + * @param snapshotPath Path to file containing the previously snapshotted value. This is relative to the resources/snapshot directory. + * @param eq Compares the found and snapshot values. + * @param resultLike Constructs a framework-specific result. + */ + def assertFileSnapshot(found: String, snapshotPath: String)(implicit + config: SnapshotConfig, + snapshotEq: SnapshotEq[String], + resultLike: ResultLike[String, R] + ): R = macro AssertFileSnapshotMacro.Macro.impl[R] +} + +private[snapshot4s] object AssertFileSnapshotMacro { + + class Macro(val c: blackbox.Context) { + import c.universe.* + + def impl[E]( + found: Expr[String], + snapshotPath: Expr[String] + )( + config: Expr[SnapshotConfig], + snapshotEq: Expr[SnapshotEq[String]], + resultLike: Expr[ResultLike[String, E]] + ): Tree = { + // Scala 2 macro system will place this call in client code so the called method must be public + // `FileSnapshotProxy.createFileSnapshot` is introduced to keep `FileSnapshot` private + q"""_root_.snapshot4s.FileSnapshotProxy.createFileSnapshot($found, $snapshotPath, ${c.enclosingPosition.source.path}, $config, $snapshotEq, $resultLike)""" + } + } + +} + +object FileSnapshotProxy { + + def createFileSnapshot[E]( + found: String, + snapshotPath: String, + sourceFile: String, + config: SnapshotConfig, + snapshotEq: SnapshotEq[String], + resultLike: ResultLike[String, E] + ): E = FileSnapshot(found, snapshotPath, sourceFile, config, snapshotEq, resultLike) +} diff --git a/modules/core/src/main/scala-3/com/siriusxm/snapshot4s/AssertFileSnapshotMacro.scala b/modules/core/src/main/scala-3/com/siriusxm/snapshot4s/AssertFileSnapshotMacro.scala new file mode 100644 index 0000000..3d7ab33 --- /dev/null +++ b/modules/core/src/main/scala-3/com/siriusxm/snapshot4s/AssertFileSnapshotMacro.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2024 SiriusXM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package snapshot4s + +private[snapshot4s] trait AssertFileSnapshotMacro[R]: + + /** Assert that a found value is equal to a previously snapshotted value. + * + * If the assertion fails, the file can be recreated with the found contents using `snapshot4sPromote`. + * @see https://siriusxm.github.io/snapshot4s/file-snapshots + * + * @param found The found value. + * @param snapshotPath Path to file containing the previously snapshotted value. This is relative to the resources/snapshot directory. + * @param eq Compares the found and snapshot values. + * @param resultLike Constructs a framework-specific result. + */ + inline def assertFileSnapshot(found: String, snapshotPath: String)(implicit + config: SnapshotConfig, + eq: SnapshotEq[String], + resultLike: ResultLike[String, R] + ): R = + ${ + AssertFileSnapshotMacro.impl( + 'found, + 'snapshotPath, + 'config, + 'eq, + 'resultLike + ) + } + +import scala.quoted.* + +private[snapshot4s] object AssertFileSnapshotMacro: + + def impl[A, E]( + found: Expr[String], + snapshotPath: Expr[String], + config: Expr[SnapshotConfig], + snapshotEq: Expr[SnapshotEq[String]], + resultLike: Expr[ResultLike[String, E]] + )(using q: Quotes, ta: Type[A], te: Type[E]): Expr[E] = + import q.reflect.* + val sourceFile = Expr(Position.ofMacroExpansion.sourceFile.path) + '{ + FileSnapshot($found, $snapshotPath, $sourceFile, $config, $snapshotEq, $resultLike) + } diff --git a/modules/core/src/main/scala/com/siriusxm/snapshot4s/FileSnapshot.scala b/modules/core/src/main/scala/com/siriusxm/snapshot4s/FileSnapshot.scala index fe3a260..45baa55 100644 --- a/modules/core/src/main/scala/com/siriusxm/snapshot4s/FileSnapshot.scala +++ b/modules/core/src/main/scala/com/siriusxm/snapshot4s/FileSnapshot.scala @@ -21,13 +21,18 @@ private[snapshot4s] object FileSnapshot { def apply[E]( found: String, snapshotPath: String, + sourceFile: String, config: SnapshotConfig, eq: SnapshotEq[String], resultLike: ResultLike[String, E] ): E = resultLike { () => + val sourceFileName = Locations.getFileName(sourceFile) val absoluteSnapshotPath = config.resourceDirectory / RelPath(snapshotPath) def writePatchFile() = { - val patchPath = config.outputDirectory / RelPath("resource-patch") / RelPath(snapshotPath) + val patchPath = + config.outputDirectory / RelPath("resource-patch") / RelPath(sourceFileName) / RelPath( + snapshotPath + ) patchPath.write(found) } if (absoluteSnapshotPath.exists()) { @@ -43,4 +48,5 @@ private[snapshot4s] object FileSnapshot { Result.NonExistent(found) } } + } diff --git a/modules/core/src/main/scala/com/siriusxm/snapshot4s/InlineSnapshot.scala b/modules/core/src/main/scala/com/siriusxm/snapshot4s/InlineSnapshot.scala index 48c6b9d..3d60d14 100644 --- a/modules/core/src/main/scala/com/siriusxm/snapshot4s/InlineSnapshot.scala +++ b/modules/core/src/main/scala/com/siriusxm/snapshot4s/InlineSnapshot.scala @@ -75,7 +75,7 @@ object InlineSnapshot { val sourceFileHash = Hashing.calculateHash(sourceFileContent) val hashHeader = Hashing.produceHashHeader(sourceFileHash) val changeFile = - config.outputDirectory / RelPath("inline-patch") / relativeSourceFilePath( + config.outputDirectory / RelPath("inline-patch") / Locations.relativeSourceFilePath( sourceFile, config ) / RelPath(s"$startPosition-$endPosition") @@ -84,17 +84,6 @@ object InlineSnapshot { changeFile.write(actualStr) } - private[snapshot4s] def relativeSourceFilePath( - sourceFile: String, - config: SnapshotConfig - ): RelPath = { - val baseDirectory = config.sourceDirectory - val sourceFilePath = Path(sourceFile) - sourceFilePath.relativeTo(baseDirectory).getOrElse { - throw new SnapshotConfigUnsupportedError(config) - } - } - // See the Scala 2.13 compiler for the source of the warning we're ignoring: // https://github.com/scala/scala/blob/2.13.x/src/compiler/scala/tools/nsc/typechecker/Typers.scala#L118 private final val InterpolatorCodeRegex = """\$\{\s*(.*?)\s*\}""".r diff --git a/modules/core/src/main/scala/com/siriusxm/snapshot4s/Locations.scala b/modules/core/src/main/scala/com/siriusxm/snapshot4s/Locations.scala new file mode 100644 index 0000000..23cd1c8 --- /dev/null +++ b/modules/core/src/main/scala/com/siriusxm/snapshot4s/Locations.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2024 SiriusXM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package snapshot4s + +private[snapshot4s] object Locations { + + private[snapshot4s] def getFileName(filePath: String): String = + filePath.split("/").last + + private[snapshot4s] def relativeSourceFilePath( + sourceFile: String, + config: SnapshotConfig + ): RelPath = { + val baseDirectory = config.sourceDirectory + val sourceFilePath = Path(sourceFile) + sourceFilePath.relativeTo(baseDirectory).getOrElse { + throw new SnapshotConfigUnsupportedError(config) + } + } + +} diff --git a/modules/core/src/main/scala/com/siriusxm/snapshot4s/SnapshotAssertions.scala b/modules/core/src/main/scala/com/siriusxm/snapshot4s/SnapshotAssertions.scala index c98cf96..1832cbb 100644 --- a/modules/core/src/main/scala/com/siriusxm/snapshot4s/SnapshotAssertions.scala +++ b/modules/core/src/main/scala/com/siriusxm/snapshot4s/SnapshotAssertions.scala @@ -20,22 +20,4 @@ package snapshot4s * * @tparam R Assertion result type specific to the test framework. For example, weaver's result type is `IO[Expectations]`. */ -trait SnapshotAssertions[R] extends AssertInlineSnapshotMacro[R] { - - /** Assert that a found value is equal to a previously snapshotted value. - * - * If the assertion fails, the file can be recreated with the found contents using `snapshot4sPromote`. - * @see https://siriusxm.github.io/snapshot4s/file-snapshots - * - * @param found The found value. - * @param snapshotPath Path to file containing the previously snapshotted value. This is relative to the resources/snapshot directory. - * @param eq Compares the found and snapshot values. - * @param resultLike Constructs a framework-specific result. - */ - def assertFileSnapshot(found: String, snapshotPath: String)(implicit - config: SnapshotConfig, - eq: SnapshotEq[String], - resultLike: ResultLike[String, R] - ): R = FileSnapshot(found, snapshotPath, config, eq, resultLike) - -} +trait SnapshotAssertions[R] extends AssertInlineSnapshotMacro[R] with AssertFileSnapshotMacro[R] diff --git a/modules/core/src/test/scala/com/siriusxm/snapshot4s/InlineSnapshotSpec.scala b/modules/core/src/test/scala/com/siriusxm/snapshot4s/LocationsSpec.scala similarity index 81% rename from modules/core/src/test/scala/com/siriusxm/snapshot4s/InlineSnapshotSpec.scala rename to modules/core/src/test/scala/com/siriusxm/snapshot4s/LocationsSpec.scala index 3b71321..8644135 100644 --- a/modules/core/src/test/scala/com/siriusxm/snapshot4s/InlineSnapshotSpec.scala +++ b/modules/core/src/test/scala/com/siriusxm/snapshot4s/LocationsSpec.scala @@ -19,7 +19,7 @@ package snapshot4s import cats.effect.IO import weaver.* -object InlineSnapshotSpec extends SimpleIOSuite { +object LocationsSpec extends SimpleIOSuite { pureTest("calculates relative paths correctly") { val config = new SnapshotConfig( @@ -28,7 +28,7 @@ object InlineSnapshotSpec extends SimpleIOSuite { sourceDirectory = Path("/path/to/sources") ) val relativePath = - InlineSnapshot.relativeSourceFilePath("/path/to/sources/TestFile.scala", config) + Locations.relativeSourceFilePath("/path/to/sources/TestFile.scala", config) expect.eql(relativePath.value, "TestFile.scala") } @@ -39,7 +39,7 @@ object InlineSnapshotSpec extends SimpleIOSuite { sourceDirectory = Path("/wrong/path/to/sources") ) val result = - IO(InlineSnapshot.relativeSourceFilePath("/path/to/sources/TestFile.scala", config)) + IO(Locations.relativeSourceFilePath("/path/to/sources/TestFile.scala", config)) val message = """Your project setup is not supported by snapshot4s. We encourage you to raise an issue at https://github.com/siriusxm/snapshot4s/issues/new?template=bug.md @@ -55,4 +55,11 @@ We have detected the following configuration: } } + + pureTest("calculates file name from path") { + val fileName = "MySuites.cala" + val path = s"/path/to/$fileName" + expect.eql(Locations.getFileName(path), fileName) + } + } diff --git a/modules/core/src/test/scalajvm/com/siriusxm/snapshot4s/FileSnapshotSpec.scala b/modules/core/src/test/scalajvm/com/siriusxm/snapshot4s/FileSnapshotSpec.scala index 9f98c19..1bfea8d 100644 --- a/modules/core/src/test/scalajvm/com/siriusxm/snapshot4s/FileSnapshotSpec.scala +++ b/modules/core/src/test/scalajvm/com/siriusxm/snapshot4s/FileSnapshotSpec.scala @@ -41,7 +41,7 @@ object FileSnapshotSpec extends SimpleIOSuite { } private def assert(found: String, path: PathChunk)(config: SnapshotConfig) = { - FileSnapshot(found, path.toString, config, comparison, resultLike) + FileSnapshot(found, path.toString, "FileSnapshotSpec.scala", config, comparison, resultLike) } private def writeSnapshot(snapshot: String, path: PathChunk)(config: SnapshotConfig) = { diff --git a/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala b/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala index 2d5a324..b07b485 100644 --- a/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala +++ b/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala @@ -104,15 +104,22 @@ object generated { val patches = (resourcePatchDir ** (-DirectoryFilter)).get val filteredPatches = patches.filter(file => filter.accept(file.getParent)) filteredPatches.foreach { patchFile => - val patchContents = IO.read(patchFile) - val relativeSourceFile = IO.relativize(resourcePatchDir, patchFile).get - val sourceFile = resourceDir / relativeSourceFile + val patchContents = IO.read(patchFile) + val sourceFile = locateResourceFile(resourcePatchDir, patchFile, resourceDir) IO.delete(patchFile) IO.write(sourceFile, patchContents) log.info(s"Patch applied to $sourceFile") } } + private def locateResourceFile(resourcePatchDir: File, patchFile: File, resourceDir: File) = { + val relativePath = + IO.relativize(resourcePatchDir, patchFile).get + // relative path contains file name like "MyTest.scala" as it's first segment, we need to remove that + val withoutSourceTestFileName = relativePath.split("/").tail.mkString("/") + resourceDir / withoutSourceTestFileName + } + private def applyInlinePatches( log: Logger )(inlinePatchDir: File, sourceDir: File, filter: NameFilter) = { diff --git a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test index e89dadf..277a161 100644 --- a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test +++ b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test @@ -7,11 +7,11 @@ -> ++ 2.13 test -> ++ 3 test # Not update anything with invalid filter -> snapshot4sPromote *IDontExist* -# Tests should fail as snapshots are out of date --> ++ 2.12 test --> ++ 2.13 test --> ++ 3 test +# > snapshot4sPromote *IDontExist* +# # Tests should fail as snapshots are out of date +# -> ++ 2.12 test +# -> ++ 2.13 test +# -> ++ 3 test # # Update snapshots with correct filter > snapshot4sPromote *FilterTest* # # Tests should succeed with updated snapshots From f5c4708b3b625cb1749adc601c32155232bed5da Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Wed, 18 Sep 2024 11:39:34 +0200 Subject: [PATCH 3/6] use full subpaths of suites instead of just file name --- .../main/scala/com/siriusxm/snapshot4s/FileSnapshot.scala | 6 ++---- .../src/main/scala/com/siriusxm/snapshot4s/Locations.scala | 3 --- .../test/scala/com/siriusxm/snapshot4s/LocationsSpec.scala | 6 ------ .../main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala | 4 ++-- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/modules/core/src/main/scala/com/siriusxm/snapshot4s/FileSnapshot.scala b/modules/core/src/main/scala/com/siriusxm/snapshot4s/FileSnapshot.scala index 45baa55..7e95f36 100644 --- a/modules/core/src/main/scala/com/siriusxm/snapshot4s/FileSnapshot.scala +++ b/modules/core/src/main/scala/com/siriusxm/snapshot4s/FileSnapshot.scala @@ -26,13 +26,11 @@ private[snapshot4s] object FileSnapshot { eq: SnapshotEq[String], resultLike: ResultLike[String, E] ): E = resultLike { () => - val sourceFileName = Locations.getFileName(sourceFile) + val relativePath = Locations.relativeSourceFilePath(sourceFile, config) val absoluteSnapshotPath = config.resourceDirectory / RelPath(snapshotPath) def writePatchFile() = { val patchPath = - config.outputDirectory / RelPath("resource-patch") / RelPath(sourceFileName) / RelPath( - snapshotPath - ) + config.outputDirectory / RelPath("resource-patch") / relativePath / RelPath(snapshotPath) patchPath.write(found) } if (absoluteSnapshotPath.exists()) { diff --git a/modules/core/src/main/scala/com/siriusxm/snapshot4s/Locations.scala b/modules/core/src/main/scala/com/siriusxm/snapshot4s/Locations.scala index 23cd1c8..7ba2abd 100644 --- a/modules/core/src/main/scala/com/siriusxm/snapshot4s/Locations.scala +++ b/modules/core/src/main/scala/com/siriusxm/snapshot4s/Locations.scala @@ -18,9 +18,6 @@ package snapshot4s private[snapshot4s] object Locations { - private[snapshot4s] def getFileName(filePath: String): String = - filePath.split("/").last - private[snapshot4s] def relativeSourceFilePath( sourceFile: String, config: SnapshotConfig diff --git a/modules/core/src/test/scala/com/siriusxm/snapshot4s/LocationsSpec.scala b/modules/core/src/test/scala/com/siriusxm/snapshot4s/LocationsSpec.scala index 8644135..c9a940e 100644 --- a/modules/core/src/test/scala/com/siriusxm/snapshot4s/LocationsSpec.scala +++ b/modules/core/src/test/scala/com/siriusxm/snapshot4s/LocationsSpec.scala @@ -56,10 +56,4 @@ We have detected the following configuration: } - pureTest("calculates file name from path") { - val fileName = "MySuites.cala" - val path = s"/path/to/$fileName" - expect.eql(Locations.getFileName(path), fileName) - } - } diff --git a/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala b/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala index b07b485..b600427 100644 --- a/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala +++ b/modules/plugin/src/main/scala/com/siriusxm/snapshot/Snapshot4sPlugin.scala @@ -115,8 +115,8 @@ object generated { private def locateResourceFile(resourcePatchDir: File, patchFile: File, resourceDir: File) = { val relativePath = IO.relativize(resourcePatchDir, patchFile).get - // relative path contains file name like "MyTest.scala" as it's first segment, we need to remove that - val withoutSourceTestFileName = relativePath.split("/").tail.mkString("/") + // relative path starts with subpath like "src/test/scala/MyTest.scala" we need to remove that + val withoutSourceTestFileName = relativePath.split("\\.scala/").tail.mkString("/") resourceDir / withoutSourceTestFileName } From 50f086135bd458e2fd56636386d03783c1b6f21f Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Wed, 18 Sep 2024 12:12:27 +0200 Subject: [PATCH 4/6] adjust tests --- .../siriusxm/snapshot4s/FileSnapshotSpec.scala | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/modules/core/src/test/scalajvm/com/siriusxm/snapshot4s/FileSnapshotSpec.scala b/modules/core/src/test/scalajvm/com/siriusxm/snapshot4s/FileSnapshotSpec.scala index 1bfea8d..550d9e1 100644 --- a/modules/core/src/test/scalajvm/com/siriusxm/snapshot4s/FileSnapshotSpec.scala +++ b/modules/core/src/test/scalajvm/com/siriusxm/snapshot4s/FileSnapshotSpec.scala @@ -41,7 +41,14 @@ object FileSnapshotSpec extends SimpleIOSuite { } private def assert(found: String, path: PathChunk)(config: SnapshotConfig) = { - FileSnapshot(found, path.toString, "FileSnapshotSpec.scala", config, comparison, resultLike) + FileSnapshot( + found, + path.toString, + s"${config.sourceDirectory.value}/src/test/scala/FileSnapshotSpec.scala", + config, + comparison, + resultLike + ) } private def writeSnapshot(snapshot: String, path: PathChunk)(config: SnapshotConfig) = { @@ -94,7 +101,9 @@ object FileSnapshotSpec extends SimpleIOSuite { patches <- getPatches(config) } yield expect.same( patches, - List(config.outputDirectory.osPath / "resource-patch" / "snapshot") + List( + config.outputDirectory.osPath / "resource-patch" / "src" / "test" / "scala" / "FileSnapshotSpec.scala" / "snapshot" + ) ) } @@ -105,7 +114,9 @@ object FileSnapshotSpec extends SimpleIOSuite { patches <- getPatches(config) } yield expect.same( patches, - List(config.outputDirectory.osPath / "resource-patch" / "nested" / "snapshot") + List( + config.outputDirectory.osPath / "resource-patch" / "src" / "test" / "scala" / "FileSnapshotSpec.scala" / "nested" / "snapshot" + ) ) } } From 409331b36a0a7b1e9da75e14c96f819d766f1ea7 Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Wed, 18 Sep 2024 13:37:36 +0200 Subject: [PATCH 5/6] update docs --- docs/markdown/faq.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/markdown/faq.md b/docs/markdown/faq.md index 6c28928..c66541c 100644 --- a/docs/markdown/faq.md +++ b/docs/markdown/faq.md @@ -18,3 +18,7 @@ Unlike `circe-golden`, snapshot4s is aimed at broader example-based testing. It Here are a few tools for other languages: - The [Jest Javascript testing framework](https://jestjs.io/docs/snapshot-testing) supports snapshot testing. - [Insta.rs](https://insta.rs/) is a snapshot testing tool for Rust. + +## Can I choose to promote snapshots only in selected file? + +Yes! Similarly to running only specific test with `testOnly *MySuite*` sbt command, you can use `snapshot4sPromote *MySuite*` to only update the snapshots present in and referenced by `MySuite.scala`. From 070a59b6a3642c91689e6cbc0b4512809c797553 Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Wed, 18 Sep 2024 14:09:57 +0200 Subject: [PATCH 6/6] extend scripted test for snapshot promote filtering --- .../src/test/scala/OtherTest.scala | 29 +++++++++++++++++++ .../sbt-snapshot4s/promote-filter/test | 19 +++++++----- 2 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/scala/OtherTest.scala diff --git a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/scala/OtherTest.scala b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/scala/OtherTest.scala new file mode 100644 index 0000000..6c5edcf --- /dev/null +++ b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/src/test/scala/OtherTest.scala @@ -0,0 +1,29 @@ +package simple + +import weaver.* +import snapshot4s.weaver.SnapshotExpectations +import snapshot4s.generated.snapshotConfig + +object OtherTest extends SimpleIOSuite with SnapshotExpectations { + + test("inline") { + assertInlineSnapshot(123, 456) + } + + test("new inline snapshot") { + assertInlineSnapshot(1, ???) + } + + test("file that doesn't exist") { + assertFileSnapshot("other-contents", "other-nonexistent-file") + } + + test("existing file") { + assertFileSnapshot("contents", "existing-file") + } + + test("file in nested directory") { + assertFileSnapshot("contents", "nested-directory/nested-file") + } + +} diff --git a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test index 277a161..bfcf848 100644 --- a/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test +++ b/modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test @@ -7,14 +7,17 @@ -> ++ 2.13 test -> ++ 3 test # Not update anything with invalid filter -# > snapshot4sPromote *IDontExist* -# # Tests should fail as snapshots are out of date -# -> ++ 2.12 test -# -> ++ 2.13 test -# -> ++ 3 test +> snapshot4sPromote *IDontExist* +# Tests should fail as snapshots are out of date +-> ++ 2.12 test +-> ++ 2.13 test +-> ++ 3 test # # Update snapshots with correct filter > snapshot4sPromote *FilterTest* # # Tests should succeed with updated snapshots -> ++ 2.12 test -> ++ 2.13 test -> ++ 3 test +> ++ 2.12 testOnly *FilterTest* +> ++ 2.13 testOnly *FilterTest* +> ++ 3 testOnly *FilterTest* +-> ++ 2.12 testOnly *OtherTest* +-> ++ 2.13 testOnly *OtherTest* +-> ++ 3 testOnly *OtherTest*