mirror of https://github.com/sbt/sbt.git
[2.x] feat: Capture test output into JUnit XML system-out/system-err (fixes #6537)
This commit is contained in:
parent
e0bdb531f7
commit
2bc12083cf
|
|
@ -38,14 +38,26 @@ object JUnitXmlReportPlugin extends AutoPlugin {
|
|||
object autoImport {
|
||||
val testReportsDirectory =
|
||||
settingKey[File]("Directory for outputting junit test reports.").withRank(AMinusSetting)
|
||||
val testReportXmlCaptureStdOut =
|
||||
settingKey[Boolean](
|
||||
"If true, capture test framework log output into <system-out> in JUnit XML reports."
|
||||
).withRank(BSetting)
|
||||
val testReportXmlCaptureStdErr =
|
||||
settingKey[Boolean](
|
||||
"If true, capture test framework error output into <system-err> in JUnit XML reports."
|
||||
).withRank(BSetting)
|
||||
|
||||
lazy val testReportSettings: Seq[Setting[?]] = Seq(
|
||||
testReportsDirectory := target.value / (prefix(configuration.value.name) + "reports"),
|
||||
testReportXmlCaptureStdOut := false,
|
||||
testReportXmlCaptureStdErr := false,
|
||||
testListeners += Def.uncached {
|
||||
JUnitXmlTestsListener(
|
||||
testReportsDirectory.value,
|
||||
SysProp.legacyTestReport,
|
||||
streams.value.log
|
||||
streams.value.log,
|
||||
testReportXmlCaptureStdOut.value,
|
||||
testReportXmlCaptureStdErr.value
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import scala.util.Properties
|
|||
import scala.xml.{ Elem, Node as XNode, XML }
|
||||
import testing.{
|
||||
Event as TEvent,
|
||||
Logger as TLogger,
|
||||
NestedTestSelector,
|
||||
Status as TStatus,
|
||||
OptionalThrowable,
|
||||
|
|
@ -61,15 +62,32 @@ object JUnitXmlTestsListener {
|
|||
* @param targetDir
|
||||
* directory in which test reports are generated
|
||||
*/
|
||||
class JUnitXmlTestsListener(val targetDir: File, legacyTestReport: Boolean, logger: Logger)
|
||||
extends TestsListener {
|
||||
class JUnitXmlTestsListener(
|
||||
val targetDir: File,
|
||||
legacyTestReport: Boolean,
|
||||
logger: Logger,
|
||||
captureStdOut: Boolean,
|
||||
captureStdErr: Boolean
|
||||
) extends TestsListener {
|
||||
// These constructors are for binary compatibility with older versions of sbt
|
||||
// Use old hard-coded behaviour for constructing `targetDir` from `outputDir`
|
||||
def this(
|
||||
outputDir: String,
|
||||
legacyTestReport: Boolean,
|
||||
logger: Logger,
|
||||
captureStdOut: Boolean,
|
||||
captureStdErr: Boolean
|
||||
) =
|
||||
this(new File(outputDir, "test-reports"), legacyTestReport, logger, captureStdOut, captureStdErr)
|
||||
def this(targetDir: File, legacyTestReport: Boolean, logger: Logger) =
|
||||
this(targetDir, legacyTestReport, logger, false, false)
|
||||
def this(outputDir: String, legacyTestReport: Boolean, logger: Logger) =
|
||||
this(new File(outputDir, "test-reports"), legacyTestReport, logger)
|
||||
this(new File(outputDir, "test-reports"), legacyTestReport, logger, false, false)
|
||||
def this(outputDir: String, logger: Logger) = this(outputDir, false, logger)
|
||||
def this(outputDir: String) = this(outputDir, false, null)
|
||||
|
||||
private val captureEnabled: Boolean = captureStdOut || captureStdErr
|
||||
|
||||
/** Current hostname so we know which machine executed the tests */
|
||||
lazy val hostname: String = {
|
||||
val name = JUnitXmlTestsListener.hostname
|
||||
|
|
@ -108,6 +126,8 @@ class JUnitXmlTestsListener(val targetDir: File, legacyTestReport: Boolean, logg
|
|||
def this(name: String) = this(name, LocalDateTime.now())
|
||||
|
||||
val events: ListBuffer[TEvent] = new ListBuffer()
|
||||
val stdOutBuffer: StringBuilder = new StringBuilder()
|
||||
val stdErrBuffer: StringBuilder = new StringBuilder()
|
||||
|
||||
/** Adds one test result to this suite. */
|
||||
def addEvent(e: TEvent): ListBuffer[TEvent] = events += e
|
||||
|
|
@ -188,8 +208,8 @@ class JUnitXmlTestsListener(val targetDir: File, legacyTestReport: Boolean, logg
|
|||
</testcase>
|
||||
|
||||
}
|
||||
<system-out><![CDATA[]]></system-out>
|
||||
<system-err><![CDATA[]]></system-err>
|
||||
<system-out>{scala.xml.PCData(stdOutBuffer.toString)}</system-out>
|
||||
<system-err>{scala.xml.PCData(stdErrBuffer.toString)}</system-err>
|
||||
</testsuite>
|
||||
|
||||
result
|
||||
|
|
@ -289,6 +309,40 @@ class JUnitXmlTestsListener(val targetDir: File, legacyTestReport: Boolean, logg
|
|||
/** 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
|
||||
/**
|
||||
* Returns a ContentLogger that captures test output into the suite's
|
||||
* stdout/stderr buffers when capture is enabled via testReportXmlCaptureStdOut
|
||||
* or testReportXmlCaptureStdErr settings.
|
||||
*/
|
||||
override def contentLogger(test: TestDefinition): Option[ContentLogger] = {
|
||||
if (!captureEnabled) return None
|
||||
testSuite.get().map { suite =>
|
||||
val tLogger = new TLogger {
|
||||
def error(s: String): Unit =
|
||||
if (captureStdErr) suite.stdErrBuffer.synchronized {
|
||||
suite.stdErrBuffer.append(s).append('\n')
|
||||
}
|
||||
def warn(s: String): Unit =
|
||||
if (captureStdErr) suite.stdErrBuffer.synchronized {
|
||||
suite.stdErrBuffer.append(s).append('\n')
|
||||
}
|
||||
def info(s: String): Unit =
|
||||
if (captureStdOut) suite.stdOutBuffer.synchronized {
|
||||
suite.stdOutBuffer.append(s).append('\n')
|
||||
}
|
||||
def debug(s: String): Unit =
|
||||
if (captureStdOut) suite.stdOutBuffer.synchronized {
|
||||
suite.stdOutBuffer.append(s).append('\n')
|
||||
}
|
||||
def trace(t: Throwable): Unit =
|
||||
if (captureStdErr) suite.stdErrBuffer.synchronized {
|
||||
val sw = new StringWriter()
|
||||
t.printStackTrace(new PrintWriter(sw))
|
||||
suite.stdErrBuffer.append(sw.toString)
|
||||
}
|
||||
def ansiCodesSupported(): Boolean = false
|
||||
}
|
||||
new ContentLogger(tLogger, () => ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,4 +102,140 @@ object JUnitXmlTestsListenerSpec extends BasicTestSuite:
|
|||
tempDir.listFiles().foreach(_.delete())
|
||||
tempDir.delete()
|
||||
|
||||
test("JUnitXmlTestsListener should capture stdout and stderr when enabled"):
|
||||
val tempDir = File.createTempFile("junit-test", "")
|
||||
tempDir.delete()
|
||||
tempDir.mkdirs()
|
||||
try
|
||||
val listener = new JUnitXmlTestsListener(tempDir, false, null, true, true)
|
||||
listener.doInit()
|
||||
listener.startGroup("CaptureTestSuite")
|
||||
|
||||
// Get the content logger and write to it
|
||||
val testDef = new TestDefinition(
|
||||
"CaptureTestSuite.testCapture",
|
||||
null,
|
||||
false,
|
||||
Array(new TestSelector("testCapture"))
|
||||
)
|
||||
val contentLoggerOpt = listener.contentLogger(testDef)
|
||||
assert(contentLoggerOpt.isDefined, "contentLogger should return Some when capture is enabled")
|
||||
val cl = contentLoggerOpt.get
|
||||
cl.log.info("hello stdout")
|
||||
cl.log.debug("debug stdout")
|
||||
cl.log.error("hello stderr")
|
||||
cl.log.warn("warn stderr")
|
||||
cl.flush()
|
||||
|
||||
val testEvent = new TEvent:
|
||||
def fullyQualifiedName = "CaptureTestSuite.testCapture"
|
||||
def duration() = 50L
|
||||
def status = TStatus.Success
|
||||
def fingerprint = null
|
||||
def selector = new TestSelector("testCapture")
|
||||
def throwable = new OptionalThrowable()
|
||||
|
||||
listener.testEvent(sbt.TestEvent(Seq(testEvent)))
|
||||
listener.endGroup("CaptureTestSuite", TestResult.Passed)
|
||||
|
||||
// Read and verify the XML
|
||||
val xmlFile = new File(tempDir, "TEST-CaptureTestSuite.xml")
|
||||
assert(xmlFile.exists(), "XML file should be created")
|
||||
val xml = scala.xml.XML.loadFile(xmlFile)
|
||||
val sysOut = (xml \ "system-out").text
|
||||
val sysErr = (xml \ "system-err").text
|
||||
assert(sysOut.contains("hello stdout"), s"system-out should contain 'hello stdout', got: $sysOut")
|
||||
assert(sysOut.contains("debug stdout"), s"system-out should contain 'debug stdout', got: $sysOut")
|
||||
assert(sysErr.contains("hello stderr"), s"system-err should contain 'hello stderr', got: $sysErr")
|
||||
assert(sysErr.contains("warn stderr"), s"system-err should contain 'warn stderr', got: $sysErr")
|
||||
finally
|
||||
if tempDir.exists() then
|
||||
tempDir.listFiles().foreach(_.delete())
|
||||
tempDir.delete()
|
||||
|
||||
test("JUnitXmlTestsListener should have empty system-out/err when capture is disabled"):
|
||||
val tempDir = File.createTempFile("junit-test", "")
|
||||
tempDir.delete()
|
||||
tempDir.mkdirs()
|
||||
try
|
||||
val listener = new JUnitXmlTestsListener(tempDir, false, null, false, false)
|
||||
listener.doInit()
|
||||
listener.startGroup("NoCaptureTestSuite")
|
||||
|
||||
val testDef = new TestDefinition(
|
||||
"NoCaptureTestSuite.testNoCapture",
|
||||
null,
|
||||
false,
|
||||
Array(new TestSelector("testNoCapture"))
|
||||
)
|
||||
val contentLoggerOpt = listener.contentLogger(testDef)
|
||||
assert(contentLoggerOpt.isEmpty, "contentLogger should return None when capture is disabled")
|
||||
|
||||
val testEvent = new TEvent:
|
||||
def fullyQualifiedName = "NoCaptureTestSuite.testNoCapture"
|
||||
def duration() = 50L
|
||||
def status = TStatus.Success
|
||||
def fingerprint = null
|
||||
def selector = new TestSelector("testNoCapture")
|
||||
def throwable = new OptionalThrowable()
|
||||
|
||||
listener.testEvent(sbt.TestEvent(Seq(testEvent)))
|
||||
listener.endGroup("NoCaptureTestSuite", TestResult.Passed)
|
||||
|
||||
val xmlFile = new File(tempDir, "TEST-NoCaptureTestSuite.xml")
|
||||
assert(xmlFile.exists(), "XML file should be created")
|
||||
val xml = scala.xml.XML.loadFile(xmlFile)
|
||||
val sysOut = (xml \ "system-out").text
|
||||
val sysErr = (xml \ "system-err").text
|
||||
assert(sysOut.isEmpty, s"system-out should be empty, got: $sysOut")
|
||||
assert(sysErr.isEmpty, s"system-err should be empty, got: $sysErr")
|
||||
finally
|
||||
if tempDir.exists() then
|
||||
tempDir.listFiles().foreach(_.delete())
|
||||
tempDir.delete()
|
||||
|
||||
test("JUnitXmlTestsListener should capture only stdout when only captureStdOut is enabled"):
|
||||
val tempDir = File.createTempFile("junit-test", "")
|
||||
tempDir.delete()
|
||||
tempDir.mkdirs()
|
||||
try
|
||||
val listener = new JUnitXmlTestsListener(tempDir, false, null, true, false)
|
||||
listener.doInit()
|
||||
listener.startGroup("StdOutOnlySuite")
|
||||
|
||||
val testDef = new TestDefinition(
|
||||
"StdOutOnlySuite.test",
|
||||
null,
|
||||
false,
|
||||
Array(new TestSelector("test"))
|
||||
)
|
||||
val contentLoggerOpt = listener.contentLogger(testDef)
|
||||
assert(contentLoggerOpt.isDefined, "contentLogger should return Some when captureStdOut is enabled")
|
||||
val cl = contentLoggerOpt.get
|
||||
cl.log.info("info message")
|
||||
cl.log.error("error message")
|
||||
cl.flush()
|
||||
|
||||
val testEvent = new TEvent:
|
||||
def fullyQualifiedName = "StdOutOnlySuite.test"
|
||||
def duration() = 50L
|
||||
def status = TStatus.Success
|
||||
def fingerprint = null
|
||||
def selector = new TestSelector("test")
|
||||
def throwable = new OptionalThrowable()
|
||||
|
||||
listener.testEvent(sbt.TestEvent(Seq(testEvent)))
|
||||
listener.endGroup("StdOutOnlySuite", TestResult.Passed)
|
||||
|
||||
val xmlFile = new File(tempDir, "TEST-StdOutOnlySuite.xml")
|
||||
val xml = scala.xml.XML.loadFile(xmlFile)
|
||||
val sysOut = (xml \ "system-out").text
|
||||
val sysErr = (xml \ "system-err").text
|
||||
assert(sysOut.contains("info message"), s"system-out should contain 'info message', got: $sysOut")
|
||||
assert(!sysErr.contains("error message"), s"system-err should be empty when captureStdErr is false, got: $sysErr")
|
||||
finally
|
||||
if tempDir.exists() then
|
||||
tempDir.listFiles().foreach(_.delete())
|
||||
tempDir.delete()
|
||||
|
||||
end JUnitXmlTestsListenerSpec
|
||||
|
|
|
|||
Loading…
Reference in New Issue