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

io.iteratee.testing.IterateeSuite.scala Maven / Gradle / Ivy

The newest version!
package io.iteratee.testing

import cats.{Eq, Eval, Monad, MonadError}
import cats.data.{EitherT, NonEmptyList}
import cats.laws.discipline.{ContravariantTests, MonadErrorTests, MonadTests, SemigroupalTests}
import io.iteratee.Iteratee
import io.iteratee.internal.Step
import io.iteratee.modules.{EnumerateeModule, EnumeratorModule, IterateeErrorModule, IterateeModule, Module}
import org.scalacheck.{Arbitrary, Cogen}
import scala.Predef._

abstract class IterateeSuite[F[_]: Monad] extends BaseIterateeSuite[F] {
  this: EnumerateeModule[F] with EnumeratorModule[F] with IterateeModule[F] with Module[F] =>

  checkLaws(
    s"Iteratee[$monadName, Vector[Int], Vector[Int]]",
    MonadTests[VectorIntFoldingIteratee].monad[Vector[Int], Vector[Int], Vector[Int]]
  )
}

abstract class IterateeErrorSuite[F[_], T: Arbitrary: Eq: Cogen](implicit
  MEF: MonadError[F, T]
) extends BaseIterateeSuite[F] {
  this: EnumerateeModule[F] with EnumeratorModule[F] with IterateeErrorModule[F, T] with Module[F] {
    type M[f[_]] <: MonadError[f, T]
  } =>

  implicit val monadError: MonadError[VectorIntFoldingIteratee, T] = Iteratee.iterateeMonadError[F, T, Vector[Int]]

  implicit val arbitraryVectorIntFoldingIteratee: Arbitrary[VectorIntFoldingIteratee[Vector[Int]]] =
    arbitraryVectorIteratee[F, Int]

  implicit val eqVectorIntIteratee: Eq[VectorIntFoldingIteratee[Vector[Int]]] =
    eqIteratee[F, Vector[Int], Vector[Int]]

  implicit val eqEitherUnitIteratee: Eq[VectorIntFoldingIteratee[Either[T, Unit]]] =
    eqIteratee[F, Vector[Int], Either[T, Unit]]

  implicit val eqEitherVectorIntIteratee: Eq[VectorIntFoldingIteratee[Either[T, Vector[Int]]]] =
    eqIteratee[F, Vector[Int], Either[T, Vector[Int]]]

  implicit val eqVectorInt3Iteratee: Eq[VectorIntFoldingIteratee[(Vector[Int], Vector[Int], Vector[Int])]] =
    eqIteratee[F, Vector[Int], (Vector[Int], Vector[Int], Vector[Int])]

  implicit val eqEitherTVectorInt: Eq[EitherT[({ type L[x] = Iteratee[F, Vector[Int], x] })#L, T, Vector[Int]]] =
    EitherT.catsDataEqForEitherT(eqEitherVectorIntIteratee)

  implicit val arbitraryVectorIntFunctionIteratee: Arbitrary[VectorIntFoldingIteratee[Vector[Int] => Vector[Int]]] =
    arbitraryFunctionIteratee[F, Vector[Int]]

  checkLaws(
    s"Iteratee[$monadName, Vector[Int], Vector[Int]]",
    MonadErrorTests[VectorIntFoldingIteratee, T].monadError[Vector[Int], Vector[Int], Vector[Int]]
  )

  "ensureEval" should "be executed when the iteratee is done" in forAll { (eav: EnumeratorAndValues[Int]) =>
    var done = false

    val iteratee = consume[Int].ensureEval(Eval.always(F.pure { done = true }))

    assert(!done)
    assert(eav.resultWithLeftovers(iteratee) === F.pure((eav.values, Vector.empty)))
    assert(done)
  }
}

abstract class BaseIterateeSuite[F[_]: Monad] extends ModuleSuite[F] {
  this: EnumerateeModule[F] with EnumeratorModule[F] with IterateeModule[F] with Module[F] =>

  implicit override val generatorDrivenConfig: PropertyCheckConfiguration = PropertyCheckConfiguration(
    minSize = 0,
    sizeRange = 5000
  )

  type VectorIntProducingIteratee[E] = Iteratee[F, E, Vector[Int]]
  type VectorIntFoldingIteratee[A] = Iteratee[F, Vector[Int], A]

  implicit val isomorphisms: SemigroupalTests.Isomorphisms[VectorIntFoldingIteratee] =
    SemigroupalTests.Isomorphisms.invariant[VectorIntFoldingIteratee]

  def myDrain(acc: List[Int]): Iteratee[F, Int, List[Int]] = cont[Int, List[Int]](
    els => myDrain(acc ::: els.toList),
    F.pure(acc)
  )

  checkLaws(
    s"Iteratee[$monadName, Int, Vector[Int]]",
    ContravariantTests[VectorIntProducingIteratee].contravariant[Vector[Int], Int, Vector[Int]]
  )

  checkLaws(
    s"Iteratee[$monadName, Int, Vector[Int]]",
    ContravariantTests[VectorIntProducingIteratee].invariant[Vector[Int], Int, Vector[Int]]
  )

  "cont" should "work recursively in an iteratee returning a list" in forAll { (eav: EnumeratorAndValues[Int]) =>
    assert(eav.enumerator.into(myDrain(Nil)) === F.map(eav.enumerator.toVector)(_.toList))
  }

  it should "work with fold with one value" in forAll { (es: List[Int]) =>
    val folded = myDrain(es).fold[F[List[Int]]](_(NonEmptyList(0, Nil)).run, (_, _) => F.pure(Nil))

    assert(F.flatten(folded) === F.pure(es :+ 0))
  }

  it should "work with fold with multiple values" in forAll { (es: List[Int]) =>
    val folded = myDrain(es).fold[F[List[Int]]](_(NonEmptyList(0, List(1, 2, 3))).run, (_, _) => F.pure(Nil))

    assert(F.flatten(folded) === F.pure(es ++ Vector(0, 1, 2, 3)))
  }

  "done" should "work correctly" in forAll { (eav: EnumeratorAndValues[Int], s: String) =>
    assert(eav.resultWithLeftovers(done(s)) === F.pure((s, eav.values)))
  }

  it should "work with fold with no leftovers" in forAll { (s: String) =>
    assert(done[Int, String](s).fold(_ => None, (v, r) => Some((v, r))) === F.pure(Some((s, Nil))))
  }

  "Step.doneWithLeftovers" should "do something vaguely reasonable with exactly one leftover" in {
    forAll { (eav: EnumeratorAndValues[Int], s: String, e: Int) =>
      val iteratee = Iteratee.fromStep(Step.doneWithLeftovers(s, List(e)))

      assert(eav.resultWithLeftovers(iteratee) === F.pure((s, e +: eav.values)))
    }
  }

  it should "do something vaguely reasonable with leftovers" in {
    forAll { (eav: EnumeratorAndValues[Int], s: String, es: List[Int]) =>
      val iteratee = Iteratee.fromStep(Step.doneWithLeftovers(s, es))

      assert(eav.resultWithLeftovers(iteratee) === F.pure((s, es.toVector ++ eav.values)))
    }
  }

  it should "do something vaguely reasonable with fold with leftovers" in forAll { (s: String, es: List[Int]) =>
    val iteratee = Iteratee.fromStep(Step.doneWithLeftovers[F, Int, String](s, es))

    assert(iteratee.fold(_ => None, (v, r) => Some((v, r))) === F.pure(Some((s, es))))
  }

  "liftToIteratee" should "lift a value in a context into an iteratee" in forAll { (i: Int) =>
    assert(liftToIteratee(F.pure(i)).run === F.pure(i))
  }

  it should "lift a value in a context when run on an enumerator" in forAll { (eav: EnumeratorAndValues[Int], i: Int) =>
    assert(eav.enumerator.into(liftToIteratee(F.pure(i))) === F.pure(i))
  }

  "liftMEval" should "lift a value in a context into an iteratee" in forAll { (i: Int) =>
    var counter = 0
    val eval = Eval.later {
      counter += i
      F.pure(i)
    }

    assert(counter === 0)
    assert(Iteratee.liftMEval(eval).run === F.pure(i))
    assert(counter === i)
  }

  it should "lift a value in a context when run on an enumerator" in forAll { (eav: EnumeratorAndValues[Int], i: Int) =>
    var counter = 0
    val eval = Eval.later {
      counter += i
      F.pure(i)
    }

    assert(counter === 0)
    assert(eav.enumerator.into(Iteratee.liftMEval(eval)) === F.pure(i))
    assert(counter === i)
  }

  "identityIteratee" should "consume no input" in forAll { (eav: EnumeratorAndValues[Int], it: Iteratee[F, Int, Int]) =>
    assert(eav.resultWithLeftovers(identityIteratee) === F.pure(((), eav.values)))
    assert(eav.resultWithLeftovers(identityIteratee.flatMap(_ => it)) === eav.resultWithLeftovers(it))
  }

  "consume" should "consume the entire stream" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val result = eav.resultWithLeftovers(consume)

    assert(result === F.pure((eav.values, Vector.empty)))
    assert(result === eav.resultWithLeftovers(identityIteratee.flatMap(_ => consume)))
  }

  "consumeIn" should "consume the entire stream" in forAll { (eav: EnumeratorAndValues[Int]) =>
    assert(eav.resultWithLeftovers(consumeIn[Int, List]) === F.pure((eav.values.toList, Vector.empty)))
  }

  "reversed" should "consume and reverse the stream" in forAll { (eav: EnumeratorAndValues[Int]) =>
    assert(eav.resultWithLeftovers(reversed) === F.pure((eav.values.toList.reverse, Vector.empty)))
  }

  "head" should "consume and return the first value" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val result = (eav.values.headOption, eav.values.drop(1))

    assert(eav.resultWithLeftovers(head[Int]) === F.pure(result))
  }

  "peek" should "consume the first value without consuming it" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val result = (eav.values.headOption, eav.values)

    assert(eav.resultWithLeftovers(peek[Int]) === F.pure(result))
  }

  "last" should "return the last value" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val result = (eav.values.lastOption, Vector.empty[Int])

    assert(eav.resultWithLeftovers(last[Int]) === F.pure(result))
  }

  "takeI" should "consume the specified number of values" in forAll { (eav: EnumeratorAndValues[Int], n: Int) =>
    /**
     * This isn't a comprehensive way to avoid SI-9581, but it seems to keep clear of the cases
     * ScalaCheck is likely to run into.
     */
    whenever(n != Int.MaxValue) {
      assert(eav.resultWithLeftovers(takeI[Int](n)) === F.pure((eav.values.take(n), eav.values.drop(n))))
    }
  }

  "takeWhileI" should "consume the specified values" in forAll { (eav: EnumeratorAndValues[Int], n: Int) =>
    assert(eav.resultWithLeftovers(takeWhileI(_ < n)) === F.pure(eav.values.span(_ < n)))
  }

  "dropI" should "drop the specified number of values" in forAll { (eav: EnumeratorAndValues[Int], n: Int) =>
    /**
     * This isn't a comprehensive way to avoid SI-9581, but it seems to keep clear of the cases
     * ScalaCheck is likely to run into.
     */
    whenever(n != Int.MaxValue) {
      assert(eav.resultWithLeftovers(dropI[Int](n)) === F.pure(((), eav.values.drop(n))))
    }
  }

  "dropWhileI" should "drop the specified values" in forAll { (eav: EnumeratorAndValues[Int], n: Int) =>
    assert(eav.resultWithLeftovers(dropWhileI(_ < n)) === F.pure(((), eav.values.dropWhile(_ < n))))
  }

  it should "drop the specified values with nothing left in chunk" in {
    val iteratee = for {
      _ <- dropWhileI[Int](_ < 100)
      r <- consume
    } yield r

    assert(enumVector(Vector(1, 2, 3)).into(iteratee) === F.pure(Vector.empty))
  }

  "fold" should "collapse the stream into a value" in forAll { (eav: EnumeratorAndValues[Int]) =>
    assert(eav.resultWithLeftovers(fold[Int, Int](0)(_ + _)) === F.pure((eav.values.sum, Vector.empty)))
  }

  "foldM" should "effectfully collapse the stream into a value" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val result = (eav.values.sum, Vector.empty)

    assert(eav.resultWithLeftovers(foldM[Int, Int](0)((acc, i) => F.pure(acc + i))) === F.pure(result))
  }

  "length" should "return the length of the stream" in forAll { (eav: EnumeratorAndValues[Int]) =>
    assert(eav.resultWithLeftovers(length) === F.pure((eav.values.size.toLong, Vector.empty)))
  }

  "sum" should "return the sum of a stream of integers" in forAll { (eav: EnumeratorAndValues[Int]) =>
    assert(eav.resultWithLeftovers(sum) === F.pure((eav.values.sum, Vector.empty)))
  }

  "isEnd" should "indicate whether a stream has ended" in forAll { (eav: EnumeratorAndValues[Int]) =>
    assert(eav.resultWithLeftovers(isEnd) === F.pure((eav.values.isEmpty, eav.values)))
    assert(eav.resultWithLeftovers(consume.flatMap(_ => isEnd)) === F.pure((true, Vector.empty)))
  }

  "foreach" should "perform an operation on all values in a stream" in forAll { (eav: EnumeratorAndValues[Int]) =>
    var total = 0
    val iteratee = foreach[Int](i => total += i)

    assert(eav.resultWithLeftovers(iteratee) === F.pure(((), Vector.empty)) && total === eav.values.sum)
  }

  "foreach" should "perform an operation on all values in a grouped stream" in {
    forAll { (eav: EnumeratorAndValues[Int]) =>
      val eavg = EnumeratorAndValues(eav.enumerator.grouped(3), eav.values.grouped(3).toVector)

      var total = 0
      val iteratee = foreach[Vector[Int]](is => total += is.sum)

      assert(eavg.resultWithLeftovers(iteratee) === F.pure(((), Vector.empty)) && total === eavg.values.flatten.sum)
    }
  }

  "foreachM" should "perform an effectful operation on all values in a stream" in {
    forAll { (eav: EnumeratorAndValues[Int]) =>
      var total = 0
      val iteratee = foreachM[Int](i => F.pure(total += i))

      assert(eav.resultWithLeftovers(iteratee) === F.pure(((), Vector.empty)) && total === eav.values.sum)
    }
  }

  "discard" should "throw away the result" in forAll { (eav: EnumeratorAndValues[Int]) =>
    var total = 0
    val iteratee = fold[Int, Int](0) { case (acc, i) =>
      total += i
      i
    }

    assert(eav.resultWithLeftovers(iteratee.discard) === F.pure(((), Vector.empty)))
    assert(total === eav.values.sum)
  }

  "apply" should "process the values in a stream" in forAll { (eav: EnumeratorAndValues[Int]) =>
    assert(consume.apply(eav.enumerator).apply(eav.enumerator).run === F.pure(eav.values ++ eav.values))
  }

  "flatMapM" should "apply an effectful function" in {
    forAll { (eav: EnumeratorAndValues[Int], iteratee: Iteratee[F, Int, Int]) =>
      assert(eav.enumerator.into(iteratee.flatMapM(F.pure)) === eav.enumerator.into(iteratee))
    }
  }

  "contramap" should "apply a function on incoming values" in {
    forAll { (eav: EnumeratorAndValues[Int], iteratee: Iteratee[F, Int, Int]) =>
      assert(eav.enumerator.into(iteratee.contramap(_ + 1)) === eav.enumerator.map(_ + 1).into(iteratee))
    }
  }

  "through" should "pipe incoming values through an enumeratee" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val result = (eav.values.sum + eav.values.size, Vector.empty)

    assert(eav.resultWithLeftovers(sum[Int].through(map(_ + 1))) === F.pure(result))
  }

  "as" should "replace the result" in {
    forAll { (eav: EnumeratorAndValues[Int], iteratee: Iteratee[F, Int, Int], value: Int) =>
      assert(eav.enumerator.into(iteratee.as(value)) === F.pure(value))
    }
  }

  "zip" should "zip two iteratees" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val result = ((eav.values.sum, eav.values.size.toLong), Vector.empty)

    assert(eav.resultWithLeftovers(sum[Int].zip(length)) === F.pure(result))
  }

  it should "zip two iteratees with leftovers (scalaz/scalaz#1068)" in {
    forAll { (eav: EnumeratorAndValues[Int], m: Int, n: Int) =>
      /**
       * This isn't a comprehensive way to avoid SI-9581, but it seems to keep clear of the cases
       * ScalaCheck is likely to run into.
       */
      whenever(m != Int.MaxValue && n != Int.MaxValue) {
        val result = ((eav.values.take(m), eav.values.take(n)), eav.values.drop(math.max(m, n)))

        assert(eav.resultWithLeftovers(takeI[Int](m).zip(takeI[Int](n))) === F.pure(result))
      }
    }
  }

  it should "zip two iteratees where leftover sizes must be compared" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val iteratee = takeI[Int](2).zip(takeI(3))
    val result = ((eav.values.take(2), eav.values.take(3)), eav.values.drop(3))

    assert(eav.resultWithLeftovers(iteratee) === F.pure(result))
  }

  it should "zip two iteratees where only one has leftovers" in forAll { (v: Vector[Int], n: Int) =>
    val taken = n % (v.size + 1)
    val enumerator = enumVector(v)
    val iteratee1 = takeI[Int](v.size).zip(takeI(taken)).flatMap(r => consume.map((r, _)))
    val iteratee2 = takeI[Int](taken).zip(takeI(v.size)).flatMap(r => consume.map((r, _)))

    val result1 = (v, v.take(taken))
    val result2 = (v.take(taken), v)

    assert(enumerator.into(iteratee1) === F.pure((result1, Vector.empty)))
    assert(enumerator.into(iteratee2) === F.pure((result2, Vector.empty)))
  }

  it should "zip two iteratees with single leftovers" in {
    val es = Vector(1, 2, 3, 4)
    val enumerator = enumVector(es)
    val iteratee1 = takeI[Int](2).zip(takeI(3)).zip(takeI(4))
    val iteratee2 = takeI[Int](2).zip(takeI(3)).zip(consume)
    val result = ((es.take(2), es.take(3)), es)

    assert(enumerator.into(iteratee1) === F.pure(result))
    assert(enumerator.into(iteratee2) === F.pure(result))
  }

  "foldMap" should "sum a stream while transforming it" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val result = F.pure((eav.values.sum + eav.values.size, Vector.empty[Int]))

    assert(eav.resultWithLeftovers(foldMap(_ + 1)) === result)
  }

  "foldMapM" should "sum a stream while transforming it" in forAll { (eav: EnumeratorAndValues[Int]) =>
    val result = F.pure((eav.values.sum + eav.values.size, Vector.empty[Int]))

    assert(eav.resultWithLeftovers(foldMapM(e => F.pure(e + 1))) === result)
  }

  "foldMapOption" should "sum a stream while transforming it" in
    forAll { (eav: EnumeratorAndValues[Int]) =>
      val result = F.pure(
        (
          if (eav.values.isEmpty) None else Some(eav.values.sum + eav.values.size),
          Vector.empty[Int]
        )
      )

      assert(eav.resultWithLeftovers(foldMapOption(_ + 1)) === result)
    }

  "foldMapMOption" should "sum a stream while transforming it" in
    forAll { (eav: EnumeratorAndValues[Int]) =>
      val result = F.pure(
        (
          if (eav.values.isEmpty) None else Some(eav.values.sum + eav.values.size),
          Vector.empty[Int]
        )
      )

      assert(eav.resultWithLeftovers(foldMapMOption(e => F.pure(e + 1))) === result)
    }

  "intoIteratee" should "be available on values in a context" in forAll { (i: Int) =>
    import syntax._

    assert(F.pure(i).intoIteratee.run === F.pure(i))
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy