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

utest.framework.Formatter.scala Maven / Gradle / Ivy

There is a newer version: 0.7.11
Show newest version
package utest
package framework
//import acyclic.file

import scala.collection.mutable
import scala.util.{Failure, Success}

object Formatter extends Formatter
/**
 * Default implementation of [[Formatter]], also used by the default SBT test
 * framework. Allows some degree of customization of the formatted test results.
 */
trait Formatter {

  def formatColor: Boolean = true
  def formatTruncateHeight: Int = 15
  def formatWrapWidth: Int = Int.MaxValue >> 1 // halving here to avoid overflows later

  def formatValue(x: Any) = testValueColor("" + x)

  def toggledColor(t: ufansi.Attrs) = if(formatColor) t else ufansi.Attrs.Empty
  def testValueColor = toggledColor(ufansi.Color.Blue)
  def exceptionClassColor = toggledColor(ufansi.Underlined.On ++ ufansi.Color.LightRed)
  def exceptionMsgColor = toggledColor(ufansi.Color.LightRed)
  def exceptionPrefixColor = toggledColor(ufansi.Color.Red)
  def exceptionMethodColor = toggledColor(ufansi.Color.LightRed)
  def exceptionPunctuationColor = toggledColor(ufansi.Color.Red)
  def exceptionLineNumberColor = toggledColor(ufansi.Color.LightRed)

  def formatResultColor(success: Boolean) = toggledColor(
    if (success) ufansi.Color.Green
    else ufansi.Color.Red
  )

  def formatMillisColor = toggledColor(ufansi.Bold.Faint)

  def exceptionStackFrameHighlighter(s: StackTraceElement): Boolean = true

  def formatException(x: Throwable, leftIndent: String) = {
    val output = mutable.Buffer.empty[ufansi.Str]
    var current = x
    while(current != null){
      val exCls = exceptionClassColor(current.getClass.getName)
      output.append(
        joinLineStr(
          lineWrapInput(
            current.getMessage match{
              case null => exCls
              case nonNull => ufansi.Str.join(exCls, ": ", exceptionMsgColor(nonNull))
            },
            leftIndent
          ),
          leftIndent
        )
      )

      val stack = current.getStackTrace


      StackMarker.filterCallStack(stack)
        .foreach { e =>
          // Scala.js for some reason likes putting in full-paths into the
          // filename slot, rather than just the last segment of the file-path
          // like Scala-JVM does. This results in that portion of the
          // stacktrace being terribly long, wrapping around and generally
          // being impossible to read. We thus manually drop the earlier
          // portion of the file path and keep only the last segment

          val filenameFrag: ufansi.Str = e.getFileName match{
            case null => exceptionLineNumberColor("Unknown")
            case fileName =>
              val shortenedFilename = fileName.lastIndexOf('/') match{
                case -1 => fileName
                case n => fileName.drop(n + 1)
              }
              ufansi.Str.join(
                exceptionLineNumberColor(shortenedFilename),
                ":",
                exceptionLineNumberColor(e.getLineNumber.toString)
              )
          }

          val frameIndent = leftIndent + "  "
          val wrapper =
            if(exceptionStackFrameHighlighter(e)) ufansi.Attrs.Empty
            else ufansi.Bold.Faint

          output.append(
            "\n", frameIndent,
            joinLineStr(
              lineWrapInput(
                wrapper(
                  ufansi.Str.join(
                    exceptionPrefixColor(e.getClassName + "."),
                    exceptionMethodColor(e.getMethodName),
                    exceptionPunctuationColor("("),
                    filenameFrag,
                    exceptionPunctuationColor(")")
                  )
                ),
                frameIndent
              ),
              frameIndent
            )
          )
        }
      current = current.getCause
      if (current != null) output.append("\n", leftIndent)
    }

    ufansi.Str.join(output.toSeq:_*)
  }

  def lineWrapInput(input: ufansi.Str, leftIndent: String): Seq[ufansi.Str] = {
    val output = mutable.Buffer.empty[ufansi.Str]
    val plainText = input.plainText
    var index = 0
    while(index < plainText.length){
      val nextWholeLine = index + (formatWrapWidth - leftIndent.length)
      val (nextIndex, skipOne) = plainText.indexOf('\n', index + 1) match{
        case -1 =>
          if (nextWholeLine < plainText.length) (nextWholeLine, false)
          else (plainText.length, false)
        case n =>
          if (nextWholeLine < n) (nextWholeLine, false)
          else (n, true)
      }

      output.append(input.substring(index, nextIndex))
      if (skipOne) index = nextIndex + 1
      else index = nextIndex
    }
    output.toSeq
  }

  def joinLineStr(lines: Seq[ufansi.Str], leftIndent: String) = {
    ufansi.Str.join(lines.flatMap(Seq[ufansi.Str]("\n", leftIndent, _)).drop(2):_*)
  }

  private[this] def prettyTruncate(r: Result, leftIndent: String): ufansi.Str = {
    r.value match{
      case Success(()) => ""
      case Success(v) =>

        val wrapped = lineWrapInput(formatValue(v), leftIndent)
        val truncated =
          if (wrapped.length <= formatTruncateHeight) wrapped
          else wrapped.take(formatTruncateHeight) :+ testValueColor("...")

        joinLineStr(truncated, leftIndent)

      case Failure(e) => formatException(e, leftIndent)
    }
  }

  def wrapLabel(leftIndentCount: Int, r: Result, label: String): ufansi.Str = {
    val leftIndent = "  " * leftIndentCount
    val lhs = ufansi.Str.join(
      leftIndent,
      formatIcon(r.value.isInstanceOf[Success[_]]), " ",
      label, " ",
      formatMillisColor(r.milliDuration + "ms"), " "
    )

    val rhs = prettyTruncate(r, leftIndent + "  ")

    val sep =
      if (lhs.length + rhs.length <= formatWrapWidth && !rhs.plainText.contains('\n')) " "
      else "\n" + leftIndent + "  "

    lhs ++ sep ++ rhs
  }

  def formatSingle(path: Seq[String], r: Result): Option[ufansi.Str] = Some{
    wrapLabel(0, r, path.mkString("."))
  }

  def formatIcon(success: Boolean): ufansi.Str = {
    formatResultColor(success)(if (success) "+" else "X")
  }

  def formatSummary(topLevelName: String, results: HTree[String, Result]): Option[ufansi.Str] = Some{

    val relabelled = results match{
      case HTree.Node(v, c@_*) => HTree.Node(topLevelName, c:_*)
      case HTree.Leaf(r) => HTree.Leaf(r.copy(name = topLevelName))
    }
    val (rendered, totalTime) = rec(0, relabelled){
      case (depth, Left((name, millis))) =>
        ufansi.Str("  " * depth + "- " + name + " ") ++ formatMillisColor(millis + "ms")
      case (depth, Right(r)) => wrapLabel(depth, r, r.name)
    }

    rendered.mkString("\n")
  }

  private[this] def rec(depth: Int, r: HTree[String, Result])
                       (f: (Int, Either[(String, Long), Result]) => ufansi.Str): (Seq[ufansi.Str], Long) = {
    r match{
      case HTree.Leaf(l) => (Seq(f(depth, Right(l))), l.milliDuration)
      case HTree.Node(v, c@_*) =>
        val (subStrs, subTimes) = c.map(rec(depth+1, _)(f)).unzip
        val cumulativeTime = subTimes.sum
        val thisStr = f(depth, Left(v, cumulativeTime))
        (thisStr +: subStrs.flatten, cumulativeTime)
    }
  }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy