Skip to content

Commit

Permalink
implement selective promote for file snapshots
Browse files Browse the repository at this point in the history
  • Loading branch information
majk-p committed Sep 17, 2024
1 parent 2052eb2 commit 5ee6963
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -43,4 +48,5 @@ private[snapshot4s] object FileSnapshot {
Result.NonExistent(found)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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")
}

Expand All @@ -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
Expand All @@ -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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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) = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) = {
Expand Down
10 changes: 5 additions & 5 deletions modules/plugin/src/sbt-test/sbt-snapshot4s/promote-filter/test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 5ee6963

Please sign in to comment.