org.scalatest.tools.HtmlReporter.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2001-2013 Artima, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.scalatest.tools
import org.scalatest._
import org.scalatest.events._
import HtmlReporter._
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.io.StringWriter
import java.net.URL
import java.nio.channels.Channels
import java.text.DecimalFormat
import java.util.Iterator
import java.util.Set
import java.util.UUID
import org.scalatest.exceptions.StackDepth
import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
import scala.io.Source
import scala.xml.Node
import scala.xml.NodeBuffer
import scala.xml.NodeSeq
import scala.xml.XML
import PrintReporter.BufferSize
import StringReporter.makeDurationString
import Suite.unparsedXml
import Suite.xmlContent
import org.scalatest.exceptions.TestFailedException
import com.vladsch.flexmark.parser.PegdownExtensions
import com.vladsch.flexmark.profile.pegdown.PegdownOptionsAdapter
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.html.HtmlRenderer
/**
* A Reporter
that prints test status information in HTML format to a file.
*/
private[scalatest] class HtmlReporter(
directoryPath: String,
presentAllDurations: Boolean,
cssUrl: Option[URL],
resultHolder: Option[SuiteResultHolder]
) extends ResourcefulReporter {
private val specIndent = 15
private val targetDir = new File(directoryPath)
private val imagesDir = new File(targetDir, "images")
private val jsDir = new File(targetDir, "js")
private val cssDir = new File(targetDir, "css")
if (!targetDir.exists)
targetDir.mkdirs()
if (!imagesDir.exists)
imagesDir.mkdirs()
if (!jsDir.exists)
jsDir.mkdirs()
if (!cssDir.exists)
cssDir.mkdirs()
private def copyResource(url: URL, toDir: File, targetFileName: String): Unit = {
val inputStream = url.openStream
try {
val outputStream = new FileOutputStream(new File(toDir, targetFileName))
try {
outputStream.getChannel().transferFrom(Channels.newChannel(inputStream), 0, Long.MaxValue)
}
finally {
outputStream.flush()
outputStream.close()
}
}
finally {
inputStream.close()
}
}
private def getResource(resourceName: String): URL =
classOf[Suite].getClassLoader.getResource(resourceName)
cssUrl.foreach(copyResource(_, cssDir, "custom.css"))
copyResource(getResource("org/scalatest/HtmlReporter.css"), cssDir, "styles.css")
copyResource(getResource("org/scalatest/sorttable.js"), jsDir, "sorttable.js")
copyResource(getResource("org/scalatest/d3.v2.min.js"), jsDir, "d3.v2.min.js")
copyResource(getResource("images/greenbullet.gif"), imagesDir, "testsucceeded.gif")
copyResource(getResource("images/redbullet.gif"), imagesDir, "testfailed.gif")
copyResource(getResource("images/yellowbullet.gif"), imagesDir, "testignored.gif")
copyResource(getResource("images/yellowbullet.gif"), imagesDir, "testcanceled.gif")
copyResource(getResource("images/yellowbullet.gif"), imagesDir, "testpending.gif")
copyResource(getResource("images/graybullet.gif"), imagesDir, "infoprovided.gif")
private val results = resultHolder.getOrElse(new SuiteResultHolder)
try {
Class.forName("com.vladsch.flexmark.profile.pegdown.PegdownOptionsAdapter")
}
catch {
case _: ClassNotFoundException =>
throw new ClassNotFoundException(Resources.flexmarkClassNotFound)
}
private val pegdownOptions = PegdownOptionsAdapter.flexmarkOptions(PegdownExtensions.ALL)
private val markdownParser = Parser.builder(pegdownOptions).build()
private val htmlRenderer = HtmlRenderer.builder(pegdownOptions).build()
private def markdownToHtml(s: String): String = htmlRenderer.render(markdownParser.parse(s))
private def withPossibleLineNumber(stringToPrint: String, throwable: Option[Throwable]): String = {
throwable match {
case Some(testFailedException: TestFailedException) =>
testFailedException.failedCodeFileNameAndLineNumberString match {
case Some(lineNumberString) =>
Resources.printedReportPlusLineNumber(stringToPrint, lineNumberString)
case None => stringToPrint
}
case _ => stringToPrint
}
}
private def stringsToPrintOnError(noteMessageFun: => String, errorMessageFun: Any => String, message: String, throwable: Option[Throwable],
formatter: Option[Formatter], suiteName: Option[String], testName: Option[String], duration: Option[Long]): String = {
formatter match {
case Some(IndentedText(_, rawText, _)) =>
Resources.specTextAndNote(rawText, noteMessageFun)
case _ =>
// Deny MotionToSuppress directives in error events, because error info needs to be seen by users
suiteName match {
case Some(sn) =>
testName match {
case Some(tn) => errorMessageFun(sn + ": " + tn)
case None => errorMessageFun(sn)
}
// Should not get here with built-in ScalaTest stuff, but custom stuff could get here.
case None => errorMessageFun(Resources.noNameSpecified)
}
}
}
private def stringToPrintWhenNoError(messageFun: Any => String, formatter: Option[Formatter], suiteName: String, testName: Option[String]): Option[String] =
stringToPrintWhenNoError(messageFun, formatter, suiteName, testName, None)
private def stringToPrintWhenNoError(messageFun: Any => String, formatter: Option[Formatter], suiteName: String, testName: Option[String], duration: Option[Long]): Option[String] = {
formatter match {
case Some(IndentedText(_, rawText, _)) =>
duration match {
case Some(milliseconds) =>
if (presentAllDurations)
Some(Resources.withDuration(rawText, makeDurationString(milliseconds)))
else
Some(rawText)
case None => Some(rawText)
}
case Some(MotionToSuppress) => None
case _ =>
val arg =
testName match {
case Some(tn) => suiteName + ": " + tn
case None => suiteName
}
val unformattedText = messageFun(arg)
duration match {
case Some(milliseconds) =>
if (presentAllDurations)
Some(Resources.withDuration(unformattedText, makeDurationString(milliseconds)))
else
Some(unformattedText)
case None => Some(unformattedText)
}
}
}
private def getIndentLevel(formatter: Option[Formatter]) =
formatter match {
case Some(IndentedText(formattedText, rawText, indentationLevel)) => indentationLevel
case _ => 0
}
private def getSuiteFileName(suiteResult: SuiteResult) =
suiteResult.suiteClassName.getOrElse(suiteResult.suiteName)
private def makeSuiteFile(suiteResult: SuiteResult): Unit = {
val name = getSuiteFileName(suiteResult)
val pw = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(new File(targetDir, name + ".html")), BufferSize), "UTF-8"))
try {
pw.println {
"" + "\n" +
"" + "\n" +
getSuiteHtml(name, suiteResult)
}
}
finally {
pw.flush()
pw.close()
}
}
private def appendCombinedStatus(name: String, r: SuiteResult) =
if (r.testsFailedCount > 0)
name + "_with_failed"
else if (r.testsIgnoredCount > 0 || r.testsPendingCount > 0 || r.testsCanceledCount > 0)
name + "_passed"
else
name + "_passed_all"
private def transformStringForResult(s: String, suiteResult: SuiteResult): String =
s + (if (suiteResult.testsFailedCount > 0) "_failed" else "_passed")
private def getSuiteHtml(name: String, suiteResult: SuiteResult) =
ScalaTest Suite { name } Results
{
cssUrl match {
case Some(cssUrl) =>
case None => NodeSeq.Empty
}
}
{ suiteResult.suiteName }
{ "Tests: total " + (suiteResult.testsSucceededCount + suiteResult.testsFailedCount + suiteResult.testsCanceledCount + suiteResult.testsIgnoredCount + suiteResult.testsPendingCount) + ", succeeded " +
suiteResult.testsSucceededCount + ", failed " + suiteResult.testsFailedCount + ", canceled " + suiteResult.testsCanceledCount + ", ignored " + suiteResult.testsIgnoredCount + ", pending " +
suiteResult.testsPendingCount }
{
val scopeStack = new collection.mutable.Stack[String]()
suiteResult.eventList.map { e =>
e match {
case ScopeOpened(ordinal, message, nameInfo, formatter, location, payload, threadName, timeStamp) =>
val testNameInfo = nameInfo.testName
val stringToPrint = stringToPrintWhenNoError(Resources.scopeOpened _, formatter, nameInfo.suiteName, nameInfo.testName)
stringToPrint match {
case Some(string) =>
val elementId = generateElementId
scopeStack.push(elementId)
scope(elementId, string, getIndentLevel(formatter) + 1)
case None =>
NodeSeq.Empty
}
case ScopeClosed(ordinal, message, nameInfo, formatter, location, payload, threadName, timeStamp) =>
scopeStack.pop
NodeSeq.Empty
case ScopePending(ordinal, message, nameInfo, formatter, location, payload, threadName, timeStamp) =>
val testNameInfo = nameInfo.testName
val stringToPrint = stringToPrintWhenNoError(Resources.scopePending _, formatter, nameInfo.suiteName, nameInfo.testName)
stringToPrint match {
case Some(string) =>
val elementId = generateElementId
scope(elementId, string, getIndentLevel(formatter) + 1)
case None =>
NodeSeq.Empty
}
case TestSucceeded(ordinal, suiteName, suiteId, suiteClassName, testName, testText, recordedEvents, duration, formatter, location, rerunnable, payload, threadName, timeStamp) =>
val stringToPrint = stringToPrintWhenNoError(Resources.testSucceeded _, formatter, suiteName, Some(testName), duration)
val nodeSeq =
stringToPrint match {
case Some(string) =>
val elementId = generateElementId
test(elementId, List(string), getIndentLevel(formatter) + 1, "test_passed")
case None =>
NodeSeq.Empty
}
nodeSeq :: recordedEvents.map(processInfoMarkupProvided(_, "test_passed")).toList
case TestFailed(ordinal, message, suiteName, suiteId, suiteClassName, testName, testText, recordedEvents, analysis, throwable, duration, formatter, location, rerunnable, payload, threadName, timeStamp) =>
val stringToPrint = stringsToPrintOnError(Resources.failedNote, Resources.testFailed _, message, throwable, formatter, Some(suiteName), Some(testName), duration)
val elementId = generateElementId
val nodeSeq = testWithDetails(elementId, List(stringToPrint), message, throwable, getIndentLevel(formatter) + 1, "test_failed")
nodeSeq :: recordedEvents.map(processInfoMarkupProvided(_, "test_failed")).toList
case TestIgnored(ordinal, suiteName, suiteId, suiteClassName, testName, testText, formatter, location, payload, threadName, timeStamp) =>
val stringToPrint =
formatter match {
case Some(IndentedText(_, rawText, _)) => Some(Resources.specTextAndNote(rawText, Resources.ignoredNote))
case Some(MotionToSuppress) => None
case _ => Some(Resources.testIgnored(suiteName + ": " + testName))
}
stringToPrint match {
case Some(string) =>
val elementId = generateElementId
test(elementId, List(string), getIndentLevel(formatter) + 1, "test_ignored")
case None =>
NodeSeq.Empty
}
case TestPending(ordinal, suiteName, suiteId, suiteClassName, testName, testText, recordedEvents, duration, formatter, location, payload, threadName, timeStamp) =>
val stringToPrint =
formatter match {
case Some(IndentedText(_, rawText, _)) => Some(Resources.specTextAndNote(rawText, Resources.pendingNote))
case Some(MotionToSuppress) => None
case _ => Some(Resources.testPending(suiteName + ": " + testName))
}
val nodeSeq =
stringToPrint match {
case Some(string) =>
val elementId = generateElementId
test(elementId, List(string), getIndentLevel(formatter) + 1, "test_pending")
case None =>
NodeSeq.Empty
}
nodeSeq :: recordedEvents.map(processInfoMarkupProvided(_, "test_pending")).toList
case TestCanceled(ordinal, message, suiteName, suiteId, suiteClassName, testName, testText, recordedEvents, throwable, duration, formatter, location, rerunner, payload, threadName, timeStamp) =>
val stringToPrint = stringsToPrintOnError(Resources.canceledNote, Resources.testCanceled _, message, throwable, formatter, Some(suiteName), Some(testName), duration)
val elementId = generateElementId
val nodeSeq = testWithDetails(elementId, List(stringToPrint), message, throwable, getIndentLevel(formatter) + 1, "test_canceled")
nodeSeq :: recordedEvents.map(processInfoMarkupProvided(_, "test_canceled")).toList
case infoProvided: InfoProvided =>
processInfoMarkupProvided(infoProvided, "info")
case markupProvided: MarkupProvided =>
processInfoMarkupProvided(markupProvided, "markup")
// TO CONTINUE: XML element must be last
// Allow AlertProvided and NoteProvided to use this case, because we don't want that showing up in the HTML report.
case _ => NodeSeq.Empty
}
}
}
Suite ID
{ suiteResult.suiteId }
Class name
{ suiteResult.suiteClassName.getOrElse("-") }
Total duration
{
suiteResult.duration match {
case Some(duration) => makeDurationString(duration)
case None => "-"
}
}
private def processInfoMarkupProvided(event: Event, theClass: String) = {
event match {
case InfoProvided(ordinal, message, nameInfo, throwable, formatter, location, payload, threadName, timeStamp) =>
val (suiteName, testName) =
nameInfo match {
case Some(NameInfo(suiteName, _, _, testName)) => (Some(suiteName), testName)
case None => (None, None)
}
val infoContent = stringsToPrintOnError(Resources.infoProvidedNote, Resources.infoProvided _, message, throwable, formatter, suiteName, testName, None)
val elementId = generateElementId
test(elementId, List(infoContent), getIndentLevel(formatter) + 1, theClass)
case MarkupProvided(ordinal, text, nameInfo, formatter, location, payload, threadName, timeStamp) =>
val (suiteName, testName) =
nameInfo match {
case Some(NameInfo(suiteName, _, _, testName)) => (Some(suiteName), testName)
case None => (None, None)
}
val elementId = generateElementId
markup(elementId, text, getIndentLevel(formatter) + 1, theClass)
case _ => NodeSeq.Empty
}
}
private def makeIndexFile(completeMessageFun: => String, completeInMessageFun: String => String, duration: Option[Long]): Unit = {
val pw = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(new File(targetDir, "index.html")), BufferSize), "UTF-8"))
try {
pw.println {
"\n" +
"\n" +
getIndexHtml(completeMessageFun, completeInMessageFun, duration)
}
}
finally {
pw.flush()
pw.close()
}
}
private def getHeaderStatusColor(summary: Summary) =
if (summary.testsFailedCount == 0) "scalatest-header-passed" else "scalatest-header-failed"
private def getPieChartScript(summary: Summary) = {
import summary._
"/* modified from http://www.permadi.com/tutorial/cssGettingBackgroundColor/index.html - */" + "\n" +
"function getBgColor(elementId)" + "\n" +
"{" + "\n" +
" var element = document.getElementById(elementId);" + "\n" +
" if (element.currentStyle)" + "\n" +
" return element.currentStyle.backgroundColor;" + "\n" +
" if (window.getComputedStyle)" + "\n" +
" {" + "\n" +
" var elementStyle=window.getComputedStyle(element,\"\");" + "\n" +
" if (elementStyle)" + "\n" +
" return elementStyle.getPropertyValue(\"background-color\");" + "\n" +
" }" + "\n" +
" // Return 0 if both methods failed." + "\n" +
" return 0;" + "\n" +
"}" + "\n" +
"var data = [" + testsSucceededCount + ", " + testsFailedCount + ", " + testsIgnoredCount + ", " + testsPendingCount + ", " + testsCanceledCount + "];" + "\n" +
"var color = [getBgColor('summary_view_row_1_legend_succeeded_label'), " + "\n" +
" getBgColor('summary_view_row_1_legend_failed_label'), " + "\n" +
" getBgColor('summary_view_row_1_legend_ignored_label'), " + "\n" +
" getBgColor('summary_view_row_1_legend_pending_label'), " + "\n" +
" getBgColor('summary_view_row_1_legend_canceled_label')" + "\n" +
" ];" + "\n" +
"var width = document.getElementById('chart_div').offsetWidth," + "\n" +
" height = document.getElementById('chart_div').offsetHeight," + "\n" +
" outerRadius = Math.min(width, height) / 2," + "\n" +
" innerRadius = 0," + "\n" +
" donut = d3.layout.pie()," + "\n" +
" arc = d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius);" + "\n" +
"var vis = d3.select(\"#chart_div\")" + "\n" +
" .append(\"svg\")" + "\n" +
" .data([data])" + "\n" +
" .attr(\"width\", width)" + "\n" +
" .attr(\"height\", height);" + "\n" +
"var arcs = vis.selectAll(\"g.arc\")" + "\n" +
" .data(donut)" + "\n" +
" .enter().append(\"g\")" + "\n" +
" .attr(\"class\", \"arc\")" + "\n" +
" .attr(\"transform\", \"translate(\" + outerRadius + \",\" + outerRadius + \")\");" + "\n" +
"arcs.append(\"path\")" + "\n" +
" .attr(\"fill\", function(d, i) { return color[i]; })" + "\n" +
" .attr(\"d\", arc);\n"
}
private def getIndexHtml(completeMessageFun: => String, completeInMessageFun: String => String, duration: Option[Long]) = {
val summary = results.summary
import summary._
val decimalFormat = new DecimalFormat("#.##")
ScalaTest Results
{
cssUrl match {
case Some(cssUrl) =>
case None => NodeSeq.Empty
}
}
{ header(completeMessageFun, completeInMessageFun, duration, summary) }
Succeeded
{ testsSucceededCount }
({ decimalFormat.format(testsSucceededCount * 100.0 / totalTestsCount) }%)
Failed
{ testsFailedCount }
({ decimalFormat.format(testsFailedCount * 100.0 / totalTestsCount) }%)
Canceled
{ testsCanceledCount }
({ decimalFormat.format(testsCanceledCount * 100.0 / totalTestsCount) }%)
Ignored
{ testsIgnoredCount }
({ decimalFormat.format(testsIgnoredCount * 100.0 / totalTestsCount) }%)
Pending
{ testsPendingCount }
({ decimalFormat.format(testsPendingCount * 100.0 / totalTestsCount) }%)
{ getStatistic(summary) }
{ suiteResults }
Click on suite name to view details.
Click on column name to sort.
}
// TODO: This needs to be internationalized
private def getStatistic(summary: Summary) =
private def header(completeMessageFun: => String, completeInMessageFun: String => String, duration: Option[Long], summary: Summary) =
ScalaTest Results
{ getDuration(completeMessageFun, completeInMessageFun, duration) }
{ getTotalTests(summary) }
{ getSuiteSummary(summary) }
{ getTestSummary(summary) }
private def generateElementId = UUID.randomUUID.toString
private def setBit(stack: collection.mutable.Stack[String], tagMap: collection.mutable.HashMap[String, Int], bit: Int): Unit = {
stack.foreach { scopeElementId =>
val currentBits = tagMap(scopeElementId)
tagMap.put(scopeElementId, currentBits | bit)
}
}
val tagMap = collection.mutable.HashMap[String, Int]()
private def suiteResults =
Suite
Duration (ms.)
Succeeded
Failed
Canceled
Ignored
Pending
Total
{
val sortedSuiteList = results.suiteList.sortWith { (a, b) =>
if (a.testsFailedCount == b.testsFailedCount) {
if (a.testsCanceledCount == b.testsCanceledCount) {
if (a.testsIgnoredCount == b.testsIgnoredCount) {
if (a.testsPendingCount == b.testsPendingCount)
a.startEvent.suiteName < b.startEvent.suiteName
else
a.testsPendingCount > b.testsPendingCount
}
else
a.testsIgnoredCount > b.testsIgnoredCount
}
else
a.testsCanceledCount > b.testsCanceledCount
}
else
a.testsFailedCount > b.testsFailedCount
}.toArray
sortedSuiteList map { r =>
val elementId = generateElementId
import r._
val suiteAborted = endEvent.isInstanceOf[SuiteAborted]
val totalTestsCount =
testsSucceededCount + testsFailedCount + testsIgnoredCount +
testsPendingCount + testsCanceledCount
val bits =
(if ((testsSucceededCount > 0) ||
((totalTestsCount == 0) && !suiteAborted))
SUCCEEDED_BIT else 0) +
(if ((testsFailedCount > 0) || (suiteAborted)) FAILED_BIT else 0) +
(if (testsIgnoredCount > 0) IGNORED_BIT else 0) +
(if (testsPendingCount > 0) PENDING_BIT else 0) +
(if (testsCanceledCount > 0) CANCELED_BIT else 0)
tagMap.put(elementId, bits)
suiteSummary(elementId, getSuiteFileName(r), r)
}
}
private def countStyle(prefix: String, count: Int) =
if (count == 0)
prefix + "_zero"
else
prefix
private def durationDisplay(duration: Option[Long]) =
duration.getOrElse("-")
private def suiteSummary(elementId: String, suiteFileName: String, suiteResult: SuiteResult) = {
import suiteResult._
{ suiteName }
{ durationDisplay(duration) }
{ testsSucceededCount }
{ testsFailedCount }
{ testsCanceledCount }
{ testsIgnoredCount }
{ testsPendingCount }
{ testsSucceededCount + testsFailedCount + testsIgnoredCount + testsPendingCount + testsCanceledCount }
}
private def twoLess(indentLevel: Int): Int =
indentLevel - 2 match {
case lev if lev < 0 => 0
case lev => lev
}
private def oneLess(indentLevel: Int): Int =
indentLevel - 1 match {
case lev if lev < 0 => 0
case lev => lev
}
private def scope(elementId: String, message: String, indentLevel: Int) =
{ message }
private def test(elementId: String, lines: List[String], indentLevel: Int, styleName: String) =
{
lines.map { line =>
- { line }
}
}
private def testWithDetails(elementId: String, lines: List[String], message: String, throwable: Option[Throwable], indentLevel: Int, styleName: String) = {
def getHTMLForStackTrace(stackTraceList: List[StackTraceElement]) =
stackTraceList.map((ste: StackTraceElement) => { ste.toString })
def displayErrorMessage(errorMessage: String) = {
// scala automatically change
to
, which will cause 2 line breaks, use unparsedXml("
") to solve it.
val messageLines = errorMessage.split("\n")
if (messageLines.size > 1)
messageLines.map(line => { xmlContent(line) }{ unparsedXml("
") })
else
{ message }
}
def getHTMLForCause(throwable: Throwable): NodeBuffer = {
val cause = throwable.getCause
if (cause != null) {
{ Resources.DetailsCause + ":" }
{ cause.getClass.getName }
{ Resources.DetailsMessage + ":" }
{
if (cause.getMessage != null)
displayErrorMessage(cause.getMessage)
else
{ Resources.None }
}
{ getHTMLForStackTrace(cause.getStackTrace.toList) }
&+ getHTMLForCause(cause)
}
else new scala.xml.NodeBuffer
}
val (grayStackTraceElements, blackStackTraceElements) =
throwable match {
case Some(throwable) =>
val stackTraceElements = throwable.getStackTrace.toList
throwable match {
case sde: exceptions.StackDepthException =>
(stackTraceElements.take(sde.failedCodeStackDepth), stackTraceElements.drop(sde.failedCodeStackDepth))
case _ => (List(), stackTraceElements)
}
case None => (List(), List())
}
val throwableTitle = throwable.map(_.getClass.getName)
val fileAndLineOption: Option[String] =
throwable match {
case Some(throwable) =>
throwable match {
case stackDepth: StackDepth =>
stackDepth.failedCodeFileNameAndLineNumberString
case _ => None
}
case None => None
}
val linkId = UUID.randomUUID.toString
val contentId = UUID.randomUUID.toString
{
lines.map { line =>
- { line }
}
}
}
// Chee Seng, I changed oneLess to twoLess here, because markup should indent the same as info.
// I added the call to convertSingleParaToDefinition, so simple one-liner markup looks the same as an info.
// TODO: probably actually show the exception in the HTML report rather than blowing up the reporter, because that means
// the whole suite doesn't get recorded. May want to do this more generally though.
private def markup(elementId: String, text: String, indentLevel: Int, styleName: String) = {
val htmlString = convertAmpersand(convertSingleParaToDefinition(markdownToHtml(text)))
{
try XML.loadString(htmlString)
catch {
// I really want to catch a org.xml.sax.SAXParseException, but don't want to depend on the implementation of XML.loadString,
// which may change. If the exception isn't exactly actually a the parse exception, then retrying will likely fail with
// the same exception.
case e: Exception =>
XML.loadString("" + htmlString + "")
}
}
}
private def tagMapScript =
"tagMap = { \n" +
tagMap.map { case (elementId, bitSet) => "\"" + elementId + "\": " + bitSet }.mkString(", \n") +
"};\n" +
"applyFilter();"
private var eventList = new ListBuffer[Event]()
private var runEndEvent: Option[Event] = None
def apply(event: Event): Unit = {
event match {
case _: DiscoveryStarting =>
case _: DiscoveryCompleted =>
case RunStarting(ordinal, testCount, configMap, formatter, location, payload, threadName, timeStamp) =>
case RunCompleted(ordinal, duration, summary, formatter, location, payload, threadName, timeStamp) =>
runEndEvent = Some(event)
case RunStopped(ordinal, duration, summary, formatter, location, payload, threadName, timeStamp) =>
runEndEvent = Some(event)
case RunAborted(ordinal, message, throwable, duration, summary, formatter, location, payload, threadName, timeStamp) =>
runEndEvent = Some(event)
case SuiteCompleted(ordinal, suiteName, suiteId, suiteClassName, duration, formatter, location, rerunner, payload, threadName, timeStamp) =>
val (suiteEvents, otherEvents) = extractSuiteEvents(suiteId)
eventList = otherEvents
val sortedSuiteEvents = suiteEvents.sorted
if (sortedSuiteEvents.isEmpty)
throw new IllegalStateException("Expected SuiteStarting for completion event: " + event + " in the head of suite events, but we got no suite event at all")
sortedSuiteEvents.head match {
case suiteStarting: SuiteStarting =>
val suiteResult = sortedSuiteEvents.foldLeft(SuiteResult(suiteId, suiteName, suiteClassName, duration, suiteStarting, event, Vector.empty ++ sortedSuiteEvents.tail, 0, 0, 0, 0, 0, 0, true)) { case (r, e) =>
e match {
case testSucceeded: TestSucceeded => r.copy(testsSucceededCount = r.testsSucceededCount + 1)
case testFailed: TestFailed => r.copy(testsFailedCount = r.testsFailedCount + 1)
case testIgnored: TestIgnored => r.copy(testsIgnoredCount = r.testsIgnoredCount + 1)
case testPending: TestPending => r.copy(testsPendingCount = r.testsPendingCount + 1)
case testCanceled: TestCanceled => r.copy(testsCanceledCount = r.testsCanceledCount + 1)
case scopePending: ScopePending => r.copy(scopesPendingCount = r.scopesPendingCount + 1)
case _ => r
}
}
val suiteStartingEvent = sortedSuiteEvents.head.asInstanceOf[SuiteStarting]
if (suiteStartingEvent.formatter != Some(MotionToSuppress)) {
results += suiteResult
makeSuiteFile(suiteResult)
}
case other =>
throw new IllegalStateException("Expected SuiteStarting for completion event: " + event + " in the head of suite events, but we got: " + other)
}
case SuiteAborted(ordinal, message, suiteName, suiteId, suiteClassName, throwable, duration, formatter, location, rerunner, payload, threadName, timeStamp) =>
val (suiteEvents, otherEvents) = extractSuiteEvents(suiteId)
eventList = otherEvents
val sortedSuiteEvents = suiteEvents.sorted
if (sortedSuiteEvents.isEmpty)
throw new IllegalStateException("Expected SuiteStarting for completion event: " + event + " in the head of suite events, but we got no suite event at all")
sortedSuiteEvents.head match {
case suiteStarting: SuiteStarting =>
val suiteResult = sortedSuiteEvents.foldLeft(SuiteResult(suiteId, suiteName, suiteClassName, duration, suiteStarting, event, Vector.empty ++ sortedSuiteEvents.tail, 0, 0, 0, 0, 0, 0, false)) { case (r, e) =>
e match {
case testSucceeded: TestSucceeded => r.copy(testsSucceededCount = r.testsSucceededCount + 1)
case testFailed: TestFailed => r.copy(testsFailedCount = r.testsFailedCount + 1)
case testIgnored: TestIgnored => r.copy(testsIgnoredCount = r.testsIgnoredCount + 1)
case testPending: TestPending => r.copy(testsPendingCount = r.testsPendingCount + 1)
case testCanceled: TestCanceled => r.copy(testsCanceledCount = r.testsCanceledCount + 1)
case scopePending: ScopePending => r.copy(scopesPendingCount = r.scopesPendingCount + 1)
case _ => r
}
}
results += suiteResult
makeSuiteFile(suiteResult)
case other =>
throw new IllegalStateException("Expected SuiteStarting for completion event: " + event + " in the head of suite events, but we got: " + other)
}
case _ => eventList += event
}
}
def extractSuiteEvents(suiteId: String) = eventList partition { e =>
e match {
case e: TestStarting => e.suiteId == suiteId
case e: TestSucceeded => e.suiteId == suiteId
case e: TestIgnored => e.suiteId == suiteId
case e: TestFailed => e.suiteId == suiteId
case e: TestPending => e.suiteId == suiteId
case e: TestCanceled => e.suiteId == suiteId
case e: InfoProvided => e.nameInfo.exists(_.suiteId == suiteId)
case e: AlertProvided => e.nameInfo.exists(_.suiteId == suiteId)
case e: NoteProvided => e.nameInfo.exists(_.suiteId == suiteId)
case e: MarkupProvided => e.nameInfo.exists(_.suiteId == suiteId)
case e: ScopeOpened => e.nameInfo.suiteId == suiteId
case e: ScopeClosed => e.nameInfo.suiteId == suiteId
case e: ScopePending => e.nameInfo.suiteId == suiteId
case e: SuiteStarting => e.suiteId == suiteId
case _ => false
}
}
def dispose(): Unit = {
runEndEvent match {
case Some(event) =>
event match {
case RunCompleted(ordinal, duration, summary, formatter, location, payload, threadName, timeStamp) =>
makeIndexFile(Resources.runCompleted, Resources.runCompletedIn _, duration)
case RunStopped(ordinal, duration, summary, formatter, location, payload, threadName, timeStamp) =>
makeIndexFile(Resources.runStopped, Resources.runStoppedIn _, duration)
case RunAborted(ordinal, message, throwable, duration, summary, formatter, location, payload, threadName, timeStamp) =>
makeIndexFile(Resources.runAborted, Resources.runAbortedIn _, duration)
case other =>
throw new IllegalStateException("Expected run ending event only, but got: " + other.getClass.getName)
}
case None => // If no run end event (e.g. when run in sbt), just use runCompleted with sum of suites' duration.
makeIndexFile(Resources.runCompleted, Resources.runCompletedIn _, Some(results.totalDuration))
}
}
private def getDuration(completeMessageFun: => String, completeInMessageFun: String => String, duration: Option[Long]) = {
duration match {
case Some(msSinceEpoch) =>
completeInMessageFun(makeDurationString(msSinceEpoch))
case None =>
completeMessageFun
}
}
private def getTotalTests(summary: Summary) =
Resources.totalNumberOfTestsRun(summary.testsCompletedCount.toString)
// Suites: completed {0}, aborted {1}
private def getSuiteSummary(summary: Summary) =
if (summary.scopesPendingCount > 0)
Resources.suiteScopeSummary(summary.suitesCompletedCount.toString, summary.suitesAbortedCount.toString, summary.scopesPendingCount.toString)
else
Resources.suiteSummary(summary.suitesCompletedCount.toString, summary.suitesAbortedCount.toString)
// Tests: succeeded {0}, failed {1}, canceled {4}, ignored {2}, pending {3}
private def getTestSummary(summary: Summary) =
Resources.testSummary(summary.testsSucceededCount.toString, summary.testsFailedCount.toString, summary.testsCanceledCount.toString, summary.testsIgnoredCount.toString,
summary.testsPendingCount.toString)
// We subtract one from test reports because we add "- " in front, so if one is actually zero, it will come here as -1
// private def indent(s: String, times: Int) = if (times <= 0) s else (" " * times) + s
// Stupid properties file won't let me put spaces at the beginning of a property
// " {0}" comes out as "{0}", so I can't do indenting in a localizable way. For now
// just indent two space to the left. // if (times <= 0) s
// else Resources.indentOnce(indent(s, times - 1))
}
private[tools] object HtmlReporter {
final val SUCCEEDED_BIT = 1
final val FAILED_BIT = 2
final val IGNORED_BIT = 4
final val PENDING_BIT = 8
final val CANCELED_BIT = 16
def convertSingleParaToDefinition(html: String): String = {
val firstOpenPara = html.indexOf("")
if (firstOpenPara == 0 && html.indexOf("
", 1) == -1 && html.indexOf("
") == html.length - 4)
html.replace("", "
\n- ").replace("", "
\n
")
else html
}
def convertAmpersand(html: String): String =
html.replaceAll("&", "&")
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy