-
Notifications
You must be signed in to change notification settings - Fork 929
/
JUnitXmlTestsListener.scala
258 lines (229 loc) · 9.73 KB
/
JUnitXmlTestsListener.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
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
import java.io.{ File, IOException, PrintWriter, StringWriter }
import java.net.InetAddress
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.Hashtable
import java.util.concurrent.TimeUnit.NANOSECONDS
import scala.collection.mutable.ListBuffer
import scala.util.Properties
import scala.xml.{ Elem, Node => XNode, XML }
import testing.{
Event => TEvent,
NestedTestSelector,
Status => TStatus,
OptionalThrowable,
TestSelector
}
import util.Logger
import sbt.protocol.testing.TestResult
/**
* A tests listener that outputs the results it receives in junit xml
* report format.
* @param outputDir path to the dir in which a folder with results is generated
*/
class JUnitXmlTestsListener(val outputDir: String, logger: Logger) extends TestsListener {
// This constructor is for binary compatibility with older versions of sbt.
def this(outputDir: String) = this(outputDir, null)
/**Current hostname so we know which machine executed the tests*/
val hostname = {
val start = System.nanoTime
val name = try InetAddress.getLocalHost.getHostName
catch {
case _: IOException => "localhost"
}
val elapsed = System.nanoTime - start
if ((NANOSECONDS.toSeconds(elapsed) >= 4) && Properties.isMac && logger != null) {
logger.warn(
s"Getting the hostname $name was slow (${elapsed / 1.0e6} ms). " +
"This is likely because the computer's hostname is not set. You can set the " +
"hostname with the command: scutil --set HostName $(scutil --get LocalHostName)."
)
}
name
}
/**The dir in which we put all result files. Is equal to the given dir + "/test-reports"*/
val targetDir = new File(outputDir + "/test-reports/")
/**all system properties as XML*/
val properties =
<properties>
{
// create a clone, defending against [[ConcurrentModificationException]]
val clonedProperties = System.getProperties.clone.asInstanceOf[Hashtable[AnyRef, AnyRef]]
val iter = clonedProperties.entrySet.iterator
val props: ListBuffer[XNode] = new ListBuffer()
while (iter.hasNext) {
val next = iter.next
props += <property name={next.getKey.toString} value={next.getValue.toString}/>
}
props
}
</properties>
/**
* Gathers data for one Test Suite. We map test groups to TestSuites.
* Each TestSuite gets its own output file.
*/
class TestSuite(val name: String, timestamp: LocalDateTime) {
def this(name: String) = this(name, LocalDateTime.now())
val events: ListBuffer[TEvent] = new ListBuffer()
/**Adds one test result to this suite.*/
def addEvent(e: TEvent) = events += e
/** Returns the number of tests of each state for the specified. */
def count(status: TStatus) = events.count(_.status == status)
/**
* Stops the time measuring and emits the XML for
* All tests collected so far.
*/
def stop(): Elem = {
val duration = events.map(_.duration()).sum
val (errors, failures, tests) = (count(TStatus.Error), count(TStatus.Failure), events.size)
// Junit XML reports don't differentiate between ignored, skipped or pending tests
val ignoredSkippedPending = count(TStatus.Ignored) + count(TStatus.Skipped) + count(
TStatus.Pending
)
val result =
<testsuite hostname={hostname} name={name} tests={tests + ""} errors={errors + ""} failures={
failures + ""
} skipped={ignoredSkippedPending + ""} time={(duration / 1000.0).toString} timestamp={
formatISO8601DateTime(timestamp)
}>
{properties}
{
for (e <- events)
yield
<testcase classname={name} name={
e.selector match {
case selector: TestSelector => selector.testName.split('.').last
case nested: NestedTestSelector =>
nested.suiteId().split('.').last + "." + nested.testName()
case other => s"(It is not a test it is a ${other.getClass.getCanonicalName})"
}
} time={(e.duration() / 1000.0).toString}>
{
val trace: String = if (e.throwable.isDefined) {
val stringWriter = new StringWriter()
val writer = new PrintWriter(stringWriter)
e.throwable.get.printStackTrace(writer)
writer.flush()
stringWriter.toString
} else {
""
}
e.status match {
case TStatus.Error if (e.throwable.isDefined) =>
<error message={e.throwable.get.getMessage} type={
e.throwable.get.getClass.getName
}>{trace}</error>
case TStatus.Error => <error message={"No Exception or message provided"}/>
case TStatus.Failure if (e.throwable.isDefined) =>
<failure message={e.throwable.get.getMessage} type={
e.throwable.get.getClass.getName
}>{trace}</failure>
case TStatus.Failure => <failure message={"No Exception or message provided"}/>
case TStatus.Ignored | TStatus.Skipped | TStatus.Pending => <skipped/>
case _ => {}
}
}
</testcase>
}
<system-out><![CDATA[]]></system-out>
<system-err><![CDATA[]]></system-err>
</testsuite>
result
}
}
/**The currently running test suite*/
private val testSuite = new InheritableThreadLocal[Option[TestSuite]] {
override def initialValue(): Option[TestSuite] = None
}
private def withTestSuite[T](f: TestSuite => T) =
testSuite.get().map(f).getOrElse(sys.error("no test suite"))
/**Creates the output Dir*/
override def doInit() = {
val _ = targetDir.mkdirs()
}
/**
* Starts a new, initially empty Suite with the given name.
*/
override def startGroup(name: String): Unit = testSuite.set(Some(new TestSuite(name)))
/**
* Adds all details for the given even to the current suite.
*/
override def testEvent(event: TestEvent): Unit = for (e <- event.detail) {
withTestSuite(_.addEvent(e))
}
/**
* called for each class or equivalent grouping
* We map one group to one Testsuite, so for each Group
* we create [[https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd JUnit XML file]], and looks like this:
*
* <?xml version="1.0" encoding="UTF-8" ?>
* <testsuite skipped="w" errors="x" failures="y" tests="z" hostname="example.com" name="eu.henkelmann.bla.SomeTest" time="0.23" timestamp="2018-01-01T10:00:00">
* <properties>
* <property name="os.name" value="Linux" />
* ...
* </properties>
* <testcase classname="eu.henkelmann.bla.SomeTest" name="testFooWorks" time="0.0" >
* <error message="the foo did not work" type="java.lang.NullPointerException">... stack ...</error>
* </testcase>
* <testcase classname="eu.henkelmann.bla.SomeTest" name="testBarThrowsException" time="0.0" />
* <testcase classname="eu.henkelmann.bla.SomeTest" name="testBaz" time="0.0">
* <failure message="the baz was no bar" type="junit.framework.AssertionFailedError">...stack...</failure>
* </testcase>
* <system-out><![CDATA[]]></system-out>
* <system-err><![CDATA[]]></system-err>
* </testsuite>
*/
override def endGroup(name: String, t: Throwable) = {
// create our own event to record the error
val event = new TEvent {
def fullyQualifiedName = name
//def description =
//"Throwable escaped the test run of '%s'".format(name)
def duration = -1
def status = TStatus.Error
def fingerprint = null
def selector = null
def throwable = new OptionalThrowable(t)
}
withTestSuite(_.addEvent(event))
writeSuite()
}
/**
* Ends the current suite, wraps up the result and writes it to an XML file
* in the output folder that is named after the suite.
*/
override def endGroup(name: String, result: TestResult) = {
writeSuite()
}
// Here we normalize the name to ensure that it's a nicer filename, rather than
// contort the user into not using spaces.
private[this] def normalizeName(s: String) = s.replaceAll("""\s+""", "-")
/**
* Format the date, without milliseconds or the timezone, per the JUnit spec.
*/
private[this] def formatISO8601DateTime(d: LocalDateTime): String =
d.truncatedTo(ChronoUnit.SECONDS).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
private def writeSuite() = {
val legacyFile =
new File(targetDir, s"${normalizeName(withTestSuite(_.name))}.xml").getAbsolutePath
val file =
new File(targetDir, s"TEST-${normalizeName(withTestSuite(_.name))}.xml").getAbsolutePath
// TODO would be nice to have a logger and log this with level debug
// System.err.println("Writing JUnit XML test report: " + file)
XML.save(legacyFile, withTestSuite(_.stop()), "UTF-8", true, null)
XML.save(file, withTestSuite(_.stop()), "UTF-8", true, null)
testSuite.remove()
}
/**Does nothing, as we write each file after a suite is done.*/
override def doComplete(finalResult: TestResult): Unit = {}
/**Returns None*/
override def contentLogger(test: TestDefinition): Option[ContentLogger] = None
}