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

org.specs2.matcher.describe.ComparisonResult.scala Maven / Gradle / Ivy

There is a newer version: 5.5.8
Show newest version
package org.specs2.matcher.describe

import java.io.{PrintWriter, StringWriter}

import org.specs2.matcher.describe.ComparisonResultOps.*
import org.specs2.text.Quote.*
import org.specs2.text.NotNullStrings.*
import org.specs2.text.Indent.*

/** Render the result of a comparison for different types: primitives, throwables, collections,...
  *
  * The comparison may turn out to render identical values or differences
  */
trait ComparisonResult:
  def identical: Boolean
  def render: String
  def render(indent: String): String = render

trait IdenticalComparisonResult extends ComparisonResult:
  val identical = true

trait DifferentComparisonResult extends ComparisonResult:
  val identical = false

case class PrimitiveIdentical(value: Any) extends IdenticalComparisonResult:
  def render: String =
    value.render

case class ThrowableIdentical(value: Throwable) extends IdenticalComparisonResult:
  def render: String =
    val w = new StringWriter
    value.printStackTrace(new PrintWriter(w))
    w.toString

case class ThrowableDifferentMessage(result: ComparisonResult) extends DifferentComparisonResult:
  def render: String =
    "\nthe message is incorrect\n" +
      result.render

case class ThrowableDifferentStackTrace(result: ComparisonResult) extends DifferentComparisonResult:
  def render: String =
    "\nthe stacktrace is incorrect\n" +
      result.render

case class StackElementIdentical(value: StackTraceElement) extends IdenticalComparisonResult:
  def render: String =
    value.toString

case class StackElementDifferent(
    className: ComparisonResult,
    methodName: ComparisonResult,
    fileName: Option[ComparisonResult],
    lineNumber: ComparisonResult
) extends DifferentComparisonResult:
  def render: String =
    s"${className.render}.${methodName.render}$renderSourceLocation"

  private def renderSourceLocation: String =
    fileName
      .map(f => s"(${f.render}:${lineNumber.render})")
      .getOrElse("(Unknown Source)")

case class PrimitiveDifference(actual: Any, expected: Any) extends DifferentComparisonResult:
  def render: String =
    (actual, expected).renderDiff

case class SetIdentical(value: Set[?]) extends OrderedCollectionIdentical(value) with SetTypeProvider

case class SetDifference(same: Seq[Any], added: Seq[Any], removed: Seq[Any])
    extends UnorderedCollectionDifferent(same, Seq.empty[Any], added, removed)
    with SetTypeProvider:

  protected def renderElement(indent: String)(element: Any): String =
    element.render

  protected def renderChange(indent: String)(change: Any): String =
    change.render

trait SetTypeProvider:
  val className = "Set"

case class SeqIdentical(value: Seq[Any]) extends OrderedCollectionIdentical(value) with ListTypeProvider

case class SeqDifference(result: Seq[ComparisonResult], added: Seq[Any], removed: Seq[Any])
    extends OrderedCollectionDifferent(result, added, removed)
    with ListTypeProvider

trait ListTypeProvider:
  val className = "List"

case class ArrayIdentical(value: Seq[Any]) extends OrderedCollectionIdentical(value) with ArrayTypeProvider

case class ArrayDifference(results: Seq[ComparisonResult], added: Seq[Any], removed: Seq[Any])
    extends OrderedCollectionDifferent(results, added, removed)
    with ArrayTypeProvider

trait ArrayTypeProvider:
  val className = "Array"

case class MapIdentical(m: Map[?, ?]) extends OrderedCollectionIdentical(m) with MapTypeProvider

trait MapTypeProvider:
  val className = "Map"

case class MapDifference(
    same: Seq[(Any, Any)],
    changed: Seq[(Any, ComparisonResult)],
    added: Seq[(Any, Any)],
    removed: Seq[(Any, Any)]
) extends UnorderedCollectionDifferent(same, changed, added, removed)
    with MapTypeProvider:

  protected def renderElement(indent: String)(element: (Any, Any)): String =
    element.render

  protected def renderChange(indent: String)(change: (Any, ComparisonResult)): String =
    s"${change._1.render} -> {${change._2.render}}"

case class OptionIdentical(same: Option[ComparisonResult]) extends IdenticalComparisonResult:
  def render: String =
    same.map(_.render.wrapWith("Some")).getOrElse("None")

case class EitherIdentical(same: ComparisonResult, isRight: Boolean) extends IdenticalComparisonResult:
  def render: String =
    same.render.wrapWith(if isRight then "Right" else "Left")

case class TryIdentical(same: Any, isSuccess: Boolean) extends IdenticalComparisonResult:
  def render: String =
    same.render.wrapWith(if isSuccess then "Success" else "Failure")

case class TryDifferent(value: ComparisonResult, isSuccess: Boolean) extends DifferentComparisonResult:
  def render: String =
    value.render.wrapWith(if isSuccess then "Success" else "Failure")

case class TryTypeDifferent(isActualSuccess: Boolean) extends DifferentComparisonResult:
  def render: String =
    s"${render(isActualSuccess)} ==> ${render(!isActualSuccess)}"

  private def render(success: Boolean): String =
    "...".wrapWith(if success then "Success" else "Failure")

case class EitherDifferent(changed: ComparisonResult, isRight: Boolean) extends DifferentComparisonResult:
  def render: String =
    changed.render.wrapWith(if isRight then "Right" else "Left")

case class EitherTypeDifferent(isActualRight: Boolean) extends DifferentComparisonResult:
  def render: String =
    s"${render(isActualRight)} ==> ${render(!isActualRight)}"

  private def render(right: Boolean): String =
    "...".wrapWith(if right then "Right" else "Left")

