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

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

package org.specs2.matcher.describe

import scala.util.{Failure, Success, Try}
import org.specs2.fp.syntax.*
import scala.deriving.Mirror

/** Diffable instances for various types
  */

object PrimitiveDiffable:

  def primitive[T]: Diffable[T] = new Diffable[T] {
    def diff(left: T, right: T) =
      if left == right then PrimitiveIdentical(left)
      else PrimitiveDifference(left, right)
  }

object NothingDiffable extends Diffable[Nothing]:
  def diff(actual: Nothing, expected: Nothing): Nothing =
    throw new AssertionError(
      s"Critical error: attempting to compare values $actual and $expected of type Nothing, this type does not contain any value."
    )

class EitherDiffable[L: Diffable, R: Diffable, T <: Either[L, R]] extends Diffable[T]:

  def valdiff[A](a: A, e: A, isRight: Boolean)(using di: Diffable[A]) =
    val result = di.diff(a, e)
    if result.identical then EitherIdentical(result, isRight = isRight)
    else EitherDifferent(result, isRight = isRight)

  def diff(actual: T, expected: T) =
    (actual, expected) match
      case (Left(a), Left(e))   => valdiff(a, e, false)
      case (Right(a), Right(e)) => valdiff(a, e, true)
      case (a, e)               => EitherTypeDifferent(a.isRight)

class EitherRightDiffable[R](using rdi: Diffable[R]) extends Diffable[Right[Nothing, R]]:
  def diff(actual: Right[Nothing, R], expected: Right[Nothing, R]) =
    (actual, expected) match
      case (Right(a), Right(e)) =>
        val result = rdi.diff(a, e)
        if result.identical then EitherIdentical(result, isRight = true)
        else EitherDifferent(result, isRight = true)

class EitherLeftDiffable[L](using ldi: Diffable[L]) extends Diffable[Left[L, Nothing]]:
  def diff(actual: Left[L, Nothing], expected: Left[L, Nothing]) =
    (actual, expected) match
      case (Left(a), Left(e)) =>
        val result = ldi.diff(a, e)
        if result.identical then EitherIdentical(result, isRight = false)
        else EitherDifferent(result, isRight = true)

class OptionDiffable[T: Diffable, S <: Option[T]](using di: Diffable[T]) extends Diffable[S]:
  def diff(actual: S, expected: S): ComparisonResult =
    (actual, expected) match
      case (Some(a), Some(e)) =>
        val result = di.diff(a, e)
        if result.identical then OptionIdentical(Some(result))
        else OptionDifferent(result)
      case (None, None) => OptionIdentical(None)
      case _            => OptionTypeDifferent(actual.isDefined, expected.isDefined)

class OptionNoneDiffable[T <: Option[Nothing]] extends Diffable[T]:
  def diff(actual: T, expected: T) = OptionIdentical(None)

class TryDiffable[T: Diffable, S <: Try[T]](using tdi: Diffable[Throwable]) extends Diffable[S]:
  def valdiff[A](a: A, e: A, result: ComparisonResult, isSuccess: Boolean) =
    if result.identical then TryIdentical(a, isSuccess = isSuccess)
    else TryDifferent(result, isSuccess = isSuccess)

  def diff(actual: S, expected: S) =
    (actual, expected) match
      case (Failure(a), Failure(e)) => valdiff(a, e, tdi.diff(a, e), false)
      case (Success(a), Success(e)) => valdiff(a, e, summon[Diffable[T]].diff(a, e), true)
      case (a, e)                   => TryTypeDifferent(a.isSuccess)

class FailureDiffable(using di: Diffable[Throwable]) extends Diffable[Failure[Nothing]]:
  def diff(actual: Failure[Nothing], expected: Failure[Nothing]) =
    val (a, e) = (actual.exception, expected.exception)
    val result = di.diff(a, e)
    if result.identical then TryIdentical(a, isSuccess = false)
    else TryDifferent(result, isSuccess = false)

class MapDiffable[K, V, M <: Map[K, V]](using diff: Diffable[V]) extends Diffable[M]:

  def diff(actual: M, expected: M) =
    val changed = findChanged(actual, expected)
    val added = findAdded(actual, expected)
    val removed = findRemoved(actual, expected)
    val keys = (changed ++ added ++ removed).map(_._1)
    val identical = actual.view.filterKeys(k => !keys.contains(k)).toSeq
    if keys.isEmpty then MapIdentical(actual)
    else MapDifference(identical, changed, added, removed)

  private def findChanged(actual: Map[K, V], expected: Map[K, V]) =
    for
      k <- actual.keySet.intersect(expected.keySet).toSeq
      v1 <- actual.get(k)
      v2 <- expected.get(k)
      result = diff.diff(v1, v2)
      if !result.identical
    yield (k, result)

  private def findAdded(actual: Map[K, V], expected: Map[K, V]) =
    expected.view.filterKeys(k => !actual.contains(k)).toSeq

  private def findRemoved(actual: Map[K, V], expected: Map[K, V]) =
    actual.view.filterKeys(k => !expected.contains(k)).toSeq

class StackTraceElementDiffable(using nameDiffable: Diffable[String], lineDiffable: Diffable[Int])
    extends Diffable[StackTraceElement]:

  def diff(actual: StackTraceElement, expected: StackTraceElement) =
    if actual == expected then StackElementIdentical(actual)
    else
      StackElementDifferent(
        nameDiffable.diff(actual.getClassName, expected.getClassName),
        nameDiffable.diff(actual.getMethodName, expected.getMethodName),
        (Option(actual.getFileName) |@| Option(expected.getFileName))(nameDiffable.diff),
        lineDiffable.diff(actual.getLineNumber, expected.getLineNumber)
      )

class ThrowableDiffable[T <: Throwable](using sdi: Diffable[String], adi: Diffable[List[StackTraceElement]])
    extends Diffable[T]:

  def diff(actual: T, expected: T) =
    val messageResult = sdi.diff(actual.getMessage, expected.getMessage)
    if messageResult.identical then
      val stacktraceResult = adi.diff(actual.getStackTrace.toList, expected.getStackTrace.toList)
      if stacktraceResult.identical then ThrowableIdentical(actual)
      else ThrowableDifferentStackTrace(stacktraceResult)
    else ThrowableDifferentMessage(messageResult)

class SetDiffable[E, S <: Set[E]] extends Diffable[S]:

  def diff(actual: S, expected: S): ComparisonResult =
    if actual == expected then SetIdentical(actual)
    else
      SetDifference(
        same = findSame(actual, expected),
        added = findAdded(actual, expected),
        removed = findRemoved(actual, expected)
      )

  private def findSame(actual: Set[E], expected: Set[E]): Seq[E] =
    actual.intersect(expected).toSeq

  private def findAdded(actual: Set[E], expected: Set[E]): Seq[E] =
    expected.diff(actual).toSeq

  private def findRemoved(actual: Set[E], expected: Set[E]): Seq[E] =
    actual.diff(expected).toSeq

/** This diffable uses the Lines diffables to show differences between 2 sequences as a unified sequence with inlined
  * differences
  */
class SeqLinesDiffable[E, S <: Seq[E]](using di: Diffable[E]) extends Diffable[S]:

  def diff(actual: S, expected: S) =
    LinesDiffable.linesDiffable[E].diff(actual.toList, expected.toList)

/** This diffable displays elements missing or added from a Seq
  */
class SeqDiffable[E, S <: Seq[E]](using di: Diffable[E]) extends Diffable[S]:

  def diff(actual: S, expected: S) =
    val diffs = compareExisting(actual, expected)
    if actual.length == expected.length && diffs.forall(_.identical) then SeqIdentical(actual)
    else SeqDifference(result = diffs, added = expected.drop(actual.length), removed = actual.drop(expected.length))

  private def compareExisting(actual: Seq[E], expected: Seq[E]) =
    actual
      .zip(expected)
      .map { case (a, e) => di.diff(a, e) }

class ArrayDiffable[E](using di: Diffable[E]) extends Diffable[Array[E]]:

  def diff(actual: Array[E], expected: Array[E]) =
    val result = compareExisting(actual, expected)
    if actual.length == expected.length && result.toSeq.forall(_.identical) then ArrayIdentical(actual.toIndexedSeq)
    else
      ArrayDifference(
        results = result,
        added = expected.drop(actual.length).toIndexedSeq,
        removed = actual.drop(expected.length).toIndexedSeq
      )

  private def compareExisting(actual: Array[E], expected: Array[E]) =
    actual.zip(expected).toIndexedSeq.map { case (a, e) => di.diff(a, e) }

class FallbackDiffable[T] extends Diffable[T]:
  def diff(actual: T, expected: T) =
    (actual.asInstanceOf[Matchable], expected.asInstanceOf[Matchable]) match
      case (e1: Array[?], e2: Array[?]) =>
        Diffable.diff(e1.toIndexedSeq.map(a => a: Any), e2.toIndexedSeq.map(a => a: Any))
      case (a, e) if a == e =>
        OtherIdentical(actual, expected)
      case (a, e) =>
        OtherDifferent(actual, expected)

class ProductDiffable[T](typeName: String, product: Mirror.ProductOf[T], diffables: =>List[Diffable[?]])
    extends Diffable[T]:
  def diff(actual: T, expected: T): ComparisonResult =
    val actualFields =
      actual.asInstanceOf[Product].productElementNames.toList.zip(actual.asInstanceOf[Product].productIterator.toList)
    val expectedValues = expected.asInstanceOf[Product].productIterator.toList
    val all = actualFields.zip(expectedValues).zip(diffables)
    val results: List[(String, ComparisonResult)] = all.map { case (((actualName, actualValue), expectedValue), d) =>
      (actualName, d.unsafeDiff(actualValue, expectedValue))
    }
    ProductComparisonResult(typeName, results)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy