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

teststate.dsl.assertions.scala Maven / Gradle / Ivy

The newest version!
package teststate.dsl

import acyclic.file
import japgolly.univeq.UnivEq
import teststate.data.{BeforeAfter, Name, NameFn}
import teststate.typeclass._
import scala.collection.mutable
import Name.Implicits._

// exists

  /*

  def forall[F[_]: Foldable, B](input: Any, fb: F[B])(each: B => EvalL): EvalL = {
    val es = fb.foldLeft(List.empty[Eval])((q, b) => run(each(b)) :: q)
    val ho = es.headOption
    val n  = Need(ho.fold("∅")(e => s"∀{${e.name.value}}"))
    val i  = Input(input)
    val r  = es.filter(_.failure) match {
      case Nil =>
        Eval.success(n, i)
      case fs@(_ :: _) =>
        val causes = fs.foldLeft(Eval.root)((q, e) => q.add(e.name.value, List(e)))
        Eval(n, i, causes)
    }
    r.liftL
  }
   */

object NameUtils {

  def should(pos: Boolean): String =
    if (pos) "should" else "shouldn't"

  def subjectShouldVerb(focusName: String, pos: Boolean, verb: String): Name =
    s"$focusName ${should(pos)} $verb."

  def equal[A](focusName: String, pos: Boolean, expect: A)(implicit sa: Display[A]): Name =
    subjectShouldVerb(focusName, pos, s"be ${sa(expect)}")

  def equalFn[I, A](focusName: String, pos: Boolean, expect: I => A)(implicit sa: Display[A]): Option[I] => Name = {
    case None    => subjectShouldVerb(focusName, pos, "be ")
    case Some(i) => equal(focusName, pos, expect(i))
  }

  def collChangeFn[I, A](focusName: String, pos: Boolean, verb: String, expectDel: I => TraversableOnce[A], expectAdd: I => TraversableOnce[A])(implicit sa: Display[A]): Option[BeforeAfter[I]] => Name = {
    case None    => s"$focusName ${should(pos)} $verb: ."
    case Some(BeforeAfter(i, _)) =>
      val del = expectDel(i)
      val add = expectAdd(i)
      val as = del.toIterator.map("-" + sa(_)) ++ add.toIterator.map("+" + sa(_))
      if (as.isEmpty)
        s"$focusName ${should(!pos)} $verb."
      else
        s"$focusName ${should(pos)} $verb: ${as mkString " "}."
  }
}

object CollectionAssertions {

  private def formatSet(s: TraversableOnce[_]): String =
    s.mkString("", ", ", ".")

  protected final def tallyElements[A](neg: TraversableOnce[A], pos: TraversableOnce[A]): mutable.HashMap[A, Int] = {
    val m = mutable.HashMap.empty[A, Int]
    def go(as: TraversableOnce[A], n: Int): Unit =
      for (a <- as)
        m.update(a, m.get(a).fold(n)(_ + n))
    go(neg, -1)
    go(pos, 1)
    m
  }

  case class EA[@specialized(Int) A](expect: A, actual: A)

  // ===================================================================================================================

  // Depends on A having universal equality and appropriate hashcodes
  sealed abstract class Distinct {
    def name(subject: => String): Name
    def apply[A](as: TraversableOnce[A])(implicit s: Display[A]): Option[Distinct.Failure[A]]

    protected final def prep[A](as: TraversableOnce[A]) = {
      val m = mutable.HashMap.empty[A, Int]
      for (a <- as) {
        val v = m.getOrElse(a, 0) + 1
        m.update(a, v)
      }
      m
    }

    protected final def pass(m: mutable.HashMap[_, Int]): Boolean =
      m.valuesIterator.forall(_ == 1)
  }

  object Distinct {
    def apply(positive: Boolean): Distinct =
      if (positive) Pos else Neg

    object Pos extends Distinct {
      override def name(subject: => String): Name =
        subject + " should be distinct."

      override def apply[A](as: TraversableOnce[A])(implicit s: Display[A]) = {
        val m = prep(as)
        if (pass(m))
          None
        else
          Some(Wasnt(
            m.iterator
              .filter(_._2 > 1)
              .map(x => (x, s(x._1)))
              .toList
              .sortBy(_._2)
              .map(_._1)
          ))
      }
    }

    object Neg extends Distinct {
      override def name(subject: => String): Name =
        subject + " should contain duplicates."

      override def apply[A](as: TraversableOnce[A])(implicit s: Display[A]) =
        if (pass(prep(as)))
          Some(Was)
        else
          None
    }

    sealed trait Failure[+A] extends HasErrorString with Product with Serializable

    final case class Wasnt[+A](dups: List[(A, Int)])(implicit s: Display[A]) extends Failure[A] {
      def dupsToString = formatSet(dups.iterator.map { case (a, i) => s"${s(a)} → $i" })
      override def errorString = s"Dups: $dupsToString"
    }

    case object Was extends Failure[Nothing] {
      override def errorString = "No duplicates found."
    }
  }

  // ===================================================================================================================

  sealed abstract class Exists {
    def name(subject: => String, queryName: => String): Name
    def apply[A, B](source: TraversableOnce[A], query: B)(implicit ev: A <:< B, eb: Equal[B], sb: Display[B]): Option[Exists.Failure[B]]
    protected final def found[A, B](source: TraversableOnce[A], query: B)(implicit ev: A <:< B, eb: Equal[B]) =
      source.exists(a => eb.equal(query, a))
  }

  object Exists {
    def name(expect: Boolean, subject: => String, queryName: => String): Name =
      Name.lazily(apply(expect).name(subject, queryName))

    def nameFn[I](expect: I => Boolean, subject: => String, queryName: => String): NameFn[I] =
      NameFn {
        case None    => s"$subject: possible existence of $queryName."
        case Some(i) => name(expect(i), subject, queryName)
      }

    def apply(positive: Boolean): Exists =
      if (positive) Pos else Neg

    object Pos extends Exists {
      override def name(subject: => String, queryName: => String): Name =
        s"$subject should contain $queryName."

      override def apply[A, B](source: TraversableOnce[A], query: B)(implicit ev: A <:< B, eb: Equal[B], sb: Display[B]): Option[Missing[B]] =
        if (found(source, query))
          None
        else
          Some(Missing(query))
    }

    object Neg extends Exists {
      override def name(subject: => String, queryName: => String): Name =
        s"$subject shouldn't contain $queryName."

      override def apply[A, B](source: TraversableOnce[A], query: B)(implicit ev: A <:< B, eb: Equal[B], sb: Display[B]): Option[Present.type] =
        if (found(source, query))
          Some(Present)
        else
          None
    }

    sealed trait Failure[+A] extends HasErrorString with Product with Serializable

    case class Missing[A](query: A)(implicit s: Display[A]) extends Failure[A] {
      override def errorString = s"Not found: ${s(query)}"
    }

    case object Present extends Failure[Nothing] {
      override def errorString = ""
    }
  }

  // ===================================================================================================================

  sealed abstract class ContainsAll {
    def name(subject: => String, queryNames: => String): Name
    def apply[A: UnivEq, B](source: TraversableOnce[A], query: Set[B])(implicit ev: B <:< A, sb: Display[B]): Option[ContainsAll.Failure[B]]
    protected final def missing[A: UnivEq, B](source: TraversableOnce[A], query: Set[B])(implicit ev: B <:< A) = {
      val as = UnivEq.toSet(source)
      query.iterator.filterNot(as contains _)
    }
  }

  object ContainsAll {
    def apply(positive: Boolean): ContainsAll =
      if (positive) Pos else Neg

    /** ∀b. A ∋ b */
    object Pos extends ContainsAll {
      override def name(subject: => String, queryNames: => String): Name =
        s"$subject should contain all $queryNames."

      override def apply[A: UnivEq, B](source: TraversableOnce[A], query: Set[B])(implicit ev: B <:< A, sb: Display[B]): Option[Missing[B]] = {
        val m = missing(source, query)
        if (m.isEmpty)
          None
        else
          Some(Missing(m.toSet, query))
      }
    }

    /** ∃b. A ∌ b */
    object Neg extends ContainsAll {
      override def name(subject: => String, queryNames: => String): Name =
        s"$subject shouldn't contain all $queryNames."

      override def apply[A: UnivEq, B](source: TraversableOnce[A], query: Set[B])(implicit ev: B <:< A, sb: Display[B]): Option[AllPresent.type] =
        if (missing(source, query).isEmpty)
          Some(AllPresent)
        else
          None
    }

    sealed trait Failure[+A] extends HasErrorString with Product with Serializable

    case class Missing[A](missing: Set[A], query: Set[A])(implicit s: Display[A]) extends Failure[A] {
      def missingToString = formatSet(missing.iterator.map(s(_)))
      override def errorString = s"Missing: $missingToString"
    }

    case object AllPresent extends Failure[Nothing] {
      override def errorString = "All members found."
    }
  }

  // ===================================================================================================================

  sealed abstract class ContainsAny {
    def name(subject: => String, queryNames: => String): Name
    def apply[A, B](source: TraversableOnce[A], query: Set[B])(implicit ev: A <:< B, sb: Display[B]): Option[ContainsAny.Failure[B]]
  }

  /** ∃b. A ∋ b */
  object ContainsSome extends ContainsAny {
    def name(subject: => String, queryNames: => String): Name =
      s"$subject should contain some $queryNames."

    def apply[A, B](source: TraversableOnce[A], query: Set[B])(implicit ev: A <:< B, sb: Display[B]): Option[ContainsAny.FoundNone.type] =
      if (source.exists(query contains _))
        None
      else
        Some(ContainsAny.FoundNone)
  }

  /** ∀b. A ∌ b */
  object ContainsNone extends ContainsAny {
    def name(subject: => String, queryNames: => String): Name =
      s"$subject shouldn't contain any $queryNames."

    def apply[A, B](source: TraversableOnce[A], blacklist: Set[B])(implicit ev: A <:< B, sb: Display[B]): Option[ContainsAny.FoundSome[B]] = {
      var bad = Vector.empty[B]
      for (a <- source)
        if (blacklist contains a)
          bad :+= a.asInstanceOf[B]
      if (bad.isEmpty)
        None
      else
        Some(ContainsAny.FoundSome(bad))
    }
  }

  object ContainsAny {
    def apply(positive: Boolean): ContainsAny =
      if (positive) ContainsSome else ContainsNone

    sealed trait Failure[+A] extends HasErrorString with Product with Serializable

    case object FoundNone extends Failure[Nothing] {
      override def errorString = "None found."
    }

    final case class FoundSome[+A](bad: Vector[A])(implicit s: Display[A]) extends Failure[A] {
      def badToString = formatSet(bad.iterator.map(s(_)))
      override def errorString = s"Found: $badToString"
    }
  }

  // ===================================================================================================================

  sealed abstract class ContainsOnly {
    def name(subject: => String, whitelistNames: => String): Name
    def apply[A: UnivEq, B](source: TraversableOnce[A], whitelist: Set[B])(implicit ev: A <:< B, sa: Display[A]): Option[ContainsOnly.Failure[A]]
    protected final def missing[A: UnivEq, B](source: TraversableOnce[A], whitelist: Set[B])(implicit ev: B <:< A) = {
      val as = UnivEq.toSet(source)
      whitelist.iterator.filterNot(as contains _)
    }
  }

  object ContainsOnly {
    def apply(positive: Boolean): ContainsOnly =
      if (positive) Pos else Neg

    /** ∀a. a ∈ B */
    object Pos extends ContainsOnly {
      override def name(subject: => String, whitelistNames: => String): Name =
        s"$subject should only contain $whitelistNames."

      override def apply[A: UnivEq, B](source: TraversableOnce[A], whitelist: Set[B])(implicit ev: A <:< B, sa: Display[A]) = {
        var bad = Vector.empty[A]
        for (a <- source)
          if (!whitelist.contains(a))
            bad :+= a
        if (bad.isEmpty)
          None
        else
          Some(FoundIllegal(bad))
      }
    }

