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

teststate.run.ReportFormat.scala Maven / Gradle / Ivy

The newest version!
package teststate.run

import scala.Console._
import scala.annotation.tailrec
import scala.concurrent.duration.FiniteDuration
import teststate.data.Result.{Fail, Pass, Skip}
import teststate.data.{Failure, Result}
import teststate.run.History.{Step, Steps}
import teststate.typeclass.DisplayError

trait ReportFormat {
  def format[E](report: Report[E])(implicit de: DisplayError[E]): Option[String]

  def print[E](report: Report[E])(implicit de: DisplayError[E]): Unit =
    for (s <- format(report))
      ReportFormat.printMutex.synchronized {
        System.err.flush()
        System.out.flush()
        System.out.println(s)
        System.out.flush()
      }
}

object ReportFormat {

  val printMutex = new AnyRef

  val quiet: ReportFormat =
    new ReportFormat {
      override def format[E](report: Report[E])(implicit de: DisplayError[E]) =
        None
    }

  class Default(s: Default.Settings) extends ReportFormat {
    override def format[E](report: Report[E])(implicit de: DisplayError[E]) = {
      val sb = new StringBuilder

      def appendIndent(indent: Int): Unit = {
        var i = indent
        while (i > 0) {
          sb append s.indent
          i -= 1
        }
      }

      def appendResultFlag(r: Result[Any]): Unit = {
        sb append (r match {
          case Pass    => s.onPass
          case Skip    => s.onSkip
          case Fail(_) => s.onFail
        })
        ()
      }

      def showHistory(h: History[Failure[E]], indent: Int): Unit =
        showSteps(h.steps, indent)

      def showSteps(steps: Steps[Failure[E]], indent: Int): Unit =
        steps foreach (showStep(_, indent))

      def showStep(step: Step[Failure[E]], indent: Int): Unit = {
        val showChildren = step.children.steps.nonEmpty && s.showChildren(step.children)
        val optionFailure = if (showChildren) None else step.result.failure

        appendIndent(indent)
        appendResultFlag(step.result)
        sb append ' '
        val stepDesc1 = step.name.value
        val stepDesc2 = stepDesc1.replace("\n", "\n      ")
        val multilineStepDesc = stepDesc2.length != stepDesc1.length
        sb append stepDesc2
        for (failure <- optionFailure) {
          val errorMsg = de.display(failure.failure)
          if (errorMsg.nonEmpty) {
            val firstLine = errorMsg.takeWhile(_ != '\n')
            if (firstLine.length == errorMsg.length) {
              // Single line error msg
              sb append s.failureDetail
              sb append " -- "
              sb append errorMsg
            } else {
              // Multi-line error msg
              val indent2 = indent + (if (multilineStepDesc) 1 else 2)
              @tailrec
              def go(line: String, remainderWithLine: String): Unit = {
                val remainder = remainderWithLine.drop(line.length + 1)
                sb append s.eol
                appendIndent(indent2)
                sb append s.failureDetail
                sb append line
                if (remainder.nonEmpty)
                  go(remainder.takeWhile(_ != '\n'), remainder)
              }
              go(firstLine, errorMsg)
            }
          }
        }
        sb append s.eol

        if (showChildren)
          showHistory(step.children, indent + 1)
      }

      report.name match {
        case None =>
          showHistory(report.history, 0)
        case Some(n) =>
          appendResultFlag(report.result)
          sb append ' '
          sb append n.value
          sb append s.eol
          showHistory(report.history, 1)
      }

      if (report.history.result != Skip) {
//      if (report.history.nonEmpty) { TODO Stop adding summaries in Runner
//        sb append "  "
        s.stats.run(sb, report.stats)
        sb append s.eol
      }

      Some(sb.result())
    }
  }

  object Default {

    case class StatsFormat(run: (StringBuilder, Stats) => Unit) extends AnyVal {
      def before(f: (StringBuilder, Stats) => Any): StatsFormat =
        StatsFormat { (sb, s) =>
          f(sb, s)
          run(sb, s)
        }

      def after(f: (StringBuilder, Stats) => Any): StatsFormat =
        StatsFormat { (sb, s) =>
          run(sb, s)
          f(sb, s)
          ()
        }

      def before(f: StringBuilder => Any): StatsFormat =
        before((sb, _) => f(sb))

      def after(f: StringBuilder => Any): StatsFormat =
        after((sb, _) => f(sb))
    }

