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

japgolly.microlibs.testutil.TestUtil.scala Maven / Gradle / Ivy

There is a newer version: 4.2.1
Show newest version
package japgolly.microlibs.testutil

import japgolly.univeq.UnivEq
import japgolly.univeq.UnivEqScalaz.scalazEqualFromUnivEq
import java.io.ByteArrayOutputStream
import scala.annotation.tailrec
import scala.collection.compat._
import scala.io.AnsiColor._
import scalaz.Equal
import scalaz.syntax.equal._
import sourcecode.Line
import TestUtilInternals._

trait TestUtilWithoutUnivEq
    extends ScalaVerSpecificTestUtil
       with TypeTestingUtil
       with TestUtilImplicits {

  def withAtomicOutput[A](a: => A): A = {
    val os = new ByteArrayOutputStream()
    try
      printMutex.synchronized(
        Console.withOut(os)(
          Console.withErr(os)(
            a)))
    finally
      print(os.toString())
  }

  def fail(msg: String, clearStackTrace: Boolean = true, addSrcHint: Boolean = true)(implicit q: Line): Nothing = {
    val m = if (addSrcHint) TestUtilInternals.addSrcHint(msg) else msg
    val e = new java.lang.AssertionError(m)
    if (clearStackTrace)
      e.setStackTrace(Array.empty)
    throw e
  }

  def failMethod(method: String)(implicit q: Line): Nothing =
    failMethod(method, None)

  def failMethod(method: String, desc: String)(implicit q: Line): Nothing =
    failMethod(method, Some(desc))

  def failMethod(method: String, desc: Option[String])(implicit q: Line): Nothing =
    fail(descMethod(method, desc) + " failed.")

  def onFail[A](body: => A)(onFail: => Any): A =
    try
      body
    catch {
      case t: java.lang.AssertionError =>
        onFail
        throw t
    }

  def onError[A](body: => A)(onError: Throwable => Any): A =
    try
      body
    catch {
      case t: Throwable =>
        onError(t)
        throw t
    }

  private def fail2(method: String, name: Option[String])
                   (title1: String, colour1: String, value1: Any)
                   (title2: String, colour2: String, value2: Any)
                   (implicit q: Line): Nothing = {
    printFail2(name)(title1, colour1, value1)(title2, colour2, value2)
    println()
    failMethod(method, name)
  }

  def assertEq[A: Equal](actual: A, expect: A)(implicit q: Line): Unit =
    assertEqO(None, actual, expect)

  def assertEq[A: Equal](name: => String, actual: A, expect: A)(implicit q: Line): Unit =
    assertEqO(Some(name), actual, expect)

  def assertEqO[A: Equal](name: => Option[String], actual: A, expect: A)(implicit q: Line): Unit =
    if (actual ≠ expect)
      fail2("assertEq", name)("expect", BOLD_BRIGHT_GREEN, expect)("actual", BOLD_BRIGHT_RED, actual)

  def assertNotEq[A: Equal](actual: A, expect: A)(implicit q: Line): Unit =
    assertNotEqO(None, actual, expect)

  def assertNotEq[A: Equal](name: => String, actual: A, expect: A)(implicit q: Line): Unit =
    assertNotEqO(Some(name), actual, expect)

  private def assertNotEqO[A: Equal](name: => Option[String], actual: A, expectNot: A)(implicit q: Line): Unit =
    if (actual === expectNot)
      fail2("assertNotEq", name)("expect not", BOLD_BRIGHT_BLUE, expectNot)("actual", BOLD_BRIGHT_RED, actual)

  def assertMultiline(actual: String, expect: String)(implicit q: Line): Unit =
    _assertMultiline(None, actual, expect)

  def assertMultiline(name: => String, actual: String, expect: String)(implicit q: Line): Unit =
    _assertMultiline(Some(name), actual, expect)

  private def _assertMultiline(name: => Option[String], actual: String, expect: String)(implicit q: Line): Unit =
    if (actual != expect) withAtomicOutput {
      println()
      val AE = List(actual, expect).map(_.split("\n"))
      val List(as, es) = AE
      val lim = as.length max es.length
      val List(maxA,_) = AE.map(x => (0 :: x.iterator.map(_.length).toList).max)
      val maxL = lim.toString.length
      if (maxL == 0 || maxA == 0)
        assertEqO(name, actual, expect)
      else {
        val nameSuffix = name.fold("")(": " + _)
        if (as.length == es.length) {
          println(s"${BRIGHT_YELLOW}assertMultiline$nameSuffix$RESET (actual | expect)")
          val fmtOK = s"${BRIGHT_BLACK}%${maxL}d: %-${maxA}s | | %s${RESET}\n"
          val fmtWS = s"${WHITE}%${maxL}d: ${RED_B}${BLACK}%-${maxA}s${RESET}${WHITE} |≈| ${GREEN_B}${BLACK}%s${RESET}\n"
          val fmtKO = s"${WHITE}%${maxL}d: ${BOLD_BRIGHT_RED}%-${maxA}s${RESET}${WHITE} |≠| ${BOLD_BRIGHT_GREEN}%s${RESET}\n"
          def removeWhitespace(s: String) = s.filterNot(_.isWhitespace)
          for (i <- 0 until lim) {
            val List(a, e) = AE.map(s => if (i >= s.length) "" else s(i))
            val fmt =
              if (a == e) fmtOK
              else if (removeWhitespace(a) == removeWhitespace(e)) fmtWS
              else fmtKO
            printf(fmt, i + 1, a, e)
          }
        } else {
          println(s"${BRIGHT_YELLOW}assertMultiline$nameSuffix$RESET")
          println(LineDiff(expect, actual).expectActualColoured)
          println(BRIGHT_YELLOW + ("-" * 120) + RESET)
        }
        println()
        fail("assertMultiline failed.")
      }
    }

  def assertMap[K: UnivEq, V: Equal](actual: Map[K, V], expect: Map[K, V])(implicit q: Line): Unit =
    assertMapO(None, actual, expect)

  def assertMap[K: UnivEq, V: Equal](name: => String, actual: Map[K, V], expect: Map[K, V])(implicit q: Line): Unit =
    assertMapO(Some(name), actual, expect)

  def assertMapO[K: UnivEq, V: Equal](name: => Option[String], actual: Map[K, V], expect: Map[K, V])(implicit q: Line): Unit = {
    assertSet(name.fold("Map keys")(_ + " keys"), actual.keySet, expect.keySet)
    val bad = actual.keysIterator.filter(k => actual(k) ≠ expect(k))
    if (bad.nonEmpty) {
      val x = bad.toVector
      for (k <- x) {
        println(s"MapKey: $k")
        println(s"Expect: $BOLD_BRIGHT_GREEN${expect(k)}$RESET")
        println(s"Actual: $BOLD_BRIGHT_RED${actual(k)}$RESET")
        println()
      }
      fail(s"assertMap${name.fold("")("(" + _ + ")")} failed with ${x.length} value discrepancies.")
    }
  }

  def assertSeq[A: Equal](actual: Iterable[A])(expect: A*)(implicit q: Line): Unit = assertSeq(actual, expect.toSeq)
  def assertSeq[A: Equal](actual: Iterable[A], expect: Iterable[A])(implicit q: Line): Unit = assertSeqO(None, actual, expect)
  def assertSeq[A: Equal](name: => String, actual: Iterable[A])(expect: A*)(implicit q: Line): Unit = assertSeq(name, actual, expect.toSeq)
  def assertSeq[A: Equal](name: => String, actual: Iterable[A], expect: Iterable[A])(implicit q: Line): Unit = assertSeqO(Some(name), actual, expect)

  def assertSeqO[A: Equal](name: => Option[String], actual: Iterable[A], expect: Iterable[A])(implicit q: Line): Unit = {
    var failures = List.empty[Int]
    var lenOk    = true

    val ia = actual.iterator
    val ie = expect.iterator
    @tailrec def go(i: Int): Unit =
      if (ia.hasNext) {
        val a = ia.next()
        if (ie.hasNext) {
          val e = ie.next()
          if (a ≠ e)
            failures ::= i
          go(i + 1)
        } else
          lenOk = false
      } else
        lenOk = ie.isEmpty
    go(0)

    val pass = lenOk && failures.isEmpty
    if (!pass) {
      val av         = actual.toVector
      val ev         = expect.toVector
      val lenMin     = av.length min ev.length
      val lenMax     = av.length max ev.length - 1
      val leadFmt    = s"[%${lenMax.toString.length}d]"
      val leadColour = RED_B
      val leadSize   = leadFmt.format(lenMax).length
      val leadBlank  = leadColour + (" " * leadSize)
      val n = name
      withAtomicOutput {
        failureStart(Some(descMethod("assertSeq", n)), leadSize + 8)

        var prevWasMultiline = false
        def log(i: Int): Unit = {
          val oa = av.lift(i)
          val oe = ev.lift(i)
          val lead = leadColour + leadFmt.format(i)
          def le(e: Any) = s" expect:$RESET ${BOLD_BRIGHT_GREEN}${e}$RESET"
          def la(a: Any) = s" actual:$RESET ${BOLD_BRIGHT_RED}${a}$RESET"
          if (prevWasMultiline) println()
          (oa, oe) match {

            case (Some(a), Some(e)) =>
              //val as = "" + a
              //val es = "" + e
              //if (as.length + es.length <= 110) {
              //  prevWasMultiline = false
              //  println(s"$lead noteql:$RESET ${BOLD_BRIGHT_GREEN}${es}$RESET ≠ ${BOLD_BRIGHT_RED}${as}$RESET")
              //} else {
                prevWasMultiline = true
                println(lead + le(e))
                println(leadBlank + la(a))
              //}

            case (Some(a), None) =>
              prevWasMultiline = false
              println(lead + la(a))

            case (None, Some(e)) =>
              prevWasMultiline = false
              println(lead + le(e))

            case (None, None) => ()
          }
        }

        failures.reverse.foreach(log)
        (lenMin to lenMax).foreach(log)
        println()
        fail(s"assertSeq${name.fold("")("(" + _ + ")")} failed.")
      }
    }
  }

  def assertSeqIgnoreOrder[A: Equal](actual: IterableOnce[A])(expect: A*)(implicit q: Line): Unit = assertSeqIgnoreOrder(actual, expect.toSeq)
  def assertSeqIgnoreOrder[A: Equal](actual: IterableOnce[A], expect: IterableOnce[A])(implicit q: Line): Unit = assertSeqIgnoreOrderO(None, actual, expect)
  def assertSeqIgnoreOrder[A: Equal](name: => String, actual: IterableOnce[A])(expect: A*)(implicit q: Line): Unit = assertSeqIgnoreOrder(name, actual, expect.toSeq)
  def assertSeqIgnoreOrder[A: Equal](name: => String, actual: IterableOnce[A], expect: IterableOnce[A])(implicit q: Line): Unit = assertSeqIgnoreOrderO(Some(name), actual, expect)

  def assertSeqIgnoreOrderO[A](name: => Option[String], actual: IterableOnce[A], expect: IterableOnce[A])
                              (implicit q: Line, A: Equal[A]): Unit =
    _assertSeqIgnoreOrderO("assertSeqIgnoreOrder")(name, actual, expect)

  private def _assertSeqIgnoreOrderO[A](methodName: String)
                                       (name: => Option[String], actual: IterableOnce[A], expect: IterableOnce[A])
                                       (implicit q: Line, A: Equal[A]): Unit = {
    val as = actual.iterator.toArray[Any]
    val es = expect.iterator.toArray[Any]
    var matches = 0

    for (ia <- as.indices) {
      val a = as(ia).asInstanceOf[A]
      @tailrec def go(ie: Int): Unit =
        if (ie >= 0) {
          val e = es(ie)
          val ok = !e.isInstanceOf[Poison] && A.equal(a, e.asInstanceOf[A])
          if (ok) {
            matches += 1
            as(ia) = Poison
            es(ie) = Poison
          } else
            go(ie - 1)
        }
      go(es.length - 1)
    }

    val sizeMatch = as.length == es.length
    val pass      = sizeMatch && matches == es.length
    if (!pass) {
      val n = name
      // def prefix = n.fold("")(_ + ": ")
      def eTitle = "Expect elements:"
      def aTitle = "Actual elements:"
      // def eSizeT = "Expect seq size:"
      // def aSizeT = "Actual seq size:"

      withAtomicOutput {
        // if (sizeMatch)
        //   failureStart(n, aTitle.length)
        // else
        //   printFailEA(Some(prefix + "Element count"), actual = as.length, expect = es.length)
        failureStart(n, aTitle.length)
        // if (!sizeMatch) {
        //   println(s"${lead(eSizeT)}${BOLD_BRIGHT_GREEN}${es.length}$RESET")
        //   println(s"${lead(aSizeT)}${BOLD_BRIGHT_RED}${as.length}$RESET")
        // }

        def showElements(xs: Array[Any], title: String, colour: String, sizePrefix: String): Unit = {
          val b = Array.newBuilder[String]
          for (x <- xs)
            if (!x.isInstanceOf[Poison])
              b += "" + x
          val ss = b.result()
          if (ss.nonEmpty) {
            java.util.Arrays.sort(ss, Ordering[String])
            println(lead(title) + colour + sizePrefix + ss.length + " elements" + RESET)
            for (s <- ss)
              println(s"- $colour$s$RESET")
          }
        }

        showElements(es, eTitle, BOLD_BRIGHT_GREEN, "-")
        showElements(as, aTitle, BOLD_BRIGHT_RED, "+")
        println()
        failMethod(methodName, n)
      }
    }
  }

  def assertSet[A: UnivEq](actual: Set[A])(expect: A*)(implicit q: Line): Unit = assertSet(actual, expect.toSet)
  def assertSet[A: UnivEq](actual: Set[A], expect: Set[A])(implicit q: Line): Unit = assertSetO(None, actual, expect)
  def assertSet[A: UnivEq](name: => String, actual: Set[A])(expect: A*)(implicit q: Line): Unit = assertSet(name, actual, expect.toSet)
  def assertSet[A: UnivEq](name: => String, actual: Set[A], expect: Set[A])(implicit q: Line): Unit = assertSetO(Some(name), actual, expect)

  def assertSetO[A: UnivEq](name: => Option[String], actual: Set[A], expect: Set[A])(implicit q: Line): Unit =
    if (actual != expect)
      _assertSeqIgnoreOrderO("assertSet")(name, actual, expect)

  private def ci(s: String): String = s.toLowerCase
  private def ci(s: Set[String]): Set[String] = s.map(ci(_))

  def assertContains(actual: String, substr: String)(implicit q: Line): Unit =
    _assertContains("assertContains", actual, substr, true)

  def assertContainsCI(actual: String, substr: String)(implicit q: Line): Unit =
    _assertContains("assertContainsCI", ci(actual), ci(substr), true)

  def assertNotContains(actual: String, substr: String)(implicit q: Line): Unit =
    _assertContains("assertNotContains", actual, substr, false)

  def assertNotContainsCI(actual: String, substr: String)(implicit q: Line): Unit =
    _assertContains("assertNotContainsCI", ci(actual), ci(substr), false)

  private def _assertContains(method: String, actual: String, substr: String, expect: Boolean)(implicit q: Line): Unit =
    if (actual.contains(substr) != expect) {
      printFail2(Some(method))(
        "substr", _assertContainSubstrColour(expect), substr)(
        "actual", BOLD_BRIGHT_RED, actual)
      fail(s"$method failed.")
    }

  private def _assertContainSubstrColour(expect: Boolean): String =
    if (expect) BOLD_BRIGHT_GREEN else BOLD_BRIGHT_BLUE

  def assertContainsAny(actual: String, substrs: String*)(implicit q: Line): Unit =
    _assertContainsSet("assertContainsAny", actual, substrs.toSet, ∃ = true, expect = true)

  def assertContainsAll(actual: String, substrs: String*)(implicit q: Line): Unit =
    _assertContainsSet("assertContainsAll", actual, substrs.toSet, ∃ = false, expect = true)

  def assertNotContainsAny(actual: String, substrs: String*)(implicit q: Line): Unit =
    _assertContainsSet("assertNotContainsAny", actual, substrs.toSet, ∃ = true, expect = false)

  def assertNotContainsAll(actual: String, substrs: String*)(implicit q: Line): Unit =
    _assertContainsSet("assertNotContainsAll", actual, substrs.toSet, ∃ = false, expect = false)

  def assertContainsAnyCI(actual: String, substrs: String*)(implicit q: Line): Unit =
    _assertContainsSet("assertContainsAnyCI", ci(actual), ci(substrs.toSet), ∃ = true, expect = true)

  def assertContainsAllCI(actual: String, substrs: String*)(implicit q: Line): Unit =
    _assertContainsSet("assertContainsAllCI", ci(actual), ci(substrs.toSet), ∃ = false, expect = true)

  def assertNotContainsAnyCI(actual: String, substrs: String*)(implicit q: Line): Unit =
    _assertContainsSet("assertNotContainsAnyCI", ci(actual), ci(substrs.toSet), ∃ = true, expect = false)

  def assertNotContainsAllCI(actual: String, substrs: String*)(implicit q: Line): Unit =
    _assertContainsSet("assertNotContainsAllCI", ci(actual), ci(substrs.toSet), ∃ = false, expect = false)

  private def _assertContainsSet(method: String, actual: String, substrs: Set[String], ∃ : Boolean, expect: Boolean)(implicit q: Line): Unit = {
    val result =
      if (∃)
        substrs.exists(actual.contains)
      else
        substrs.forall(actual.contains)
    if (result != expect) {
      def substrToLine(s: String) = {
        var l = s.replace("\n", "\\n")
        val limit = 100
        if (l.length > limit) l = l.take(limit) + "…"
        l
      }
      val substrDesc = substrs.map(substrToLine).toList.sorted.mkString("\n")
      printFail2(Some(method))(
        "substrs", _assertContainSubstrColour(expect), substrDesc)(
        "actual", BOLD_BRIGHT_RED, actual)
      fail(s"$method failed.")
    }
  }

  def assertChange[A, B: Equal, R](query: => A, block: => R)(actual: (A, A) => B)(expect: (A, R) => B)(implicit q: Line): R =
    assertChangeO(None, query, block)(actual)(expect)

  def assertChange[A, B: Equal, R](desc: => String, query: => A, block: => R)(actual: (A, A) => B)(expect: (A, R) => B)(implicit q: Line): R =
    assertChangeO(Some(desc), query, block)(actual)(expect)

  def assertChangeO[A, B: Equal, R](desc: => Option[String], query: => A, block: => R)(actual: (A, A) => B)(expect: (A, R) => B)(implicit q: Line): R = {
    val before = query
    val result = block
    val after  = query
    assertEqO(desc, actual(after, before), expect(before, result))
    result
  }

  def assertNoChange[B: Equal, A](query: => B)(block: => A)(implicit q: Line): A =
    assertNoChangeO(None, query)(block)

  def assertNoChange[B: Equal, A](desc: => String, query: => B)(block: => A)(implicit q: Line): A =
    assertNoChangeO(Some(desc), query)(block)

  def assertNoChangeO[B: Equal, A](desc: => Option[String], query: => B)(block: => A)(implicit q: Line): A =
    assertChangeO(desc, query, block)((b, _) => b)((b, _) => b)

  def assertDifference[N: Numeric : Equal, A](query: => N)(expect: N)(block: => A)(implicit q: Line): A =
    assertDifferenceO(None, query)(expect)(block)

  def assertDifference[N: Numeric : Equal, A](desc: => String, query: => N)(expect: N)(block: => A)(implicit q: Line): A =
    assertDifferenceO(Some(desc), query)(expect)(block)

  def assertDifferenceO[N: Numeric : Equal, A](desc: => Option[String], query: => N)(expect: N)(block: => A)(implicit q: Line): A =
    assertChangeO(desc, query, block)(implicitly[Numeric[N]].minus)((_, _) => expect)

  def assertEqWithTolerance(actual: Double, expect: Double)(implicit l: Line): Unit =
    _assertEqWithTolerance(None, actual, expect)

  def assertEqWithTolerance(name: => String, actual: Double, expect: Double)(implicit l: Line): Unit =
    _assertEqWithTolerance(Some(name), actual, expect)

  def assertEqWithTolerance(actual: Double, expect: Double, tolerance: Double)(implicit l: Line): Unit =
    _assertEqWithTolerance(None, actual, expect, tolerance)

  def assertEqWithTolerance(name: => String, actual: Double, expect: Double, tolerance: Double)(implicit l: Line): Unit =
    _assertEqWithTolerance(Some(name), actual, expect, tolerance)

  private def _assertEqWithTolerance(_name: => Option[String], actual: Double, expect: Double, tolerance: Double = 0.001)(implicit l: Line): Unit = {
    val d = Math.abs(actual - expect)
    if (d > tolerance) {
      val name = _name
      val titleSuffix = name.fold("")(n => s" ${BOLD_BRIGHT_YELLOW}$n$RESET")
      val errorPrefix = name.fold("")(n => s"[$n] ")
      println(
        s"""
           |${YELLOW_B}${BLACK}assertEqWithTolerance failed:$RESET$titleSuffix
           |${BOLD_BRIGHT_GREEN}expect: $expect$RESET
           |${BOLD_BRIGHT_RED}actual: $actual$RESET
           |${BOLD_BRIGHT_RED} delta: $d$RESET
           |$YELLOW   tol: $tolerance$RESET
           |""".stripMargin)
      fail(s"$errorPrefix$actual ≠ $expect by $d which exceeds tolerance of $tolerance")
    }
  }

}

trait TestUtil
  extends TestUtilWithoutUnivEq
     with japgolly.univeq.UnivEqExports
     with japgolly.univeq.UnivEqScalaz

object TestUtil extends TestUtil




© 2015 - 2025 Weber Informatics LLC | Privacy Policy