    /** ∃a. a ∉ B */
    object Neg extends ContainsOnly {
      override def name(subject: => String, whitelistNames: => String): Name =
        s"$subject should contain other than $whitelistNames."

      override def apply[A: UnivEq, B](source: TraversableOnce[A], whitelist: Set[B])(implicit ev: A <:< B, sa: Display[A]) =
        if (source.exists(!whitelist.contains(_)))
          None
        else
          Some(NothingOffWhitelist)
    }

    sealed trait Failure[+A] extends HasErrorString with Product with Serializable

    case class FoundIllegal[+A](illegal: Vector[A])(implicit s: Display[A]) extends Failure[A] {
      def illegalToString = formatSet(illegal.iterator.map(s(_)))
      override def errorString = s"Found: $illegalToString"
    }

    case object NothingOffWhitelist extends Failure[Nothing] {
      override def errorString = "None found."
    }
  }

  // ===================================================================================================================

  // TODO: Need sub/super-set matchers. This is just a specific case.
  // ∀/∃ ⊆ ⊇ ⊂ ⊃ ⊄ ⊅ ⊈ ⊉

  object ExistenceOfAll {
    def name(expect: Boolean, subject: => String, allNames: => String): Name =
      Name.lazily(
        if (expect)
          ContainsAll.Pos.name(subject, allNames)
        else
          ContainsNone.name(subject, allNames))

    def nameFn[I](expect: I => Boolean, subject: => String, allNames: => String): NameFn[I] =
      NameFn {
        case None    => s"$subject: possible existence of $allNames."
        case Some(i) => name(expect(i), subject, allNames)
      }

    def apply[A: Display: UnivEq](expect: Boolean, source: TraversableOnce[A], all: Set[A]): Option[Either[ContainsAny.FoundSome[A], ContainsAll.Missing[A]]] =
      if (expect)
        ContainsAll.Pos(source, all).map(Right(_))
      else
        ContainsNone(source, all).map(Left(_))
  }

  // ===================================================================================================================

  sealed abstract class EqualIgnoringOrder {
//    def name(subject: => String, expectName: => String): Name
    def apply[A](source: TraversableOnce[A], expect: TraversableOnce[A])(implicit s: Display[A]): Option[EqualIgnoringOrder.Failure[A]]

    protected final def prep[A](source: TraversableOnce[A], expect: TraversableOnce[A]) =
      tallyElements(expect, source)

    protected final def pass(m: mutable.HashMap[_, Int]): Boolean =
      m.valuesIterator.forall(_ == 0)
  }

  object EqualIgnoringOrder {
    def apply(positive: Boolean): EqualIgnoringOrder =
      if (positive) Pos else Neg

    object Pos extends EqualIgnoringOrder {
//      override def name(subject: => String, expectName: => String): Name =
//        s"$subject should equal $expectName ignoring order."

      override def apply[A](source: TraversableOnce[A], expect: TraversableOnce[A])(implicit s: Display[A]) = {
        val m = prep(source, expect)
        if (pass(m))
          None
        else
          Some {
            val missing = Vector.newBuilder[A]
            val excess  = Vector.newBuilder[A]
            for {
              (a, n) <- m if n != 0
              target = if (n < 0) missing else excess
              _ <- 1 to n.abs
            } target += a
            Mismatch(missing.result(), excess.result())
          }
      }
    }

    object Neg extends EqualIgnoringOrder {
//      override def name(subject: => String, expectName: => String): Name =
//        s"$subject shouldn't equal $expectName ignoring order."

      override def apply[A](source: TraversableOnce[A], expect: TraversableOnce[A])(implicit s: Display[A]) =
        if (pass(prep(source, expect)))
          Some(Matched)
        else
          None
    }

    sealed trait Failure[+A] extends HasErrorString with Product with Serializable

    case class Mismatch[+A](missing: Vector[A], excess: Vector[A])(implicit s: Display[A]) extends Failure[A] {
      private def fmt[AA >: A](name: String, as: Vector[AA])(implicit s: Display[AA]): Option[String] =
        if (as.isEmpty) None else Some(name + ": " +formatSet(as.iterator.map(s(_))))
      override def errorString =
        (fmt("Missing", missing).toList ::: fmt("Excess", excess).toList).mkString(" ")
    }