case class OptionDifferent(changed: ComparisonResult) extends DifferentComparisonResult:
  def render: String =
    changed.render.wrapWith("Some")

case class OptionTypeDifferent(isActualSome: Boolean, isExpectedSome: Boolean) extends DifferentComparisonResult:
  def render: String =
    s"${render(isActualSome)} ==> ${render(isExpectedSome)}"

  private def render(some: Boolean) = if some then "Some(...)" else "None"

case class OtherIdentical(actual: Any, expected: Any) extends IdenticalComparisonResult:
  def render: String =
    s"${actual.renderAny(showAll = comparingPrimitiveWithObject(actual, expected))} == " +
      s"${expected.renderAny(showAll = comparingPrimitiveWithObject(actual, expected))}"

case class OtherDifferent(actual: Any, expected: Any) extends DifferentComparisonResult:
  def render: String =
    val actualRendered = s"${actual.renderAny(showAll = comparingPrimitiveWithObject(actual, expected))}"
    val expectedRendered = s"${expected.renderAny(showAll = comparingPrimitiveWithObject(actual, expected))}"
    if actualRendered.contains("\n") || actualRendered.contains("\n") then s"${actualRendered}\n !=\n$expectedRendered"
    else s"$actualRendered != $expectedRendered"

private[specs2] def comparingPrimitiveWithObject(a: Any, e: Any): Boolean =
  val (classA, classB) = classOf(a) -> classOf(e)
  classA != classB && (isPrimitive(classA) ^ isPrimitive(classB))

private[specs2] def isPrimitive(clazz: String): Boolean =
  clazz.startsWith("java.lang.")

private[specs2] def classOf(v: Any): String =
  Option(v)
    .map(_.getClass.getName)
    .getOrElse("null")

abstract class OrderedCollectionIdentical(value: Iterable[Any]) extends IdenticalComparisonResult:
  def render: String =
    value.map(_.render).mkString(", ").wrapWith(className)

  def className: String

abstract class UnorderedCollectionDifferent[Element, Change](
    same: Seq[Element],
    changed: Seq[Change],
    added: Seq[Element],
    removed: Seq[Element]
) extends DifferentComparisonResult:

  override def render: String =
    render("")

  override def render(indent: String): String =
    val newIndent = indent + " " * (className.length + 1)

    Seq(
      renderIdentical(newIndent) ++ renderChanged(newIndent) ++ renderAdded(newIndent) ++ renderRemoved(newIndent)
    ).flatten.mkString("", ",\n" + newIndent, "").wrapWith(className)

  private def renderIdentical(indent: String): Option[String] =
    same.toOption.map(_.map(renderElement(indent)).mkString("", ",\n" + indent, ""))

  private def renderChanged(indent: String): Option[String] =
    changed.toOption.map(_.map(renderChange(indent)).mkString("", ",\n" + indent, ""))

  private def renderAdded(indent: String): Option[String] =
    added.toOption.map(_.map(renderElement(indent)).mkString("", ",\n" + indent, "").tagWith("added"))

  private def renderRemoved(indent: String): Option[String] =
    removed.toOption.map(_.map(renderElement(indent)).mkString("", ",\n" + indent, "").tagWith("removed"))

  def className: String

  protected def renderElement(indent: String)(element: Element): String
  protected def renderChange(indent: String)(change: Change): String

abstract class OrderedCollectionDifferent[Element](
    results: Seq[ComparisonResult],
    added: Seq[Element],
    removed: Seq[Element]
) extends DifferentComparisonResult:
  override def render: String =
    render("")

  override def render(indent: String): String =
    val newIndent = indent + " " * (className.length + 1)

    Seq(renderResult(newIndent) ++ renderAdded(newIndent) ++ renderRemoved(newIndent)).flatten
      .mkString("", ",\n" + newIndent, "")
      .wrapWith(className)

  private def renderResult(indent: String): Option[String] =
    results.toOption.map(_.map(_.render(indent)).mkString(", "))

  private def renderAdded(indent: String): Option[String] =
    added.toOption.map(_.map(_.render).mkString(", ").tagWith("added"))

  private def renderRemoved(indent: String): Option[String] =
    removed.toOption.map(_.map(_.render).mkString(", ").tagWith("removed"))

  def className: String

object ComparisonResultOps:

  case class PropertyDifference[A, E](actual: A, expected: E)

  extension (value: Any)
    def renderAny(showAll: Boolean = false): String =
      value.asInstanceOf[Matchable] match
        case v if showAll => v.notNullWithClass(showAll = true)
        case (k, v)       => s"${k.render} -> ${v.render}"
        case x: String    => q(x)
        case x            => unq(x)

    def render: String = renderAny(false)

  extension (diff: (Any, Any))
    def renderDiff: String =
      s"${diff._1.render} != ${diff._2.render}"

  extension (values: String)
    def wrapWith(`type`: String): String =
      s"${`type`}($values)"

    def tagWith(tag: String): String =
      s"$tag: $values"

  extension [T](s: Seq[T])
    def toOption: Option[Seq[T]] =
      if s.isEmpty then None else Some(s)

class ProductComparisonResult(typeName: String, results: List[(String, ComparisonResult)]) extends ComparisonResult:
  private val typeNameIndent = " " * (typeName.length + 1)

  def identical: Boolean =
    results.forall(_._2.identical)

  override def render(indent: String): String =
    typeName + "(" + results
      .map { case (fieldName, fieldResult) =>
        val fieldResultRendered = fieldResult.render
        val indented =
          if fieldResultRendered.contains("\n") then "\n" + fieldResultRendered.indentWith(indent + " " * 2)
          else " " + fieldResultRendered
        s"$fieldName:$indented"
      }
      .mkString("\n" + indent) + ")"

  def render: String =
    render(typeNameIndent)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy