/
Assertions.scala
333 lines (307 loc) · 9.9 KB
/
Assertions.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package munit
import munit.internal.console.{Lines, StackTraces}
import munit.diff.console.Printers
import munit.diff.Printer
import munit.Clue
import munit.Clues
import munit.diff.EmptyPrinter
import scala.reflect.ClassTag
import scala.util.control.NonFatal
import scala.collection.mutable
import munit.diff.console.AnsiColors
import org.junit.AssumptionViolatedException
import munit.internal.MacroCompat
object Assertions extends Assertions
trait Assertions extends MacroCompat.CompileErrorMacro {
val munitLines = new Lines
def munitAnsiColors: Boolean = true
private def munitFilterAnsi(message: String): String =
if (munitAnsiColors) message
else AnsiColors.filterAnsi(message)
def assert(
cond: => Boolean,
clue: => Any = "assertion failed"
)(implicit loc: Location): Unit = {
StackTraces.dropInside {
val (isTrue, clues) = munitCaptureClues(cond)
if (!isTrue) {
fail(munitPrint(clue), clues)
}
}
}
def assume(
cond: Boolean,
clue: => Any = "assumption failed"
)(implicit loc: Location): Unit = {
StackTraces.dropInside {
if (!cond) {
throw new AssumptionViolatedException(munitPrint(clue))
}
}
}
def assertNoDiff(
obtained: String,
expected: String,
clue: => Any = "diff assertion failed"
)(implicit loc: Location): Unit = {
StackTraces.dropInside {
DiffsAssetion.assertNoDiff(
obtained,
expected,
exceptionHandlerFromAssertions(this, Clues.empty),
munitPrint(clue),
printObtainedAsStripMargin = true
)
}
}
/**
* Asserts that two elements are not equal according to the `Compare[A, B]` type-class.
*
* By default, uses `==` to compare values.
*/
def assertNotEquals[A, B](
obtained: A,
expected: B,
clue: => Any = "values are the same"
)(implicit loc: Location, compare: Compare[A, B]): Unit = {
StackTraces.dropInside {
if (compare.isEqual(obtained, expected)) {
failComparison(
s"${munitPrint(clue)}, expected 2 different values: $expected is equal to $obtained",
obtained,
expected
)
}
}
}
/**
* Asserts that two elements are equal according to the `Compare[A, B]` type-class.
*
* By default, uses `==` to compare values.
*/
def assertEquals[A, B](
obtained: A,
expected: B,
clue: => Any = "values are not the same"
)(implicit loc: Location, compare: Compare[A, B]): Unit = {
StackTraces.dropInside {
if (!compare.isEqual(obtained, expected)) {
(obtained, expected) match {
case (a: Array[_], b: Array[_]) if a.sameElements(b) =>
// Special-case error message when comparing arrays. See
// https://github.com/scalameta/munit/pull/393 and
// https://github.com/scalameta/munit/issues/339 for a related
// discussion on how MUnit should handle array comparisons. Other
// testing frameworks have special cases for arrays so the
// comparison succeeds as long as `sameElements()` returns true.
// MUnit chooses instead to fail the test with a custom error
// message because arrays have reference equality, for better or
// worse, and we should not hide that fact from our users.
failComparison(
"arrays have the same elements but different reference equality. " +
"Convert the arrays to a non-Array collection if you intend to assert the two arrays have the same elements. " +
"For example, `assertEquals(a.toSeq, b.toSeq)",
obtained,
expected
)
case _ =>
}
compare.failEqualsComparison(obtained, expected, clue, loc, this)
}
}
}
/**
* Asserts that two doubles are equal to within a positive delta.
* If the expected value is infinity then the delta value is ignored.
* NaNs are considered equal: assertEquals(Double.NaN, Double.NaN, *) passes.
*/
def assertEqualsDouble(
obtained: Double,
expected: Double,
delta: Double,
clue: => Any = "values are not the same"
)(implicit loc: Location): Unit = {
StackTraces.dropInside {
val exactlyTheSame = java.lang.Double.compare(expected, obtained) == 0
val almostTheSame = Math.abs(expected - obtained) <= delta
if (!exactlyTheSame && !almostTheSame) {
failComparison(
s"${munitPrint(clue)} expected: $expected but was: $obtained",
obtained,
expected
)
}
}
}
/**
* Asserts that two floats are equal to within a positive delta.
* If the expected value is infinity then the delta value is ignored.
* NaNs are considered equal: assertEquals(Float.NaN, Float.NaN, *) passes.
*/
def assertEqualsFloat(
obtained: Float,
expected: Float,
delta: Float,
clue: => Any = "values are not the same"
)(implicit loc: Location): Unit = {
StackTraces.dropInside {
val exactlyTheSame = java.lang.Float.compare(expected, obtained) == 0
val almostTheSame = Math.abs(expected - obtained) <= delta
if (!exactlyTheSame && !almostTheSame) {
failComparison(
s"${munitPrint(clue)} expected: $expected but was: $obtained",
obtained,
expected
)
}
}
}
/**
* Evalutes the given expression and asserts that an exception of type T is thrown.
*/
def intercept[T <: Throwable](
body: => Any
)(implicit T: ClassTag[T], loc: Location): T = {
runIntercept(None, body)
}
/**
* Evalutes the given expression and asserts that an exception of type T with the expected message is thrown.
*/
def interceptMessage[T <: Throwable](expectedExceptionMessage: String)(
body: => Any
)(implicit T: ClassTag[T], loc: Location): T = {
runIntercept(Some(expectedExceptionMessage), body)
}
private def runIntercept[T <: Throwable](
expectedExceptionMessage: Option[String],
body: => Any
)(implicit T: ClassTag[T], loc: Location): T = {
try {
body
fail(
s"expected exception of type '${T.runtimeClass.getName()}' but body evaluated successfully"
)
} catch {
case e: FailExceptionLike[_]
if !T.runtimeClass.isAssignableFrom(e.getClass()) =>
throw e
case NonFatal(e) =>
if (T.runtimeClass.isAssignableFrom(e.getClass())) {
if (
expectedExceptionMessage.isEmpty || e.getMessage == expectedExceptionMessage.get
)
e.asInstanceOf[T]
else {
val obtained = e.getClass().getName()
throw new FailException(
s"intercept failed, exception '$obtained' had message '${e.getMessage}', which was different from expected message '${expectedExceptionMessage.get}'",
cause = e,
isStackTracesEnabled = false,
location = loc
)
}
} else {
val obtained = e.getClass().getName()
val expected = T.runtimeClass.getName()
throw new FailException(
s"intercept failed, exception '$obtained' is not a subtype of '$expected",
cause = e,
isStackTracesEnabled = false,
location = loc
)
}
}
}
/**
* Unconditionally fails this test with the given message and exception marked as the cause.
*/
def fail(message: String, cause: Throwable)(implicit
loc: Location
): Nothing = {
throw new FailException(
munitFilterAnsi(munitLines.formatLine(loc, message)),
cause,
isStackTracesEnabled = true,
location = loc
)
}
/**
* Unconditionally fails this test with the given message and optional clues.
*/
def fail(
message: String,
clues: Clues = new Clues(Nil)
)(implicit loc: Location): Nothing = {
throw new FailException(
munitFilterAnsi(munitLines.formatLine(loc, message, clues)),
loc
)
}
/**
* Unconditionally fails this test due to result of comparing two values.
*
* The only reason to use this method instead of `fail()` is if you want to
* allow comparing the two different values in the the IntelliJ GUI diff
* viewer.
*/
def failComparison(
message: String,
obtained: Any,
expected: Any,
clues: Clues = new Clues(Nil)
)(implicit loc: Location): Nothing = {
throw new ComparisonFailException(
munitFilterAnsi(munitLines.formatLine(loc, message, clues)),
obtained,
expected,
loc,
isStackTracesEnabled = false
)
}
/**
* Unconditionally fail this test case and cancel all the subsequent tests in this suite.
*/
def failSuite(
message: String,
clues: Clues = new Clues(Nil)
)(implicit loc: Location): Nothing = {
throw new FailSuiteException(
munitFilterAnsi(munitLines.formatLine(loc, message, clues)),
loc
)
}
private def exceptionHandlerFromAssertions(
assertions: Assertions,
clues: => Clues
): ComparisonFailExceptionHandler =
new ComparisonFailExceptionHandler {
def handle(
message: String,
obtained: String,
expected: String,
loc: Location
): Nothing = {
assertions.failComparison(message, obtained, expected, clues)(loc)
}
}
private val munitCapturedClues: mutable.ListBuffer[Clue[_]] =
mutable.ListBuffer.empty
def munitCaptureClues[T](thunk: => T): (T, Clues) =
synchronized {
munitCapturedClues.clear()
val result = thunk
(result, new Clues(munitCapturedClues.toList))
}
def clue[T](c: Clue[T]): T = synchronized {
munitCapturedClues += c
c.value
}
def clues(clue: Clue[_]*): Clues = new Clues(clue.toList)
def printer: Printer = EmptyPrinter
def munitPrint(clue: => Any): String = {
clue match {
case message: String => message
case value => Printers.print(value, printer)
}
}
}