Skip to content

Commit

Permalink
Close #259 - Add project to test cats related code
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin-lee committed Aug 7, 2021
1 parent 5cbf18a commit b5880e4
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 12 deletions.
47 changes: 35 additions & 12 deletions build.sbt
Expand Up @@ -40,7 +40,7 @@ lazy val effectie = (project in file("."))
libraryDependenciesPostProcess(isScala3_0(scalaVersion.value), libraryDependencies.value),
)
.settings(noPublish)
.aggregate(core, catsEffect, catsEffect3, monix, scalazEffect)
.aggregate(core, testing4Cats, catsEffect, catsEffect3, monix, scalazEffect)

lazy val core = projectCommonSettings("core", ProjectName("core"), file("core"))
.settings(
Expand All @@ -51,7 +51,20 @@ lazy val core = projectCommonSettings("core", ProjectName("core"), file("core"))
"""import effectie._""",
)

lazy val catsEffect = projectCommonSettings("catsEffect", ProjectName("cats-effect"), file("cats-effect"))
lazy val testing4Cats = projectCommonSettings("test4cats", ProjectName("test4cats"), file("test4cats"))
.settings(
description := "Effect's test utils for Cats",
libraryDependencies :=
libraryDependencies.value ++ List(
libs.libCatsCore(props.catsLatestVersion),
) ++ libs.hedgehogLibs
,
libraryDependencies := libraryDependenciesPostProcess(isScala3_0(scalaVersion.value), libraryDependencies.value),
console / initialCommands :=
"""import effectie.testing.cats._""",
)

