From 5c291d95a3c1e203eb6d9b35879a0c9ce373f208 Mon Sep 17 00:00:00 2001 From: Brian Holt Date: Thu, 26 Sep 2024 12:23:47 -0500 Subject: [PATCH] use ScalaCheck to drive the Pagination tests --- build.sbt | 1 + .../com/dwolla/fs2utils/PaginationSpec.scala | 69 +++++++++++++------ 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/build.sbt b/build.sbt index c62a23d..269e355 100644 --- a/build.sbt +++ b/build.sbt @@ -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, ), ) diff --git a/core/shared/src/test/scala/com/dwolla/fs2utils/PaginationSpec.scala b/core/shared/src/test/scala/com/dwolla/fs2utils/PaginationSpec.scala index 5a38293..b0767a4 100644 --- a/core/shared/src/test/scala/com/dwolla/fs2utils/PaginationSpec.scala +++ b/core/shared/src/test/scala/com/dwolla/fs2utils/PaginationSpec.scala @@ -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])] = { @@ -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 }