Skip to content

Commit 674f82d

Browse files
authored
Implement assertGoldenFile and assertGoldenLiteral tests (#394)
This PR implements `assertGoldenLiteral` and `assertGoldenFile`, which are assertions that not only compare the given values (literal v.s. literal or string v.s. file contents respectively), but can take the `UTEST_UPDATE_GOLDEN_TESTS` environment variable to automatically update the source code and golden files to new values. - `assertGoldenFile` is straightforward: if the contents of the specified file does not match the given string, `UTEST_UPDATE_GOLDEN_TESTS` makes utest over-write that file with the given string. - `assertGoldenLiteral` is more tricky: this takes a new `utest.framework.GoldenFix.Span[T]` implicit conversion similar to `sourcecode.Text`, but instead of just capturing the text contents of the expression it captures the sourcefile and the start/end offset of the literal within it. If the two values do not match, `UTEST_UPDATE_GOLDEN_TESTS` makes utest use `utest.shaded.pprint.apply` to pretty-print the given expression and the sourcefile/start/end metadata to over-write the original source file in-place. Some extra book-keeping is necessary to make sure the offsets line up even when there are multiple edits in the same file, and to ensure the indentation of start of the expression is preserved Inspired by Jane Street's https://blog.janestreet.com/the-joy-of-expect-tests/[What if writing tests was a joyful experience?] blog post
1 parent 8d9612d commit 674f82d

25 files changed

+386
-53
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package utest.framework
2+
3+
import scala.language.experimental.macros
4+
import scala.reflect.internal.util.RangePosition
5+
6+
trait GoldenSpanMacros {
7+
implicit def generate[T](v: T): GoldenFix.Span[T] = macro GoldenSpanMacros.text[T]
8+
9+
def apply[T](v: T): GoldenFix.Span[T] = macro GoldenSpanMacros.text[T]
10+
}
11+
12+
object GoldenSpanMacros {
13+
object Compat {
14+
type Context = scala.reflect.macros.blackbox.Context
15+
16+
def enclosingOwner(c: Context) = c.internal.enclosingOwner
17+
18+
def enclosingParamList(c: Context): List[List[c.Symbol]] = {
19+
def nearestEnclosingMethod(owner: c.Symbol): c.Symbol =
20+
if (owner.isMethod) owner
21+
else if (owner.isClass) owner.asClass.primaryConstructor
22+
else nearestEnclosingMethod(owner.owner)
23+
24+
nearestEnclosingMethod(enclosingOwner(c)).asMethod.paramLists
25+
}
26+
}
27+
def text[T: c.WeakTypeTag](c: Compat.Context)(v: c.Expr[T]): c.Expr[GoldenFix.Span[T]] = {
28+
import c.universe._
29+
val (start, end) = if (v.tree.pos.isInstanceOf[RangePosition]) {
30+
val r = v.tree.pos.asInstanceOf[RangePosition]
31+
(r.start, r.end)
32+
} else (-1, -1)
33+
val tree = q"""${c.prefix}(${v.tree}, ${v.tree.pos.source.path}, $start, $end)"""
34+
c.Expr[GoldenFix.Span[T]](tree)
35+
}
36+
}

utest/src-2-jvm/utest/PlatformShims.scala renamed to utest/src-2-jvm/utest/framework/PlatformShims.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
package utest
2-
3-
import scala.concurrent.{Await, Future}
4-
import concurrent.duration._
1+
package utest.framework
52

63
/**
74
* Platform specific stuff that differs between JVM and JS

utest/src-2/utest/asserts/Asserts.scala

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package utest
22
package asserts
33
//import acyclic.file
4-
import utest.framework.StackMarker
54

65
import scala.annotation.{StaticAnnotation, tailrec}
76
import scala.collection.mutable
@@ -134,12 +133,6 @@ trait AssertsVersionSpecific {
134133
*/
135134
def assertCompileError(expr: String): CompileError = macro Asserts.assertCompileError
136135

137-
/**
138-
* Forwarder for `Predef.assert`, for when you want to explicitly write the
139-
* assert message and don't want or need the fancy smart asserts
140-
*/
141-
def assert(expr: Boolean, msg: => Any) = Predef.assert(expr, msg)
142-
143136
/**
144137
* Checks that the expression is true; otherwise raises an
145138
* exception with some debugging info
@@ -175,5 +168,6 @@ trait AssertsVersionSpecific {
175168
* exception does not appear.
176169
*/
177170
def assertThrows[T: ClassTag](expr: Unit): T = macro Asserts.assertThrowsProxy[T]
171+
178172
}
179173

