From 88babeeca3e926e885bc35ed6e9e82b254b693be Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Tue, 8 Dec 2015 08:32:11 -0500 Subject: [PATCH 01/10] Use SBT doctest plugin This helps ensure that our ScalaDoc examples actually compile and produce the expected result. --- build.sbt | 2 +- core/src/main/scala/cats/Foldable.scala | 30 ++++++++++++------- core/src/main/scala/cats/data/OptionT.scala | 6 ++-- core/src/main/scala/cats/data/Validated.scala | 13 ++++++-- core/src/main/scala/cats/data/Xor.scala | 6 ++-- core/src/main/scala/cats/data/XorT.scala | 21 ++++++++----- core/src/main/scala/cats/syntax/flatMap.scala | 8 ++++- project/plugins.sbt | 23 +++++++------- 8 files changed, 72 insertions(+), 37 deletions(-) diff --git a/build.sbt b/build.sbt index 600a3ff05b..e2187c24fe 100644 --- a/build.sbt +++ b/build.sbt @@ -34,7 +34,7 @@ lazy val commonSettings = Seq( compilerPlugin("org.spire-math" %% "kind-projector" % "0.6.3") ), parallelExecution in Test := false -) ++ warnUnusedImport +) ++ warnUnusedImport ++ doctestSettings lazy val commonJsSettings = Seq( scalaJSStage in Global := FastOptStage, diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index 5ea749a5e6..0db2e66d95 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -87,10 +87,15 @@ import simulacrum.typeclass * For example: * * {{{ - * def parseInt(s: String): Option[Int] = ... - * val F = Foldable[List] - * F.traverse_(List("333", "444"))(parseInt) // Some(()) - * F.traverse_(List("333", "zzz"))(parseInt) // None + * scala> import cats.data.Xor + * scala> import cats.std.list._ + * scala> import cats.std.option._ + * scala> def parseInt(s: String): Option[Int] = Xor.catchOnly[NumberFormatException](s.toInt).toOption + * scala> val F = Foldable[List] + * scala> F.traverse_(List("333", "444"))(parseInt) + * res0: Option[Unit] = Some(()) + * scala> F.traverse_(List("333", "zzz"))(parseInt) + * res1: Option[Unit] = None * }}} * * This method is primarily useful when `G[_]` represents an action @@ -111,9 +116,13 @@ import simulacrum.typeclass * For example: * * {{{ - * val F = Foldable[List] - * F.sequence_(List(Option(1), Option(2), Option(3))) // Some(()) - * F.sequence_(List(Option(1), None, Option(3))) // None + * scala> import cats.std.list._ + * scala> import cats.std.option._ + * scala> val F = Foldable[List] + * scala> F.sequence_(List(Option(1), Option(2), Option(3))) + * res0: Option[Unit] = Some(()) + * scala> F.sequence_(List(Option(1), None, Option(3))) + * res1: Option[Unit] = None * }}} */ def sequence_[G[_]: Applicative, A, B](fga: F[G[A]]): G[Unit] = @@ -128,9 +137,10 @@ import simulacrum.typeclass * For example: * * {{{ - * val F = Foldable[List] - * F.foldK(List(1 :: 2 :: Nil, 3 :: 4 :: 5 :: Nil)) - * // List(1, 2, 3, 4, 5) + * scala> import cats.std.list._ + * scala> val F = Foldable[List] + * scala> F.foldK(List(1 :: 2 :: Nil, 3 :: 4 :: 5 :: Nil)) + * res0: List[Int] = List(1, 2, 3, 4, 5) * }}} */ def foldK[G[_], A](fga: F[G[A]])(implicit G: MonoidK[G]): G[A] = diff --git a/core/src/main/scala/cats/data/OptionT.scala b/core/src/main/scala/cats/data/OptionT.scala index b683b57108..24972b49ab 100644 --- a/core/src/main/scala/cats/data/OptionT.scala +++ b/core/src/main/scala/cats/data/OptionT.scala @@ -108,8 +108,10 @@ object OptionT extends OptionTInstances { * Note: The return type is a FromOptionPartiallyApplied[F], which has an apply method * on it, allowing you to call fromOption like this: * {{{ - * val t: Option[Int] = ... - * val x: OptionT[List, Int] = fromOption[List](t) + * scala> import cats.std.list._ + * scala> val o: Option[Int] = Some(2) + * scala> OptionT.fromOption[List](o) + * res0: OptionT[List, Int] = OptionT(List(Some(2))) * }}} * * The reason for the indirection is to emulate currying type parameters. diff --git a/core/src/main/scala/cats/data/Validated.scala b/core/src/main/scala/cats/data/Validated.scala index 4c247ee98d..aa6cf41296 100644 --- a/core/src/main/scala/cats/data/Validated.scala +++ b/core/src/main/scala/cats/data/Validated.scala @@ -123,6 +123,12 @@ sealed abstract class Validated[+E, +A] extends Product with Serializable { */ def map[B](f: A => B): Validated[E,B] = bimap(identity, f) + /** + * Apply a function to an Invalid value, returning a new Invalid value. + * Or, if the original valid was Valid, return it. + */ + def leftMap[EE](f: E => EE): Validated[EE,A] = bimap(f, identity) + /** * When Valid, apply the function, marking the result as valid * inside the Applicative's context, @@ -211,6 +217,7 @@ private[data] sealed abstract class ValidatedInstances extends ValidatedInstance implicit def validatedBifunctor: Bifunctor[Validated] = new Bifunctor[Validated] { override def bimap[A, B, C, D](fab: Validated[A, B])(f: A => C, g: B => D): Validated[C, D] = fab.bimap(f, g) + override def leftMap[A, B, C](fab: Validated[A, B])(f: A => C): Validated[C, B] = fab.leftMap(f) } implicit def validatedInstances[E](implicit E: Semigroup[E]): Traverse[Validated[E, ?]] with Applicative[Validated[E, ?]] = @@ -267,8 +274,10 @@ trait ValidatedFunctions { * Evaluates the specified block, catching exceptions of the specified type and returning them on the invalid side of * the resulting `Validated`. Uncaught exceptions are propagated. * - * For example: {{{ - * val result: Validated[NumberFormatException, Int] = catchOnly[NumberFormatException] { "foo".toInt } + * For example: + * {{{ + * scala> Validated.catchOnly[NumberFormatException] { "foo".toInt } + * res0: Validated[NumberFormatException, Int] = Invalid(java.lang.NumberFormatException: For input string: "foo") * }}} */ def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T] = new CatchOnlyPartiallyApplied[T] diff --git a/core/src/main/scala/cats/data/Xor.scala b/core/src/main/scala/cats/data/Xor.scala index ac5892198b..4333775c30 100644 --- a/core/src/main/scala/cats/data/Xor.scala +++ b/core/src/main/scala/cats/data/Xor.scala @@ -222,8 +222,10 @@ trait XorFunctions { * Evaluates the specified block, catching exceptions of the specified type and returning them on the left side of * the resulting `Xor`. Uncaught exceptions are propagated. * - * For example: {{{ - * val result: NumberFormatException Xor Int = catchOnly[NumberFormatException] { "foo".toInt } + * For example: + * {{{ + * scala> Xor.catchOnly[NumberFormatException] { "foo".toInt } + * res0: Xor[NumberFormatException, Int] = Left(java.lang.NumberFormatException: For input string: "foo") * }}} */ def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T] = diff --git a/core/src/main/scala/cats/data/XorT.scala b/core/src/main/scala/cats/data/XorT.scala index 6bc5c4a23c..54145a8c00 100644 --- a/core/src/main/scala/cats/data/XorT.scala +++ b/core/src/main/scala/cats/data/XorT.scala @@ -120,12 +120,15 @@ final case class XorT[F[_], A, B](value: F[A Xor B]) { * * Example: * {{{ - * val v1: Validated[NonEmptyList[Error], Int] = ... - * val v2: Validated[NonEmptyList[Error], Int] = ... - * val xort: XorT[Error, Int] = ... - * - * val result: XorT[NonEmptyList[Error], Int] = - * xort.withValidated { v3 => (v1 |@| v2 |@| v3.leftMap(NonEmptyList(_))) { case (i, j, k) => i + j + k } } + * scala> import cats.std.option._ + * scala> import cats.std.list._ + * scala> import cats.syntax.apply._ + * scala> type Error = String + * scala> val v1: Validated[NonEmptyList[Error], Int] = Validated.Invalid(NonEmptyList("error 1")) + * scala> val v2: Validated[NonEmptyList[Error], Int] = Validated.Invalid(NonEmptyList("error 2")) + * scala> val xort: XorT[Option, Error, Int] = XorT(Some(Xor.left("error 3"))) + * scala> xort.withValidated { v3 => (v1 |@| v2 |@| v3.leftMap(NonEmptyList(_))).map{ case (i, j, k) => i + j + k } } + * res0: XorT[Option, NonEmptyList[Error], Int] = XorT(Some(Left(OneAnd(error 1,List(error 2, error 3))))) * }}} */ def withValidated[AA, BB](f: Validated[A, B] => Validated[AA, BB])(implicit F: Functor[F]): XorT[F, AA, BB] = @@ -148,8 +151,10 @@ trait XorTFunctions { * Note: The return type is a FromXorPartiallyApplied[F], which has an apply method * on it, allowing you to call fromXor like this: * {{{ - * val t: Xor[String, Int] = ... - * val x: XorT[Option, String, Int] = fromXor[Option](t) + * scala> import cats.std.option._ + * scala> val t: Xor[String, Int] = Xor.right(3) + * scala> XorT.fromXor[Option](t) + * res0: XorT[Option, String, Int] = XorT(Some(Right(3))) * }}} * * The reason for the indirection is to emulate currying type parameters. diff --git a/core/src/main/scala/cats/syntax/flatMap.scala b/core/src/main/scala/cats/syntax/flatMap.scala index ae444f4e15..7c8b278faa 100644 --- a/core/src/main/scala/cats/syntax/flatMap.scala +++ b/core/src/main/scala/cats/syntax/flatMap.scala @@ -34,7 +34,13 @@ final class FlatMapOps[F[_], A](fa: F[A])(implicit F: FlatMap[F]) { * you can evaluate it only ''after'' the first action has finished: * * {{{ - * fa.followedByEval(later(fb)) + * scala> import cats.Eval + * scala> import cats.std.option._ + * scala> import cats.syntax.flatMap._ + * scala> val fa: Option[Int] = Some(3) + * scala> def fb: Option[String] = Some("foo") + * scala> fa.followedByEval(Eval.later(fb)) + * res0: Option[String] = Some(foo) * }}} */ def followedByEval[B](fb: Eval[F[B]]): F[B] = F.flatMap(fa)(_ => fb.value) diff --git a/project/plugins.sbt b/project/plugins.sbt index a61f92053c..68e5161cca 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,14 +3,15 @@ resolvers += Resolver.url( url("http://dl.bintray.com/content/tpolecat/sbt-plugin-releases"))( Resolver.ivyStylePatterns) -addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.3.2") -addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") -addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.3") -addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.8.1") -addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.0") -addSbtPlugin("pl.project13.scala"% "sbt-jmh" % "0.2.3") -addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.6.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.2.0") -addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.8.4") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5") +addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.3.2") +addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.3") +addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.8.1") +addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.0") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.3") +addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.6.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.2.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.8.4") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5") +addSbtPlugin("com.github.tkawachi" % "sbt-doctest" % "0.3.5") From d1186b1feec786ac8583438a1c04d1a9a4c76712 Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Tue, 8 Dec 2015 09:30:36 -0500 Subject: [PATCH 02/10] Don't generate JS tests for sbt-doctest This was causing some build failures. Also use an explicit dependency on our version of scalacheck instead of letting sbt-doctest bring in its own version. --- build.sbt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index e2187c24fe..f716c96070 100644 --- a/build.sbt +++ b/build.sbt @@ -18,6 +18,10 @@ lazy val buildSettings = Seq( crossScalaVersions := Seq("2.10.5", "2.11.7") ) +lazy val catsDoctestSettings = Seq( + doctestWithDependencies := false +) ++ doctestSettings + lazy val commonSettings = Seq( scalacOptions ++= commonScalacOptions, resolvers ++= Seq( @@ -34,7 +38,7 @@ lazy val commonSettings = Seq( compilerPlugin("org.spire-math" %% "kind-projector" % "0.6.3") ), parallelExecution in Test := false -) ++ warnUnusedImport ++ doctestSettings +) ++ warnUnusedImport lazy val commonJsSettings = Seq( scalaJSStage in Global := FastOptStage, @@ -43,12 +47,16 @@ lazy val commonJsSettings = Seq( lazy val commonJvmSettings = Seq( testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oDF") -) +// currently sbt-doctest is only running on the JVM, because I was running into +// some issues in the generated JS tests. +) ++ catsDoctestSettings lazy val catsSettings = buildSettings ++ commonSettings ++ publishSettings ++ scoverageSettings +lazy val scalacheckVersion = "1.12.5" + lazy val disciplineDependencies = Seq( - libraryDependencies += "org.scalacheck" %%% "scalacheck" % "1.12.5", + libraryDependencies += "org.scalacheck" %%% "scalacheck" % scalacheckVersion, libraryDependencies += "org.typelevel" %%% "discipline" % "0.4" ) @@ -122,6 +130,7 @@ lazy val core = crossProject.crossType(CrossType.Pure) .settings( sourceGenerators in Compile <+= (sourceManaged in Compile).map(Boilerplate.gen) ) + .settings(libraryDependencies += "org.scalacheck" %%% "scalacheck" % scalacheckVersion % "test") .jsSettings(commonJsSettings:_*) .jvmSettings(commonJvmSettings:_*) From fbf562f3be82b0c21fbf5e48a3d1663221d548c4 Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Tue, 8 Dec 2015 10:14:29 -0500 Subject: [PATCH 03/10] Reduce depth of arbitrary `Free` instances The build has been hanging during tests in the `free` module recently, and I suspect this may be the cause. --- free/src/test/scala/cats/free/FreeTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/free/src/test/scala/cats/free/FreeTests.scala b/free/src/test/scala/cats/free/FreeTests.scala index fb330a5a5a..bd00a5e131 100644 --- a/free/src/test/scala/cats/free/FreeTests.scala +++ b/free/src/test/scala/cats/free/FreeTests.scala @@ -73,7 +73,7 @@ sealed trait FreeTestsInstances { } implicit def freeArbitrary[F[_], A](implicit F: Arbitrary[F[A]], A: Arbitrary[A]): Arbitrary[Free[F, A]] = - Arbitrary(freeGen[F, A](6)) + Arbitrary(freeGen[F, A](4)) implicit def freeEq[S[_]: Monad, A](implicit SA: Eq[S[A]]): Eq[Free[S, A]] = new Eq[Free[S, A]] { From aaca73c36e819856a9b342f9dae5c0d8bd2ac9df Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Tue, 8 Dec 2015 10:26:36 -0500 Subject: [PATCH 04/10] Add note about doctest not working in JS builds --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index f716c96070..1ddc91a35d 100644 --- a/build.sbt +++ b/build.sbt @@ -47,8 +47,8 @@ lazy val commonJsSettings = Seq( lazy val commonJvmSettings = Seq( testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oDF") -// currently sbt-doctest is only running on the JVM, because I was running into -// some issues in the generated JS tests. +// currently sbt-doctest doesn't work in JS builds, so this has to go in the +// JVM settings. https://github.com/tkawachi/sbt-doctest/issues/52 ) ++ catsDoctestSettings lazy val catsSettings = buildSettings ++ commonSettings ++ publishSettings ++ scoverageSettings From 43a1398a4c356003bb9f8d01735e31296174deb4 Mon Sep 17 00:00:00 2001 From: "Frank S. Thomas" Date: Tue, 8 Dec 2015 20:37:10 +0100 Subject: [PATCH 05/10] Update to scalastyle 0.8.0 --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a61f92053c..a878ce7732 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -10,7 +10,7 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.3") addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.8.1") addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.0") addSbtPlugin("pl.project13.scala"% "sbt-jmh" % "0.2.3") -addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.6.0") +addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.2.0") addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.8.4") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5") From 1ae5d583d935d6d8bd7b2cce1d39f6a181a05ac6 Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Tue, 8 Dec 2015 19:01:29 -0500 Subject: [PATCH 06/10] Add coreJVM/test to the buildJVM alias The sbt-doctest plugin generates tests within the `core` module. --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 1ddc91a35d..f043cfffbb 100644 --- a/build.sbt +++ b/build.sbt @@ -227,7 +227,7 @@ lazy val publishSettings = Seq( ) ++ credentialSettings ++ sharedPublishSettings ++ sharedReleaseProcess // These aliases serialise the build for the benefit of Travis-CI. -addCommandAlias("buildJVM", ";macrosJVM/compile;coreJVM/compile;freeJVM/compile;freeJVM/test;stateJVM/compile;stateJVM/test;lawsJVM/compile;testsJVM/test;jvm/test;bench/test") +addCommandAlias("buildJVM", ";macrosJVM/compile;coreJVM/compile;coreJVM/test;freeJVM/compile;freeJVM/test;stateJVM/compile;stateJVM/test;lawsJVM/compile;testsJVM/test;jvm/test;bench/test") addCommandAlias("validateJVM", ";scalastyle;buildJVM;makeSite") From f7a22b53dea0f033021f15cea3d1283430bd745e Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Tue, 8 Dec 2015 20:55:06 -0500 Subject: [PATCH 07/10] Add MonadFilter consistency to MonadFilter tests A `monadFilterConsistency` law was added in 8f7a110d536c087c8b70032103fb5a51fab5f34f but wasn't hooked up in the tests. This might be better accomplished with something like #370, but in the interim I think we should at least hook it up. --- .../src/main/scala/cats/laws/discipline/MonadFilterTests.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/laws/src/main/scala/cats/laws/discipline/MonadFilterTests.scala b/laws/src/main/scala/cats/laws/discipline/MonadFilterTests.scala index d9948843fd..65ad0acf1c 100644 --- a/laws/src/main/scala/cats/laws/discipline/MonadFilterTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/MonadFilterTests.scala @@ -23,7 +23,8 @@ trait MonadFilterTests[F[_]] extends MonadTests[F] { name = "monadFilter", parent = Some(monad[A, B, C]), "monadFilter left empty" -> forAll(laws.monadFilterLeftEmpty[A, B] _), - "monadFilter right empty" -> forAll(laws.monadFilterRightEmpty[A, B] _)) + "monadFilter right empty" -> forAll(laws.monadFilterRightEmpty[A, B] _), + "monadFilter consistency" -> forAll(laws.monadFilterConsistency[A, B] _)) } } From a21ede97de20a26a4fc184f48cb3c9c535f14762 Mon Sep 17 00:00:00 2001 From: "Frank S. Thomas" Date: Wed, 9 Dec 2015 20:54:39 +0100 Subject: [PATCH 08/10] Test associativity of (Co)Kleisli composition closes #732 --- .../test/scala/cats/tests/OptionTests.scala | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/src/test/scala/cats/tests/OptionTests.scala b/tests/src/test/scala/cats/tests/OptionTests.scala index 72702002ad..2799ab4ea2 100644 --- a/tests/src/test/scala/cats/tests/OptionTests.scala +++ b/tests/src/test/scala/cats/tests/OptionTests.scala @@ -1,6 +1,7 @@ package cats package tests +import cats.laws.{CoflatMapLaws, FlatMapLaws} import cats.laws.discipline.{TraverseTests, CoflatMapTests, MonadCombineTests, SerializableTests} class OptionTests extends CatsSuite { @@ -21,4 +22,30 @@ class OptionTests extends CatsSuite { fs.show should === (fs.toString) } } + + // The following two tests check the kleisliAssociativity and + // cokleisliAssociativity laws which are a different formulation of + // the flatMapAssociativity and coflatMapAssociativity laws. Since + // these laws are more or less duplicates of existing laws, we don't + // check them for all types that have FlatMap or CoflatMap instances. + + test("Kleisli associativity") { + forAll { (l: Long, + f: Long => Option[Int], + g: Int => Option[Char], + h: Char => Option[String]) => + val isEq = FlatMapLaws[Option].kleisliAssociativity(f, g, h, l) + isEq.lhs should === (isEq.rhs) + } + } + + test("Cokleisli associativity") { + forAll { (l: Option[Long], + f: Option[Long] => Int, + g: Option[Int] => Char, + h: Option[Char] => String) => + val isEq = CoflatMapLaws[Option].cokleisliAssociativity(f, g, h, l) + isEq.lhs should === (isEq.rhs) + } + } } From 5231d730bb9be3231a34f7038688bda4394ff2b0 Mon Sep 17 00:00:00 2001 From: Mike Curry Date: Wed, 9 Dec 2015 21:21:42 +0000 Subject: [PATCH 09/10] Add some notes on conventions in test writing --- CONTRIBUTING.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfdc7ca9c4..5b557d7e9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,10 +114,27 @@ Write about https://github.com/non/cats/pull/36#issuecomment-72892359 ### Write tests -Tests go into the tests module, under the `cats.tests` package. Cats tests -should extend `CatsSuite`. `CatsSuite` integrates ScalaTest with Discipline -for law checking, and imports all syntax and standard instances for -convenience. +- Tests for cats-core go into the tests module, under the `cats.tests` package. +- Tests for additional modules, such as `free`, go into the tests directory within that module. +- Cats tests should extend `CatsSuite`. `CatsSuite` integrates [ScalaTest](http://www.scalatest.org/) +with [Discipline](https://github.com/typelevel/discipline) for law checking, and imports all syntax and standard instances for convenience. +- The first parameter to the `checkAll` method provided by + [Discipline](https://github.com/typelevel/discipline), is the name of the test and will be output to the + console as part of the test execution. By convention: + - When checking laws, this parameter generally takes a form that describes the data type being tested. + For example the name *"Validated[String, Int]"* might be used when testing a type class instance + that the `Validated` data type supports. + - An exception to this is serializability tests, where the type class name is also included in the name. + For example, in the case of `Validated`, the serializability test would take the form, + *"Applicative[Validated[String, Int]"*, to indicate that this test is verifying that the `Applicative` + type class instance for the `Validated` data type is serializable. + - This convention helps to ensure clear and easy to understand output, with minimal duplication in the output. +- It is also a goal that, for every combination of data type and supported type class instance: + - Appropriate law checks for that combination are included to ensure that the instance meets the laws for that type class. + - A serializability test for that combination is also included, such that we know that frameworks which + rely heavily on serialization, such as `Spark`, will have strong compatibility with `cats`. + - Note that custom serialization tests are not required for instances of type classes which come from + `algebra`, such as `Monoid`, because the `algebra` laws include a test for serialization. TODO From 8788d9d4ce775b2ee7f882efa43611da4d26233d Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Wed, 9 Dec 2015 21:22:15 -0500 Subject: [PATCH 10/10] Test Monad map/flatMap coherence law This is basically the same story as #728, but it was introduced in fa6457a61b527736d88a5192e9fae060db2b3ffe. --- laws/src/main/scala/cats/laws/discipline/MonadTests.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/laws/src/main/scala/cats/laws/discipline/MonadTests.scala b/laws/src/main/scala/cats/laws/discipline/MonadTests.scala index 3bf1b54788..e739f22aee 100644 --- a/laws/src/main/scala/cats/laws/discipline/MonadTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/MonadTests.scala @@ -25,7 +25,8 @@ trait MonadTests[F[_]] extends ApplicativeTests[F] with FlatMapTests[F] { def parents: Seq[RuleSet] = Seq(applicative[A, B, C], flatMap[A, B, C]) def props: Seq[(String, Prop)] = Seq( "monad left identity" -> forAll(laws.monadLeftIdentity[A, B] _), - "monad right identity" -> forAll(laws.monadRightIdentity[A] _) + "monad right identity" -> forAll(laws.monadRightIdentity[A] _), + "map flatMap coherence" -> forAll(laws.mapFlatMapCoherence[A, B] _) ) } }