    object StatsFormat {

      def formatDuration(secPrec: Int): FiniteDuration => String = {
        val secfmt = "%." + secPrec + "f sec"
        d =>
          if (d.toMicros == 0)
            d.toNanos.toString + " μs"
          else if (d.toSeconds == 0)
            d.toMillis.toString + " ms"
          else
            secfmt.format(d.toMicros.toDouble / 1000000)
      }

      val sec1 = formatDuration(1)

      private def units(sb: StringBuilder, n: Int, u: String) = {
        sb append n
        sb append ' '
        sb append u
        if (n != 1)
          sb append 's'
        ()
      }

      val _withoutTime: StatsFormat =
        StatsFormat { (sb, s) =>
          sb append "Performed "
          units(sb, s.performedActions, "action")
          sb append ", "
          units(sb, s.performedChecks, "check")
          if (s.retries > 0) {
            sb append " (with "
            sb append s.retries
            sb append " retries)"
          }
          ()
        }

      val withoutTime: StatsFormat =
        _withoutTime.after(_ append '.')

      val onlyTime: StatsFormat =
        StatsFormat { (sb, s) =>
          sb append "Took "
          sb append sec1(s.totalTime)
          sb append '.'
          ()
        }

      val default: StatsFormat =
        StatsFormat { (sb, s) =>
          _withoutTime.run(sb, s)
          sb append " in "
          sb append sec1(s.totalTime)
          sb append '.'
          ()
        }
    }

    case class Settings(indent       : String,
                        onPass       : String,
                        onSkip       : String,
                        onFail       : String,
                        failureDetail: String,
                        eol          : String,
                        showChildren : History[Any] => Boolean,
                        stats        : StatsFormat) {

      def alwaysShowChildren =
        copy(showChildren = _ => true)

      def onlyShowFailedChildren =
        copy(showChildren = _.failure.isDefined)

      def apply: ReportFormat =
        new Default(this)
    }

    implicit def settingsToReportFormat(s: Settings): ReportFormat =
      s.apply

    def uncolouredUnicode = Settings(
      indent        = "  ",
      onPass        = "✓", // ✓ ✔
      onFail        = "✘", // ✗ ✘
      onSkip        = "-", // ⇣ ↶ ↷
      failureDetail = "",
      eol           = "\n",
      showChildren  = _.failure.isDefined,
      stats         = StatsFormat.default)

    def uncolouredAscii: Settings =
      uncolouredUnicode.copy(
      onPass = "[pass]",
      onFail = "[fail]",
      onSkip = "[skip]")

    def addColour(uncoloured: Settings): Settings = {
      // scala.Console.BOLD exists but not BRIGHT
      // The difference affects OS X
      val BRIGHT_BLACK  = "\u001b[90m"
      val BRIGHT_RED    = "\u001b[91m"
      val BRIGHT_GREEN  = "\u001b[92m"
      val BRIGHT_YELLOW = "\u001b[93m"
      // val BRIGHT_BLUE    = "\u001b[94m"
      // val BRIGHT_MAGENTA = "\u001b[95m"
      // val BRIGHT_CYAN    = "\u001b[96m"
      // val BRIGHT_WHITE   = "\u001b[97m"
      Settings(
        indent        = "  ",
        onPass        = BOLD + BRIGHT_GREEN + uncoloured.onPass + RESET + WHITE,
        onFail        = BRIGHT_RED + uncoloured.onFail + BOLD,
        onSkip        = BOLD + BRIGHT_YELLOW + uncoloured.onSkip + RESET + BRIGHT_BLACK,
        failureDetail = RESET + RED + uncoloured.failureDetail,
        eol           = RESET + uncoloured.eol,
        showChildren  = uncoloured.showChildren,
        stats         = uncoloured.stats.before(_ append WHITE))
    }

    val uncoloured: Settings =
      uncolouredUnicode

    val coloured: Settings =
      addColour(uncoloured)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy