mirror of https://github.com/sbt/sbt.git
Merge pull request #1278 from sbt/wip/junit-xml-reporter
Migrate JUnitXmlReporter into sbt as autoplugin
This commit is contained in:
commit
1819276bb8
|
|
@ -31,7 +31,8 @@ object PluginDiscovery
|
|||
val defaultAutoPlugins = Seq(
|
||||
"sbt.plugins.IvyPlugin" -> sbt.plugins.IvyPlugin,
|
||||
"sbt.plugins.JvmPlugin" -> sbt.plugins.JvmPlugin,
|
||||
"sbt.plugins.CorePlugin" -> sbt.plugins.CorePlugin
|
||||
"sbt.plugins.CorePlugin" -> sbt.plugins.CorePlugin,
|
||||
"sbt.plugins.JUnitXmlReportPlugin" -> sbt.plugins.JUnitXmlReportPlugin
|
||||
)
|
||||
val detectedAutoPugins = discover[AutoPlugin](AutoPlugins)
|
||||
val allAutoPlugins = (defaultAutoPlugins ++ detectedAutoPugins.modules) map { case (name, value) =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package sbt
|
||||
package plugins
|
||||
|
||||
import Def.Setting
|
||||
import Keys._
|
||||
import Project.inConfig
|
||||
import Configurations.Test
|
||||
|
||||
/** An experimental plugin that adds the ability for junit-xml to be generated.
|
||||
*
|
||||
* To disable this plugin, you need to add:
|
||||
* {{{
|
||||
* val myProject = project in file(".") disablePlugins (plugins.JunitXmlReportPlugin)
|
||||
* }}}
|
||||
*
|
||||
* Note: Using AutoPlugins to enable/disable build features is experimental in sbt 0.13.5.
|
||||
*/
|
||||
object JUnitXmlReportPlugin extends AutoPlugin {
|
||||
// TODO - If testing becomes its own plugin, we only rely on the core settings.
|
||||
override def requires = JvmPlugin
|
||||
override def trigger = allRequirements
|
||||
|
||||
// Right now we add to the global test listeners which should capture *all* tests.
|
||||
// It might be a good idea to derive this setting into specific test scopes.
|
||||
override lazy val projectSettings: Seq[Setting[_]] =
|
||||
Seq(
|
||||
testListeners += new JUnitXmlTestsListener(target.value.getAbsolutePath)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import sbt._
|
||||
import Keys._
|
||||
import scala.xml.XML
|
||||
import Tests._
|
||||
import Defaults._
|
||||
|
||||
object JUnitXmlReportTest extends Build {
|
||||
val checkReport = taskKey[Unit]("Check the test reports")
|
||||
val checkNoReport = taskKey[Unit]("Check that no reports are present")
|
||||
|
||||
private val oneSecondReportFile = "target/test-reports/a.pkg.OneSecondTest.xml"
|
||||
private val failingReportFile = "target/test-reports/another.pkg.FailingTest.xml"
|
||||
|
||||
lazy val root = Project("root", file("."), settings = defaultSettings ++ Seq(
|
||||
scalaVersion := "2.9.2",
|
||||
libraryDependencies += "com.novocode" % "junit-interface" % "0.10" % "test",
|
||||
|
||||
// TODO use matchers instead of sys.error
|
||||
checkReport := {
|
||||
val oneSecondReport = XML.loadFile(oneSecondReportFile)
|
||||
if( oneSecondReport.label != "testsuite" ) sys.error("Report should have a root <testsuite> element.")
|
||||
// somehow the 'success' event doesn't go through... TODO investigate
|
||||
// if( (oneSecondReport \ "@time").text.toFloat < 1f ) sys.error("expected test to take at least 1 sec")
|
||||
if( (oneSecondReport \ "@name").text != "a.pkg.OneSecondTest" ) sys.error("wrong test name: " + (oneSecondReport \ "@name").text)
|
||||
// TODO more checks
|
||||
|
||||
val failingReport = XML.loadFile(failingReportFile)
|
||||
if( failingReport.label != "testsuite" ) sys.error("Report should have a root <testsuite> element.")
|
||||
if( (failingReport \ "@failures").text != "2" ) sys.error("expected 2 failures")
|
||||
if( (failingReport \ "@name").text != "another.pkg.FailingTest" ) sys.error("wrong test name: " + (failingReport \ "@name").text)
|
||||
// TODO more checks -> the two test cases with time etc..
|
||||
|
||||
// TODO check console output is in the report
|
||||
},
|
||||
|
||||
checkNoReport := {
|
||||
if( file(oneSecondReportFile).exists() ) sys.error(oneSecondReportFile + " should not exist")
|
||||
if( file(failingReportFile).exists() ) sys.error(failingReportFile + " should not exist")
|
||||
}
|
||||
))
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import org.junit.Test
|
||||
|
||||
package a.pkg {
|
||||
class OneSecondTest {
|
||||
@Test
|
||||
def oneSecond() {
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package another.pkg {
|
||||
class FailingTest {
|
||||
@Test
|
||||
def failure1_OneSecond() {
|
||||
Thread.sleep(1000)
|
||||
sys.error("fail1")
|
||||
}
|
||||
|
||||
@Test
|
||||
def failure2_HalfSecond() {
|
||||
Thread.sleep(500)
|
||||
sys.error("fail2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package console.test.pkg {
|
||||
// we won't check console output in the report
|
||||
// until SBT supports that
|
||||
class ConsoleTests {
|
||||
@Test
|
||||
def sayHello() {
|
||||
println("Hello")
|
||||
System.out.println("World!")
|
||||
}
|
||||
|
||||
@Test
|
||||
def multiThreadedHello() {
|
||||
for( i <- 1 to 5 ) {
|
||||
new Thread("t-" + i) {
|
||||
override def run() {
|
||||
println("Hello from thread " + i)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
-> test
|
||||
> checkReport
|
||||
|
||||
# there might be discrepancies between the 'normal' and the 'forked' mode
|
||||
|
||||
> clean
|
||||
> checkNoReport
|
||||
|
||||
> set fork in Test := true
|
||||
-> test
|
||||
> checkReport
|
||||
|
|
@ -13,6 +13,7 @@ Changes
|
|||
- name-hashing incremental compiler now supports scala macros.
|
||||
- ``testResultLogger`` is now configured.
|
||||
- sbt-server hooks for task cancellation.
|
||||
- Add ``JUnitXmlReportPlugin`` which generates junit-xml-reports for all tests.
|
||||
|
||||
|
||||
0.13.1 to 0.13.2
|
||||
|
|
|
|||
|
|
@ -104,6 +104,18 @@ tests for that file complete. This can be disabled by setting :key:`logBuffered`
|
|||
|
||||
logBuffered in Test := false
|
||||
|
||||
|
||||
Test Reports
|
||||
------------
|
||||
By default, sbt will generate JUnit XML test reports for all tests in the build, located
|
||||
in the ``target/test-reports`` directory for a project. This can be disabled by
|
||||
disabling the ``JUnitXmlReportPlugin``
|
||||
|
||||
::
|
||||
|
||||
val myProject = project in file(".") disablePlugins (plugins.JUnitXmlReportPlugin)
|
||||
|
||||
|
||||
Options
|
||||
=======
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
package sbt
|
||||
|
||||
import java.io.{StringWriter, PrintWriter, File}
|
||||
import java.net.InetAddress
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.util.DynamicVariable
|
||||
import scala.xml.{Elem, Node, XML}
|
||||
import testing.{Event => TEvent, Status => TStatus, OptionalThrowable, TestSelector}
|
||||
|
||||
/**
|
||||
* 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) extends TestsListener
|
||||
{
|
||||
/**Current hostname so we know which machine executed the tests*/
|
||||
val hostname = InetAddress.getLocalHost.getHostName
|
||||
/**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> {
|
||||
val iter = System.getProperties.entrySet.iterator
|
||||
val props:ListBuffer[Node] = 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) {
|
||||
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)
|
||||
|
||||
val result = <testsuite hostname={hostname} name={name}
|
||||
tests={tests + ""} errors={errors + ""} failures={failures + ""}
|
||||
time={(duration/1000.0).toString} >
|
||||
{properties}
|
||||
{
|
||||
for (e <- events) yield
|
||||
<testcase classname={name}
|
||||
name={
|
||||
e.selector match {
|
||||
case selector: TestSelector => selector.testName.split('.').last
|
||||
case _ => "(It is not a test)"
|
||||
}
|
||||
}
|
||||
time={(e.duration() / 1000.0).toString}> {
|
||||
var 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.Skipped => <skipped />
|
||||
case _ => {}
|
||||
}
|
||||
}
|
||||
</testcase>
|
||||
|
||||
}
|
||||
<system-out><![CDATA[]]></system-out>
|
||||
<system-err><![CDATA[]]></system-err>
|
||||
</testsuite>
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/**The currently running test suite*/
|
||||
var testSuite = new DynamicVariable(null: TestSuite)
|
||||
|
||||
/**Creates the output Dir*/
|
||||
override def doInit() = {targetDir.mkdirs()}
|
||||
|
||||
/** Starts a new, initially empty Suite with the given name.
|
||||
*/
|
||||
override def startGroup(name: String) {testSuite.value_=(new TestSuite(name))}
|
||||
|
||||
/** Adds all details for the given even to the current suite.
|
||||
*/
|
||||
override def testEvent(event: TestEvent): Unit = for (e <- event.detail) {testSuite.value.addEvent(e)}
|
||||
|
||||
/** called for each class or equivalent grouping
|
||||
* We map one group to one Testsuite, so for each Group
|
||||
* we create an XML like this:
|
||||
* <?xml version="1.0" encoding="UTF-8" ?>
|
||||
* <testsuite errors="x" failures="y" tests="z" hostname="example.com" name="eu.henkelmann.bla.SomeTest" time="0.23">
|
||||
* <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)
|
||||
}
|
||||
testSuite.value.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.Value) = {
|
||||
writeSuite()
|
||||
}
|
||||
|
||||
private def writeSuite() = {
|
||||
val file = new File(targetDir, testSuite.value.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 (file, testSuite.value.stop(), "UTF-8", true, null)
|
||||
}
|
||||
|
||||
/**Does nothing, as we write each file after a suite is done.*/
|
||||
override def doComplete(finalResult: TestResult.Value): Unit = {}
|
||||
|
||||
/**Returns None*/
|
||||
override def contentLogger(test: TestDefinition): Option[ContentLogger] = None
|
||||
}
|
||||
Loading…
Reference in New Issue