    case object Matched extends Failure[Nothing] {
      override def errorString = "Set members match."
    }
  }

  // ===================================================================================================================

  sealed abstract class EqualIncludingOrder {
    def apply[A](source: TraversableOnce[A], expect: TraversableOnce[A])(implicit eq: Equal[A], s: Display[A]): Option[EqualIncludingOrder.Failure[A]]
  }

  // TODO Could do much better. Should diff
  object EqualIncludingOrder {
    def apply(positive: Boolean): EqualIncludingOrder =
      if (positive) Pos else Neg

    object Pos extends EqualIncludingOrder {
      override def apply[A](source: TraversableOnce[A], expect: TraversableOnce[A])(implicit eq: Equal[A], s: Display[A]) = {
        val a = source.toVector
        val e = expect.toSeq
        if (a.corresponds(e)(eq.equal))
          None
        else
          Some(Mismatch(a, e.toVector))
      }
    }

    object Neg extends EqualIncludingOrder {
      override def apply[A](source: TraversableOnce[A], expect: TraversableOnce[A])(implicit eq: Equal[A], s: Display[A]) =
        if (source.toSeq.corresponds(expect.toSeq)(eq.equal))
          Some(Matched)
        else
          None
    }

    sealed trait Failure[+A] extends HasErrorString with Product with Serializable

    case class Mismatch[+A](actual: Vector[A], expect: Vector[A])(implicit s: Display[A]) extends Failure[A] {
      private def fmt[AA >: A](name: String, as: Vector[AA])(implicit s: Display[AA]): Option[String] =
        if (as.isEmpty) None else Some(name + ": " +formatSet(as.iterator.map(s(_))))
      override def errorString =
        (fmt("Actual", actual).toList ::: fmt("Expect", expect).toList).mkString(" ")
    }

    case object Matched extends Failure[Nothing] {
      override def errorString = "Set members match."
    }
  }


  // ===================================================================================================================

  sealed abstract class ElemChanges {
    import ElemChanges._

    def apply[A](args: Args[A])(implicit s: Display[A]): Option[Failure[A]]

    protected final def prep[A](args: Args[A]): Option[Map[A, EA[Int]]] = {
      val actual = tallyElements(args.before, args.after)
      val expect = tallyElements(args.expectDel, args.expectAdd)

      var errors = Map.empty[A, EA[Int]]

      for ((k, a) <- actual) {
        val e = expect.getOrElse(k, 0)
        if (a != e)
          errors = errors.updated(k, EA(expect = e, actual = a))
      }

      for ((k, e) <- expect)
        if (e != 0 && !actual.contains(k))
          errors = errors.updated(k, EA(expect = e, actual = 0))

      if (errors.isEmpty)
        None
      else
        Some(errors)
    }
  }

  object ElemChanges {
    case class Args[A](before   : TraversableOnce[A],
                       after    : TraversableOnce[A],
                       expectDel: TraversableOnce[A],
                       expectAdd: TraversableOnce[A])

    def apply(positive: Boolean): ElemChanges =
      if (positive) Pos else Neg

    object Pos extends ElemChanges {
      override def apply[A](args: Args[A])(implicit s: Display[A]) =
        prep(args).map(Mismatch(_))
    }

    object Neg extends ElemChanges {
      override def apply[A](args: Args[A])(implicit s: Display[A]) =
        prep(args) match {
          case Some(errors) => None
          case None         => Some(Matched())
        }
    }

    sealed trait Failure[A] extends HasErrorString with Product with Serializable

    case class Mismatch[A](errors: Map[A, EA[Int]])(implicit s: Display[A]) extends Failure[A] {
      override def errorString =
        errors.iterator
          .map { case (i, EA(e, a)) => s"${s(i)} moved by $a, expected $e." }
          .mkString(" ")
    }

    case class Matched[A]() extends Failure[A] {
      override def errorString = "Expected changes occurred."
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy