All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalatest.tools.HtmlReporter.scala Maven / Gradle / Ivy

/*
 * 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.profiles.pegdown.Extensions
import com.vladsch.flexmark.profiles.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)

  private val pegdownOptions = PegdownOptionsAdapter.flexmarkOptions(Extensions.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 } } } 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 = { 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) } }
Suite Duration (ms.) Succeeded Failed Canceled Ignored Pending Total
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