lazy val catsEffect = projectCommonSettings("catsEffect", ProjectName("cats-effect"), file("cats-effect"))
.settings(
description := "Effect Utils - Cats Effect",
libraryDependencies :=
Expand Down Expand Up @@ -79,24 +92,29 @@ lazy val catsEffect = projectCommonSettings("catsEffect", ProjectName("cats-eff
console / initialCommands :=
"""import effectie.cats._""",
)
.dependsOn(core % props.IncludeTest)
.dependsOn(
core % props.IncludeTest,
testing4Cats % Test,
)

lazy val catsEffect3 = projectCommonSettings("catsEffect3", ProjectName("cats-effect3"), file("cats-effect3"))
.settings(
description := "Effect Utils - Cats Effect 3",
libraryDependencies ++= List(
libs.libCatsCore(props.catsLatestVersion),
libs.libCatsEffect(props.catsEffect3Version)
libs.libCatsEffect(props.catsEffect3Version),
libs.libCatsEffectTestKit % Test
),
libraryDependencies := libraryDependenciesPostProcess(isScala3_0(scalaVersion.value), libraryDependencies.value),
console / initialCommands :=
"""import effectie.cats._""",
)
.dependsOn(
core % props.IncludeTest,
core % props.IncludeTest,
testing4Cats % Test,
)

lazy val monix = projectCommonSettings("monix", ProjectName("monix"), file(s"${props.RepoName}-monix"))
lazy val monix = projectCommonSettings("monix", ProjectName("monix"), file(s"${props.RepoName}-monix"))
.settings(
description := "Effect Utils - Monix",
libraryDependencies :=
Expand All @@ -113,7 +131,10 @@ lazy val monix = projectCommonSettings("monix", ProjectName("monix"), fil
console / initialCommands :=
"""import effectie.monix._""",
)
.dependsOn(core % props.IncludeTest)
.dependsOn(
core % props.IncludeTest,
testing4Cats % Test,
)

lazy val scalazEffect = projectCommonSettings("scalazEffect", ProjectName("scalaz-effect"), file("scalaz-effect"))
.settings(
Expand Down Expand Up @@ -215,12 +236,12 @@ lazy val props =

lazy val libs =
new {
def hedgehogLibs(scalaVersion: String): List[ModuleID] = {
lazy val hedgehogLibs: List[ModuleID] = {
val hedgehogVersion = props.hedgehogLatestVersion
List(
"qa.hedgehog" %% "hedgehog-core" % hedgehogVersion % Test,
"qa.hedgehog" %% "hedgehog-runner" % hedgehogVersion % Test,
"qa.hedgehog" %% "hedgehog-sbt" % hedgehogVersion % Test,
"qa.hedgehog" %% "hedgehog-core" % hedgehogVersion,
"qa.hedgehog" %% "hedgehog-runner" % hedgehogVersion,
"qa.hedgehog" %% "hedgehog-sbt" % hedgehogVersion,
)
}

Expand All @@ -231,6 +252,8 @@ lazy val libs =

def libCatsEffect(catsEffectVersion: String): ModuleID = "org.typelevel" %% "cats-effect" % catsEffectVersion

lazy val libCatsEffectTestKit = "org.typelevel" %% "cats-effect-testkit" % props.catsEffect3Version

lazy val libCatsCore_2_0_0: ModuleID = "org.typelevel" %% "cats-core" % props.cats2_0_0Version
lazy val libCatsEffect_2_0_0: ModuleID = "org.typelevel" %% "cats-effect" % props.catsEffect2_0_0Version

Expand Down Expand Up @@ -258,7 +281,7 @@ def projectCommonSettings(id: String, projectName: ProjectName, file: File): Pro
.settings(
name := prefixedProjectName(projectName.projectName),
scalacOptions ~= (_.filterNot(props.isScala3IncompatibleScalacOption)),
libraryDependencies ++= libs.hedgehogLibs(scalaVersion.value),
libraryDependencies ++= libs.hedgehogLibs.map(_ % Test),
/* WartRemover and scalacOptions { */
// Compile / compile / wartremoverErrors ++= commonWarts((update / scalaBinaryVersion).value),
// Test / compile / wartremoverErrors ++= commonWarts((update / scalaBinaryVersion).value),
Expand Down
36 changes: 36 additions & 0 deletions test4cats/src/main/scala/effectie/testing/cats/Gens.scala
@@ -0,0 +1,36 @@
package effectie.testing.cats

import cats.Monad
import hedgehog.{Gen, Range}

/** @author Kevin Lee
* @since 2021-08-04
*/
object Gens {

def genInt(from: Int, to: Int): Gen[Int] =
Gen.int(Range.linear(from, to))

def genIntFromMinToMax: Gen[Int] = Gens.genInt(Int.MinValue, Int.MaxValue)

def genIntToInt: Gen[Int => Int] =
/* It has some hard-coded functions for now until Hedgehog has Gen[A => B]
* Reference: https://github.com/hedgehogqa/scala-hedgehog/issues/90
*/
Gen.element1[Int => Int](
identity[Int],
x => x + x,
x => x - x,
x => x * x,
x => x + 100,
x => x - 100,
x => x * 100
)

def genAToMonadA[F[_]: Monad, A](genF: Gen[A => A]): Gen[A => F[A]] =
genF.map(f => x => Monad[F].pure(f(x)))

def genFA[F[_]: Monad, A](genA: Gen[A]): Gen[F[A]] =
genA.map(a => Monad[F].pure(a))

}
134 changes: 134 additions & 0 deletions test4cats/src/main/scala/effectie/testing/cats/Laws.scala
@@ -0,0 +1,134 @@
package effectie.testing.cats

import cats.{Applicative, Eq, Functor, Monad}

/** @author Kevin Lee
* @since 2021-08-04
*/
object Laws {
trait FunctorLaws {
/* Functors must preserve identity morphisms
* fmap id = id
*/
def identity[F[_], A](fa: F[A])(
implicit F: Functor[F],
FA: Eq[F[A]]
): Boolean =
FA.eqv(
F.map(fa)(scala.Predef.identity),
fa
)

/* Functors preserve composition of morphisms
* fmap (f . g) == fmap f . fmap g
*/
def composition[F[_]: Functor, A, B, C](fa: F[A], f: B => C, g: A => B)(
implicit F: Functor[F],
FC: Eq[F[C]]
): Boolean =
FC.eqv(
F.map(fa)(f compose g),
F.map(F.map(fa)(g))(f)
)
}
object FunctorLaws extends FunctorLaws

trait ApplicativeLaws extends FunctorLaws {
/* Identity
* pure id <*> v = v
*/
def identityAp[F[_]: Applicative, A](fa: => F[A])(
implicit F: Functor[F],
FA: Eq[F[A]]
): Boolean =
FA.eqv(
Applicative[F].ap[A, A](Applicative[F].pure(scala.Predef.identity))(fa),
fa
)

/* Homomorphism
* pure f <*> pure x = pure (f x)
*/
def homomorphism[F[_]: Applicative, A, B](f: A => B, a: => A)(
implicit F: Functor[F],
FB: Eq[F[B]]
): Boolean =
FB.eqv(
Applicative[F].ap(Applicative[F].pure(f))(Applicative[F].pure(a)),
Applicative[F].pure(f(a))
)

/* Interchange
* u <*> pure y = pure ($ y) <*> u
*/
def interchange[F[_], A, B](a: => A, f: F[A => B])(
implicit F: Applicative[F],
FB: Eq[F[B]]
): Boolean =
FB.eqv(
F.ap[A, B](f)(F.pure(a)),
F.ap[A => B, B](F.pure(g => g(a)))(f)
)

/* Composition
* pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
*/
def compositionAp[F[_], A, B, C](fa: F[A], f: F[B => C], g: F[A => B])(
implicit F: Applicative[F],
FC: Eq[F[C]]
): Boolean =
FC.eqv(
F.ap[A, C](
F.ap[A => B, A => C](
F.ap[B => C, (A => B) => (A => C)](
F.pure(bc => ab => bc compose ab)
)(f)
)(g)
)(fa),
F.ap[B, C](f)(
F.ap[A, B](g)(fa)
)
)
}
object ApplicativeLaws extends ApplicativeLaws

trait MonadLaws extends ApplicativeLaws {
/*
* return a >>= f === f a
*/
def leftIdentity[F[_], A, B](a: A, f: A => F[B])(
implicit F: Monad[F],
FB: Eq[F[B]]
): Boolean =
FB.eqv(
F.flatMap(F.pure(a))(f),
f(a)
)

/*
* m >>= return === m
*/
def rightIdentity[F[_], A](fa: F[A])(
implicit F: Monad[F],
FA: Eq[F[A]]
): Boolean =
FA.eqv(
F.flatMap(fa)(F.pure(_: A)),
fa
)

/*
* (m >>= f) >>= g === m >>= (\x -> f x >>= g)
*/
def associativity[F[_], A, B, C](fa: F[A], f: A => F[B], g: B => F[C])(
implicit F: Monad[F],
FC: Eq[F[C]]
): Boolean =
FC.eqv(
F.flatMap(F.flatMap(fa)(f))(g),
F.flatMap(fa)(x => F.flatMap(f(x))(g))
)
}
object MonadLaws extends MonadLaws

}
19 changes: 19 additions & 0 deletions test4cats/src/main/scala/effectie/testing/cats/MonadSpec.scala
@@ -0,0 +1,19 @@
package effectie.testing.cats

import cats.{Eq, Monad}
import hedgehog._

/** @author Kevin Lee
* @since 2021-08-04
*/
object MonadSpec {
def testLaws[F[_]: Monad](implicit eqF: Eq[F[Int]]): Property =
Specs
.monadLaws
.laws[F](
Gens.genFA[F, Int](Gens.genInt(Int.MinValue, Int.MaxValue)),
Gens.genIntFromMinToMax,
Gens.genIntToInt,
Gens.genAToMonadA(Gens.genIntToInt)
)
}

0 comments on commit b5880e4

Please sign in to comment.