utest/src-3-jvm/utest/EnableReflectiveInstantiation.java renamed to utest/src-3-jvm/utest/framework/EnableReflectiveInstantiation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package utest;
1+
package utest.framework;
22

33
import java.lang.annotation.*;
44

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package utest.framework
2+
3+
import scala.language.implicitConversions
4+
import scala.quoted.*
5+
6+
7+
trait GoldenSpanMacros {
8+
inline implicit def generate[T](v: => T): GoldenFix.Span[T] = ${ GoldenSpanMacros.text('v) }
9+
inline def apply[T](v: => T): GoldenFix.Span[T] = ${ GoldenSpanMacros.text('v) }
10+
}
11+
object GoldenSpanMacros {
12+
def text[T: Type](v: Expr[T])(using quotes: Quotes): Expr[GoldenFix.Span[T]] = {
13+
import quotes.reflect.*
14+
'{
15+
new GoldenFix.Span[T](
16+
$v,
17+
${Expr(v.asTerm.pos.sourceFile.path)},
18+
${Expr(v.asTerm.pos.start)},
19+
${Expr(v.asTerm.pos.end)},
20+
)
21+
}
22+
}
23+
24+
}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
package utest
2-
3-
import scala.concurrent.{Await, Future}
4-
import concurrent.duration._
1+
package utest.framework
52

63
/**
74
* Platform specific stuff that differs between JVM and JS
85
*/
96
trait PlatformShimsVersionSpecific {
107
type EnableReflectiveInstantiation =
11-
utest.EnableReflectiveInstantiation
8+
utest.framework.EnableReflectiveInstantiation
129

1310
val Reflect = PortableScalaReflectExcerpts
1411
}

utest/src-3-jvm/utest/PortableScalaReflectExcerpts.scala renamed to utest/src-3-jvm/utest/framework/PortableScalaReflectExcerpts.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package utest
1+
package utest.framework
22

3-
import java.lang.reflect._
3+
import java.lang.reflect.*
44
import scala.collection.mutable
55

66
/**

utest/src-3/utest/asserts/Asserts.scala

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,6 @@ trait AssertsVersionSpecific {
7070
*/
7171
inline def assertAll(inline expr: Boolean*): Unit = ${Asserts.assertAllProxy('expr)}
7272

73-
/**
74-
* Forwarder for `Predef.assert`, for when you want to explicitly write the
75-
* assert message and don't want or need the fancy smart asserts
76-
*/
77-
def assert(expr: Boolean, msg: => Any) = Predef.assert(expr, msg)
78-
7973
/**
8074
* Checks that one or more expressions all become true within a certain
8175
* period of time. Polls at a regular interval to check this.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package utest.asserts
2+
3+
trait AssertsPlatformSpecific {
4+
5+
}

utest/src-js/utest/PlatformShims.scala renamed to utest/src-js/utest/framework/PlatformShims.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
package utest
1+
package utest.framework
22

3-
import scala.concurrent.Future
43
import org.portablescala.reflect.Reflect
54

5+
import scala.concurrent.Future
6+
67
/**
78
* Platform specific stuff that differs between JVM, JS and Native
89
*/

0 commit comments

Comments
 (0)