Merge pull request #1278 from sbt/wip/junit-xml-reporter

Migrate JUnitXmlReporter into sbt as autoplugin
This commit is contained in:
eugene yokota 2014-04-22 16:53:02 -04:00
commit 1819276bb8
8 changed files with 313 additions and 1 deletions

View File

@ -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) =>

View File

@ -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)
)
}

View File

@ -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")
}
))
}

View File

@ -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()
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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
=======

View File

@ -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
}