Skip to content

Commit

Permalink
use ScalaCheck to drive the Pagination tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bpholt committed Sep 26, 2024
1 parent 8f38e5b commit 5c291d9
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 21 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ lazy val `fs2-utils` = crossProject(JSPlatform, JVMPlatform)
"co.fs2" %%% "fs2-core" % fs2Version,
"org.scalameta" %%% "munit" % "1.0.2" % Test,
"org.typelevel" %%% "munit-cats-effect" % "2.0.0" % Test,
"org.typelevel" %% "scalacheck-effect-munit" % "2.0.0-M2" % Test,
),
)

Expand Down
69 changes: 48 additions & 21 deletions core/shared/src/test/scala/com/dwolla/fs2utils/PaginationSpec.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.dwolla.fs2utils

import cats.*
import cats.arrow.FunctionK
import cats.effect.IO
import cats.syntax.all.*
import com.dwolla.fs2utils.ArbitraryEffect.genArbitraryEffect
import fs2.*
import munit.*
import org.scalacheck.effect.PropF
import org.scalacheck.{Arbitrary, Gen}

class PaginationSpec extends CatsEffectSuite {
private def unfoldFunction[F[_] : Applicative](nextToken: Option[Long]): F[(Chunk[Long], Option[Long])] = {
Expand All @@ -14,38 +18,61 @@ class PaginationSpec extends CatsEffectSuite {
(Chunk.from(ints), if (offset < 3) Option(offset + 1) else None).pure[F]
}

test("Pagination should unfold the given thing in a pure stream") {
val stream = Pagination.offsetUnfoldChunkEval(unfoldFunction[Id]).take(5)
test("Pagination should unfold the given thing up to an arbitrary take limit") {
PropF.forAllF(genArbitraryEffect, Gen.choose(0, 12)) { (effect: ArbitraryEffect, takeLimit: Int) =>
type F[a] = effect.F[a]
implicit val ef: ArbitraryEffect.Aux[F] = effect

assertEquals(stream.compile.toList, 0 until 5)
val stream = Pagination.offsetUnfoldChunkEval(unfoldFunction[F]).take(takeLimit.toLong)
effect.toIO(stream.compile.toList.map(assertEquals(_, 0 until takeLimit)))
}
}

test("Pagination should unfold the given thing in a pure stream") {
val stream = Pagination.offsetUnfoldChunkEval(unfoldFunction[Id]).take(5)
test("Pagination should stop unfolding when None is returned as the token value") {
PropF.forAllF { (effect: ArbitraryEffect) =>
type F[a] = effect.F[a]
implicit val ef: ArbitraryEffect.Aux[F] = effect

assertEquals(stream.compile.toList, 0 until 5)
val stream = Pagination.offsetUnfoldChunkEval(unfoldFunction[F])
effect.toIO(stream.compile.toList.map(assertEquals(_, 0 until 12)))
}
}

test("Pagination should stop unfolding when None is returned as the token value in a pure stream") {
val stream = Pagination.offsetUnfoldChunkEval(unfoldFunction[Id])

assertEquals(stream.compile.toList, 0 until 12)
private implicit def compareListAndRange[A: Integral]: Compare[List[A], Range] = new Compare[List[A], Range] {
override def isEqual(obtained: List[A], expected: Range): Boolean =
obtained.map(implicitly[Integral[A]].toInt) == expected.toList
}
}

test("Pagination should unfold the given thing in an effectual stream") {
val stream = Pagination.offsetUnfoldChunkEval(unfoldFunction[IO]).take(5)

stream.compile.toList.map(assertEquals(_, 0 until 5))
}
sealed trait ArbitraryEffect {
type F[_]
val applicative: Applicative[F]
val compiler: Compiler[F, F]
val toIO: F ~> IO
}

test("Pagination should stop unfolding when None is returned as the token value in an effectual stream") {
val stream = Pagination.offsetUnfoldChunkEval(unfoldFunction[IO])
object ArbitraryEffect {
type Aux[FF[_]] = ArbitraryEffect { type F[a] = FF[a] }

stream.compile.toList.map(assertEquals(_, 0 until 12))
val idInstance: ArbitraryEffect.Aux[Id] = new ArbitraryEffect {
override type F[a] = Id[a]
override val applicative: Applicative[F] = implicitly
override val compiler: Compiler[F, F] = implicitly
override val toIO: Id ~> IO = new (Id ~> IO) {
override def apply[A](fa: Id[A]): IO[A] = IO.pure(fa)
}
}

private implicit def compareListAndRange[A: Integral]: Compare[List[A], Range] = new Compare[List[A], Range] {
override def isEqual(obtained: List[A], expected: Range): Boolean =
obtained.map(implicitly[Integral[A]].toInt) == expected.toList
val ioInstance: ArbitraryEffect.Aux[IO] = new ArbitraryEffect {
override type F[a] = IO[a]
override val applicative: Applicative[F] = implicitly
override val compiler: Compiler[F, F] = implicitly
override val toIO: IO ~> IO = FunctionK.id
}

val genArbitraryEffect: Gen[ArbitraryEffect] = Gen.oneOf(idInstance, ioInstance)
implicit val arbitraryEffect: Arbitrary[ArbitraryEffect] = Arbitrary(genArbitraryEffect)

implicit def applicativeFromEffectHolder[F[_]](implicit effect: ArbitraryEffect.Aux[F]): Applicative[F] = effect.applicative
implicit def streamCompilerFromEffectHolder[F[_]](implicit effect: ArbitraryEffect.Aux[F]): Compiler[F, F] = effect.compiler
}

0 comments on commit 5c291d9

Please sign in to comment.