mirror of https://github.com/sbt/sbt.git
Merge a1db6902aa into 8bcc6ae420
This commit is contained in:
commit
5ac55877ee
|
|
@ -28,6 +28,7 @@ import testing.{
|
|||
}
|
||||
import util.Logger
|
||||
import sbt.protocol.testing.TestResult
|
||||
import sbt.internal.worker1.ForkTestMain
|
||||
|
||||
/**
|
||||
* Companion object for JUnitXmlTestsListener that caches the hostname lazily.
|
||||
|
|
@ -159,25 +160,36 @@ class JUnitXmlTestsListener(val targetDir: File, legacyTestReport: Boolean, logg
|
|||
} 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
|
||||
e.throwable.get match {
|
||||
case fe: ForkTestMain.ForkError =>
|
||||
formatForkErrorTrace(fe)
|
||||
case other =>
|
||||
val stringWriter = new StringWriter()
|
||||
val writer = new PrintWriter(stringWriter)
|
||||
other.printStackTrace(writer)
|
||||
writer.flush()
|
||||
stringWriter.toString
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val (exType, exMessage) = e.throwable match {
|
||||
case t if t.isDefined =>
|
||||
t.get match {
|
||||
case fe: ForkTestMain.ForkError =>
|
||||
(fe.getOriginalName, fe.getOriginalMessage)
|
||||
case other =>
|
||||
(other.getClass.getName, other.getMessage)
|
||||
}
|
||||
case _ => ("", "")
|
||||
}
|
||||
e.status match {
|
||||
case TStatus.Error if (e.throwable.isDefined) =>
|
||||
<error message={e.throwable.get.getMessage} type={
|
||||
e.throwable.get.getClass.getName
|
||||
}>{trace}</error>
|
||||
<error message={exMessage} type={exType}>{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>
|
||||
<failure message={exMessage} type={exType}>{trace}</failure>
|
||||
case TStatus.Failure =>
|
||||
<failure message={"No Exception or message provided"}/>
|
||||
case TStatus.Ignored | TStatus.Skipped | TStatus.Pending =>
|
||||
|
|
@ -262,6 +274,36 @@ class JUnitXmlTestsListener(val targetDir: File, legacyTestReport: Boolean, logg
|
|||
writeSuite()
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a ForkError stacktrace using the original exception name instead of
|
||||
* the ForkError wrapper class. This fixes the JUnit XML report showing
|
||||
* `sbt.internal.worker1.ForkTestMain$ForkError` instead of the actual exception type.
|
||||
* See https://github.com/sbt/sbt/issues/1469
|
||||
*/
|
||||
private def formatForkErrorTrace(fe: ForkTestMain.ForkError): String = {
|
||||
val sb = new StringBuilder
|
||||
sb.append(fe.getOriginalName)
|
||||
val msg = fe.getOriginalMessage
|
||||
if (msg != null) sb.append(": ").append(msg)
|
||||
sb.append('\n')
|
||||
for (elem <- fe.getStackTrace)
|
||||
sb.append("\tat ").append(elem).append('\n')
|
||||
val cause = fe.getCause
|
||||
if (cause != null) {
|
||||
cause match {
|
||||
case feCause: ForkTestMain.ForkError =>
|
||||
sb.append("Caused by: ").append(formatForkErrorTrace(feCause))
|
||||
case other =>
|
||||
val stringWriter = new StringWriter()
|
||||
val writer = new PrintWriter(stringWriter)
|
||||
other.printStackTrace(writer)
|
||||
writer.flush()
|
||||
sb.append("Caused by: ").append(stringWriter.toString)
|
||||
}
|
||||
}
|
||||
sb.toString
|
||||
}
|
||||
|
||||
// Here we normalize the name to ensure that it's a nicer filename, rather than
|
||||
// contort the user into not using spaces.
|
||||
private def normalizeName(s: String) = s.replaceAll("""\s+""", "-")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import testing.{ Event as TEvent, OptionalThrowable, Status as TStatus, TestSele
|
|||
import util.{ AbstractLogger, Level, ControlEvent, LogEvent }
|
||||
import sbt.io.IO
|
||||
import sbt.protocol.testing.TestResult
|
||||
import sbt.internal.worker1.ForkTestMain
|
||||
import verify.BasicTestSuite
|
||||
import scala.xml.XML
|
||||
|
||||
object JUnitXmlTestsListenerSpec extends BasicTestSuite:
|
||||
|
||||
|
|
@ -88,4 +90,61 @@ object JUnitXmlTestsListenerSpec extends BasicTestSuite:
|
|||
val xmlFile = new File(tempDir, "TEST-TestSuite.xml")
|
||||
assert(xmlFile.exists(), "XML file should be created even when logger is null")
|
||||
|
||||
test("JUnit XML report should use original exception type for forked test failures"):
|
||||
val tempDir = File.createTempFile("junit-test", "")
|
||||
tempDir.delete()
|
||||
tempDir.mkdirs()
|
||||
try
|
||||
val listener = new JUnitXmlTestsListener(tempDir, false, null)
|
||||
listener.doInit()
|
||||
listener.startGroup("TestSuite")
|
||||
|
||||
// Simulate a forked test failure: the original NullPointerException gets
|
||||
// wrapped in ForkError during serialization across the forked JVM boundary.
|
||||
val originalException = new NullPointerException("something was null")
|
||||
originalException.setStackTrace(
|
||||
Array(new StackTraceElement("com.example.MyTest", "testFoo", "MyTest.java", 42))
|
||||
)
|
||||
val forkError = new ForkTestMain.ForkError(originalException)
|
||||
|
||||
val testEvent = new TEvent:
|
||||
def fullyQualifiedName = "TestSuite.testFoo"
|
||||
def duration() = 100L
|
||||
def status = TStatus.Failure
|
||||
def fingerprint = null
|
||||
def selector = new TestSelector("testFoo")
|
||||
def throwable = new OptionalThrowable(forkError)
|
||||
|
||||
listener.testEvent(sbt.TestEvent(Seq(testEvent)))
|
||||
listener.endGroup("TestSuite", TestResult.Failed)
|
||||
|
||||
val xmlFile = new File(tempDir, "TEST-TestSuite.xml")
|
||||
assert(xmlFile.exists(), "XML file should be created")
|
||||
val xml = XML.loadFile(xmlFile)
|
||||
val failureNodes = xml \\ "failure"
|
||||
assert(failureNodes.nonEmpty, "Should have a failure element")
|
||||
val failureType = (failureNodes.head \ "@type").text
|
||||
val failureMessage = (failureNodes.head \ "@message").text
|
||||
assert(
|
||||
failureType == "java.lang.NullPointerException",
|
||||
s"Expected type 'java.lang.NullPointerException' but got '$failureType'"
|
||||
)
|
||||
assert(
|
||||
failureMessage == "something was null",
|
||||
s"Expected message 'something was null' but got '$failureMessage'"
|
||||
)
|
||||
val traceText = failureNodes.head.text
|
||||
assert(
|
||||
traceText.contains("java.lang.NullPointerException"),
|
||||
s"Stacktrace should contain original exception name, got: $traceText"
|
||||
)
|
||||
assert(
|
||||
!traceText.contains("ForkError"),
|
||||
s"Stacktrace should not contain ForkError, got: $traceText"
|
||||
)
|
||||
finally
|
||||
if tempDir.exists() then
|
||||
tempDir.listFiles().foreach(_.delete())
|
||||
tempDir.delete()
|
||||
|
||||
end JUnitXmlTestsListenerSpec
|
||||
|
|
|
|||
|
|
@ -158,6 +158,16 @@ public class ForkTestMain {
|
|||
return originalName + ": " + originalMessage;
|
||||
}
|
||||
|
||||
/** Returns the fully qualified class name of the original exception. */
|
||||
public String getOriginalName() {
|
||||
return originalName;
|
||||
}
|
||||
|
||||
/** Returns the original exception message (without the class name prefix). */
|
||||
public String getOriginalMessage() {
|
||||
return originalMessage;
|
||||
}
|
||||
|
||||
public Exception getCause() {
|
||||
return cause1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue