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

zio.test.render.ConsoleRenderer.scala Maven / Gradle / Ivy

/*
 * Copyright 2019-2024 John A. De Goes and the ZIO Contributors
 *
 * 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 zio.test.render

import zio.stacktracer.TracingImplicits.disableAutoTrace
import zio.test.ExecutionEvent.{SectionEnd, SectionStart, Test, TestStarted, TopLevelFlush}
import zio.test.render.ExecutionResult.{ResultType, Status}
import zio.test.render.LogLine.Fragment.Style
import zio.test.render.LogLine.{Fragment, Line, Message}
import zio.test._
import zio.{Cause, Trace}
import zio.internal.ansi.AnsiStringOps

trait ConsoleRenderer extends TestRenderer {
  private val tabSize = 2

  override def renderEvent(event: ExecutionEvent, includeCause: Boolean)(implicit
    trace: Trace
  ): Seq[ExecutionResult] =
    event match {
      case _: TestStarted => Nil
      case SectionStart(labelsReversed, _, _) =>
        val depth = labelsReversed.length - 1
        labelsReversed.reverse match {
          case Nil => Seq.empty
          case nonEmptyList =>
            Seq(
              ExecutionResult.withoutSummarySpecificOutput(
                ResultType.Suite,
                label = nonEmptyList.last,
                // We no longer know if the suite has passed here, because the output is streamed
                Status.Passed,
                offset = depth,
                List(TestAnnotationMap.empty), // TODO Examine all results to get this
                lines = List(fr(nonEmptyList.last).toLine),
                duration = None
              )
            )
        }

      case Test(labelsReversed, results, annotations, _, _, _, _) =>
        val labels       = labelsReversed.reverse
        val initialDepth = labels.length - 1
        val (streamingOutput, summaryOutput) =
          testCaseOutput(labels, results, includeCause, annotations)

        Seq(
          ExecutionResult(
            ResultType.Test,
            labels.headOption.getOrElse(""),
            results match {
              case Left(_) => Status.Failed
              case Right(value: TestSuccess) =>
                value match {
                  case TestSuccess.Succeeded(_) => Status.Passed
                  case TestSuccess.Ignored(_)   => Status.Ignored
                }
            },
            initialDepth,
            List(annotations),
            streamingOutput,
            summaryOutput,
            duration = None
          )
        )
      case runtimeFailure @ ExecutionEvent.RuntimeFailure(_, _, failure, _) =>
        val depth = event.labels.length
        failure match {
          case TestFailure.Assertion(result, annotations) =>
            Seq(renderAssertFailure(result, runtimeFailure.labels, depth, annotations))
          case TestFailure.Runtime(cause, annotations) =>
            Seq(
              renderRuntimeCause(
                cause,
                runtimeFailure.labels,
                depth,
                includeCause
              )
            )
        }
      case SectionEnd(_, _, _) =>
        Nil
      case TopLevelFlush(_) =>
        Nil
    }

  override protected def renderOutput(results: Seq[ExecutionResult])(implicit trace: Trace): Seq[String] =
    results.map { result =>
      val message = Message(result.streamingLines).intersperse(Line.fromString("\n"))

      val output = result.resultType match {
        case ResultType.Suite =>
          renderSuite(result.status, result.offset, message)
        case ResultType.Test =>
          renderTest(result.status, result.offset, message)
        case ResultType.Other =>
          Message(result.streamingLines)
      }

      renderToStringLines(output).mkString
    }

  private def renderOutput(output: List[ConsoleIO]): Message =
    if (output.isEmpty)
      Message.empty
    else
      Message(
        Line.fromString("          Console IO Produced by Test         ".red.underlined, 2) +:
          output.map(s => Line.fromString("| ".red + renderConsoleIO(s), 2)) :+
          Line.fromString("==============================================\n".red, 2)
      )

  private def renderConsoleIO(s: ConsoleIO) =
    s match {
      case ConsoleIO.Input(line)  => line.magenta
      case ConsoleIO.Output(line) => line.yellow
    }

  def renderForSummary(results: Seq[ExecutionResult], testAnnotationRenderer: TestAnnotationRenderer): Seq[String] =
    results.map { result =>
      val testOutput: List[ConsoleIO] = result.annotations.flatMap(_.get(TestAnnotation.output))
      val renderedAnnotations         = renderAnnotations(result.annotations, testAnnotationRenderer)
      val message = (Message(result.summaryLines) ++ renderedAnnotations ++ renderOutput(testOutput))
        .intersperse(Line.fromString("\n"))

      val output = result.resultType match {
        case ResultType.Suite =>
          renderSuite(result.status, result.offset, message)
        case ResultType.Test =>
          renderTest(result.status, result.offset, message)
        case ResultType.Other =>
          Message(result.streamingLines)
      }

      renderToStringLines(output).mkString
    }

  private def renderFailure(
    label: String,
    offset: Int,
    details: TestTrace[Boolean],
    annotations: TestAnnotationMap
  ): Message =
    renderFailureLabel(label, offset) +: (renderAnnotations(
      List(annotations),
      TestAnnotationRenderer.default
    ) ++ renderAssertionResult(details, offset)) :+ Line.empty

  private def renderSuite(status: Status, offset: Int, message: Message): Message =
    status match {
      case Status.Passed => withOffset(offset)(info("+") + sp) +: message
      case Status.Failed => withOffset(offset)(Line.empty) +: message
      case Status.Ignored =>
        withOffset(offset)(Line.empty) +: message :+ fr(" - " + TestAnnotation.ignored.identifier + " suite").toLine
    }

  private def renderTest(status: Status, offset: Int, message: Message) =
    status match {
      case Status.Passed  => withOffset(offset)(info("+") + sp) +: message
      case Status.Ignored => withOffset(offset)(warn("-") + sp) +: message
      case Status.Failed  => message
    }

  def renderToStringLines(message: Message): Seq[String] = {
    def renderFragment(f: Fragment): String =
      f.style match {
        case Style.Default             => f.text
        case Style.Primary             => ConsoleUtils.blue(f.text)
        case Style.Warning             => ConsoleUtils.yellow(f.text)
        case Style.Error               => ConsoleUtils.red(f.text)
        case Style.Info                => ConsoleUtils.green(f.text)
        case Style.Detail              => ConsoleUtils.cyan(f.text)
        case Style.Dimmed              => ConsoleUtils.dim(f.text)
        case Style.Bold(fr)            => ConsoleUtils.bold(renderFragment(fr))
        case Style.Underlined(fr)      => ConsoleUtils.underlined(renderFragment(fr))
        case Style.Ansi(fr, ansiColor) => ConsoleUtils.ansi(ansiColor, renderFragment(fr))
      }

    message.lines.map { line =>
      renderOffset(line.offset)(line.optimized.fragments.foldLeft("")((str, f) => str + renderFragment(f)))
    }
  }

  private def renderAnnotations(
    annotations: List[TestAnnotationMap],
    annotationRenderer: TestAnnotationRenderer
  ): Message =
    annotations match {
      case annotations :: ancestors =>
        val rendered = annotationRenderer.run(ancestors, annotations)
        if (rendered.isEmpty)
          Message.empty
        else
          Message(rendered.mkString(" - ", ", ", ""))
      case Nil =>
        Message.empty
    }

  private def renderOffset(n: Int)(s: String) =
    " " * (n * tabSize) + s

  import zio.duration2DurationOps
  def renderSummary(summary: Summary): String =
    s"""${summary.success} tests passed. ${summary.fail} tests failed. ${summary.ignore} tests ignored.
       |${summary.failureDetails}
       |Executed in ${summary.interval.duration.render}
       |""".stripMargin

  def render(cause: Cause[_], labels: List[String]): Option[String] =
    cause match {
      case _: Cause.Interrupt =>
        val renderedInterruption =
          ConsoleRenderer.renderToStringLines(
            Message(Seq(LogLine.Line(Vector(LogLine.Fragment(labels.mkString(" - "), Style.Error)))))
          )

        Some("Interrupted during execution: " + renderedInterruption.mkString("\n"))
      case _ => None
    }
}
object ConsoleRenderer extends ConsoleRenderer




© 2015 - 2024 Weber Informatics LLC | Privacy Policy