/
Checkpoints.scala
171 lines (163 loc) · 6.97 KB
/
Checkpoints.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
/*
* Copyright 2001-2013 Artima, Inc.
*
* 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 org.scalatest
import org.scalatest.exceptions.StackDepthException
import org.scalatest.exceptions._
import org.scalactic._
/**
* Trait providing class <code>Checkpoint</code>, which enables multiple assertions
* to be performed within a test, with any failures accumulated and reported
* together at the end of the test.
*
* <p>
* Because ScalaTest uses exceptions to signal failed assertions, normally execution
* of a test will stop as soon as the first failed assertion is encountered. Trait
* <code>Checkpoints</code> provides an option when you want to continue executing
* the remainder of the test body, or part of it, even if an assertion has already failed in that test.
* </p>
* <p>
* To use a <code>Checkpoint</code> (once you've mixed in or imported the members of trait
* <code>Checkpoints</code>), you first need to create one, like this:
* </p>
*
* <pre>
* val cp = new Checkpoint
* </pre>
*
* <p>
* Then give the <code>Checkpoint</code> assertions to execute by passing them (via a by-name parameter)
* to its <code>apply</code> method, like this:
* </p>
*
* <pre class="stHighlighted">
* <span class="stReserved">val</span> (x, y) = (<span class="stLiteral">1</span>, <span class="stLiteral">2</span>)
* cp { x should be < <span class="stLiteral">0</span> }
* cp { y should be > <span class="stLiteral">9</span> }
* </pre>
*
* <p>
* Both of the above assertions will fail, but it won't be reported yet. The <code>Checkpoint</code> will execute them
* right away, each time its <code>apply</code> method is invoked. But it will catch the <code>TestFailedExceptions</code> and
* save them, only reporting them later when <code>reportAll</code> is invoked. Thus, at the end of the test, you must call
* <code>reportAll</code>, like this:
* </p>
*
* <pre>
* cp.reportAll()
* </pre>
*
* <p>
* This <code>reportAll</code> invocation will complete abruptly with a <code>TestFailedException</code> whose message
* includes the message, source file, and line number of each of the checkpointed assertions that previously failed. For example:
* </p>
*
* <pre>
* 1 was not less than 0 (in Checkpoint) at ExampleSpec.scala:12
* 2 was not greater than 9 (in Checkpoint) at ExampleSpec.scala:13
* </pre>
*
* <p>
* Make sure you invoke <code>reportAll</code> before the test completes, otherwise any failures that were detected by the
* <code>Checkpoint</code> will not be reported.
* </p>
*
* <p>
* Note that a <code>Checkpoint</code> will catch and record for later reporting (via <code>reportAll</code>) exceptions that mix in <code>StackDepth</code>
* except for <code>TestCanceledException</code>, <code>TestRegistrationClosedException</code>, <code>NotAllowedException</code>,
* and <code>DuplicateTestNameException</code>. If a block of code passed to a <code>Checkpoint</code>'s <code>apply</code> method completes
* abruptly with any of the <code>StackDepth</code> exceptions in the previous list, or any non-<code>StackDepth</code> exception, that invocation
* of the <code>apply</code> method will complete abruptly with the same exception immediately. Unless you put <code>reportAll</code> in a finally
* clause and handle this case, such an unexpected exception will cause you to lose any information about assertions that failed earlier in the test and were
* recorded by the <code>Checkpoint</code>.
* </p>
*
* @author Bill Venners
* @author George Berger
*/
trait Checkpoints {
/**
* Class that allows multiple assertions to be performed within a test, with any
* failures accumulated and reported together at the end of the test.
*
* <p>
* See the main documentation for trait <code>Checkpoints</code> for more information and an example.
* </p>
*/
class Checkpoint {
private final val failures: ConcurrentLinkedQueue[Throwable with StackDepth] =
new ConcurrentLinkedQueue
//
// Returns a string containing the file name and line number where
// the test failure occurred, e.g. "HelloSuite.scala:18".
//
private def getFailLine(t: Throwable with StackDepth): String =
t.failedCodeFileNameAndLineNumberString match {
case Some(failLine) => failLine
case None => "unknown line number"
}
/**
* Executes the passed block of code and catches and records for later reporting (via <code>reportAll</code>) any exceptions that mix in <code>StackDepth</code>
* except for <code>TestCanceledException</code>, <code>TestRegistrationClosedException </code>, <code>NotAllowedException </code>,
* and <code>DuplicateTestNameException </code>.
*
* <p>
* If the block of code completes abruptly with any of the <code>StackDepth</code> exceptions in the
* previous list, or any non-<code>StackDepth</code> exception, that invocation of this <code>apply</code> method will complete abruptly
* with the same exception.
* </p>
*
* @param f the block of code, likely containing one or more assertions, to execute
*/
def apply(f: => Unit): Unit = {
try {
f
}
catch {
case e: TestCanceledException => throw e
case e: TestRegistrationClosedException => throw e
case e: NotAllowedException => throw e
case e: DuplicateTestNameException => throw e
case e: Throwable with StackDepth => failures.add(e)
case e: Throwable => throw e
}
}
/**
* If any failures were caught by checkpoints, throws a <code>TestFailedException</code>
* whose detail message lists the failure messages and line numbers from each of the
* failed checkpoints.
*/
def reportAll()(implicit pos: source.Position): Unit = {
// SKIP-SCALATESTJS,NATIVE-START
val stackDepth = 1
// SKIP-SCALATESTJS,NATIVE-END
//SCALATESTJS,NATIVE-ONLY val stackDepth = 10
if (!failures.isEmpty) {
val failMessages =
for (failure <- failures.asScala)
yield failure.getMessage + " " + Resources.atCheckpointAt + " " + getFailLine(failure)
throw new TestFailedException((sde: StackDepthException) => Some(failMessages.mkString("\n")), None, pos)
}
}
}
}
/**
* Companion object that facilitates the importing the members of trait <code>Checkpoints</code> as
* an alternative to mixing it in. One use case is to import <code>Checkpoints</code> so you can use
* it in the Scala interpreter.
*
* @author Bill Venners
*/
object Checkpoints extends Checkpoints