diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4748241678..3e74f8d04a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,22 +20,33 @@ jobs: build: name: Build and Test strategy: + fail-fast: false matrix: os: [ubuntu-latest] - scala: [2.12.12, 2.13.4, 3.0.0-M2, 3.0.0-M3] + scala: [2.12.13, 2.13.4, 3.0.0-M2, 3.0.0-M3] java: - adopt@1.8 - adopt@1.11 - adopt@1.15 - graalvm-ce-java8@20.2.0 - platform: [jvm, js] + platform: [jvm, js, native] exclude: - platform: js java: adopt@1.11 + - platform: native + java: adopt@1.11 - platform: js java: adopt@1.15 + - platform: native + java: adopt@1.15 - platform: js java: graalvm-ce-java8@20.2.0 + - platform: native + java: graalvm-ce-java8@20.2.0 + - platform: native + scala: 3.0.0-M2 + - platform: native + scala: 3.0.0-M3 runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (full) @@ -67,6 +78,10 @@ jobs: if: matrix.platform == 'js' run: sbt ++${{ matrix.scala }} validateAllJS + - name: Validate Scala Native + if: matrix.platform == 'native' + run: sbt ++${{ matrix.scala }} validateAllNative + - name: Validate JVM (scala 2) if: matrix.platform == 'jvm' && (matrix.scala != '3.0.0-M2' && matrix.scala != '3.0.0-M3') run: sbt ++${{ matrix.scala }} buildJVM bench/test @@ -84,7 +99,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.12, 2.13.4, 3.0.0-M2, 3.0.0-M3] + scala: [2.12.13, 2.13.4, 3.0.0-M2, 3.0.0-M3] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: @@ -120,7 +135,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.12, 2.13.4, 3.0.0-M2, 3.0.0-M3] + scala: [2.12.13, 2.13.4, 3.0.0-M2, 3.0.0-M3] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: @@ -155,7 +170,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.12] + scala: [2.12.13] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: diff --git a/README.md b/README.md index e3995c3eb3..1323e35397 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Support this project with your organization. Your logo will show up here with a ### Getting Started -Cats is currently available for Scala 2.10 (up to 1.2.x), 2.11, 2.12, 2.13, and [Scala.js](http://www.scala-js.org/). +Cats is currently available for Scala 2.10 (up to 1.2.x), 2.11, 2.12, 2.13, [Scala.js](http://www.scala-js.org/), and [Scala Native](https://www.scala-native.org/). Cats relies on improved type inference via the fix for [SI-2712](https://github.com/scala/bug/issues/2712), which is not enabled by default. For **Scala 2.11.9+ or 2.12** you should add the following to your `build.sbt`: diff --git a/alleycats-tests/native/src/test/scala/alleycats/tests/TestSettings.scala b/alleycats-tests/native/src/test/scala/alleycats/tests/TestSettings.scala new file mode 100644 index 0000000000..423ff01760 --- /dev/null +++ b/alleycats-tests/native/src/test/scala/alleycats/tests/TestSettings.scala @@ -0,0 +1,14 @@ +package alleycats.tests + +import org.scalacheck.Test.Parameters + +trait TestSettings { + + lazy val checkConfiguration: Parameters = + Parameters.default + .withMinSuccessfulTests(50) + .withMaxDiscardRatio(5.0f) + .withMaxSize(10) + .withMinSize(0) + .withWorkers(1) +} diff --git a/build.sbt b/build.sbt index 4c0cfe211d..e3f462307c 100644 --- a/build.sbt +++ b/build.sbt @@ -35,7 +35,7 @@ val GraalVM8 = "graalvm-ce-java8@20.2.0" ThisBuild / githubWorkflowJavaVersions := Seq(PrimaryJava, LTSJava, LatestJava, GraalVM8) -val Scala212 = "2.12.12" +val Scala212 = "2.12.13" val Scala213 = "2.13.4" val DottyOld = "3.0.0-M2" val DottyNew = "3.0.0-M3" @@ -46,24 +46,34 @@ ThisBuild / scalaVersion := Scala213 ThisBuild / githubWorkflowPublishTargetBranches := Seq() // disable publication for now ThisBuild / githubWorkflowBuildMatrixAdditions += - "platform" -> List("jvm", "js") + "platform" -> List("jvm", "js", "native") ThisBuild / githubWorkflowBuildMatrixExclusions ++= - githubWorkflowJavaVersions.value.filterNot(Set(PrimaryJava)).map { java => - MatrixExclude(Map("platform" -> "js", "java" -> java)) + githubWorkflowJavaVersions.value.filterNot(Set(PrimaryJava)).flatMap { java => + Seq(MatrixExclude(Map("platform" -> "js", "java" -> java)), + MatrixExclude(Map("platform" -> "native", "java" -> java)) + ) } +ThisBuild / githubWorkflowBuildMatrixExclusions ++= Seq(DottyOld, DottyNew).map { dottyVersion => + MatrixExclude(Map("platform" -> "native", "scala" -> dottyVersion)) +} // Dotty is not yet supported by Scala Native + // we don't need this since we aren't publishing ThisBuild / githubWorkflowArtifactUpload := false +ThisBuild / githubWorkflowBuildMatrixFailFast := Some(false) + val JvmCond = s"matrix.platform == 'jvm'" val JsCond = s"matrix.platform == 'js'" +val NativeCond = s"matrix.platform == 'native'" val Scala2Cond = s"(matrix.scala != '$DottyOld' && matrix.scala != '$DottyNew')" val Scala3Cond = s"(matrix.scala == '$DottyOld' || matrix.scala == '$DottyNew')" ThisBuild / githubWorkflowBuild := Seq( WorkflowStep.Sbt(List("validateAllJS"), name = Some("Validate JavaScript"), cond = Some(JsCond)), + WorkflowStep.Sbt(List("validateAllNative"), name = Some("Validate Scala Native"), cond = Some(NativeCond)), WorkflowStep.Sbt(List("buildJVM", "bench/test"), name = Some("Validate JVM (scala 2)"), cond = Some(JvmCond + " && " + Scala2Cond) @@ -196,6 +206,14 @@ lazy val commonJsSettings = Seq( doctestGenTests := Seq.empty ) +lazy val commonNativeSettings = Seq( + // currently sbt-doctest doesn't work in Native/JS builds + // https://github.com/tkawachi/sbt-doctest/issues/52 + doctestGenTests := Seq.empty, + // Currently scala-native does not support Dotty + crossScalaVersions := { crossScalaVersions.value.filterNot(Seq(DottyOld, DottyNew).contains) } +) + lazy val commonJvmSettings = Seq( testOptions in Test += { val flag = if (githubIsWorkflowBuild.value) "-oCI" else "-oDF" @@ -494,8 +512,8 @@ lazy val cats = project .settings(moduleName := "root") .settings(publishSettings) // these settings are needed to release all aggregated modules under this root module .settings(noPublishSettings) // this is to exclude the root module itself from being published. - .aggregate(catsJVM, catsJS) - .dependsOn(catsJVM, catsJS, tests.jvm % "test-internal -> test") + .aggregate(catsJVM, catsJS, catsNative) + .dependsOn(catsJVM, catsJS, catsNative, tests.jvm % "test-internal -> test") lazy val catsJVM = project .in(file(".catsJVM")) @@ -562,7 +580,41 @@ lazy val catsJS = project ) .enablePlugins(ScalaJSPlugin) -lazy val kernel = crossProject(JSPlatform, JVMPlatform) +lazy val catsNative = project + .in(file(".catsNative")) + .settings(moduleName := "cats") + .settings(noPublishSettings) + .settings(catsSettings) + .settings(commonNativeSettings) + .aggregate( + kernel.native, + kernelLaws.native, + core.native, + laws.native, + free.native, + testkit.native, + tests.native, + alleycatsCore.native, + alleycatsLaws.native, + alleycatsTests.native, + native + ) + .dependsOn( + kernel.native, + kernelLaws.native, + core.native, + laws.native, + free.native, + testkit.native, + tests.native % "test-internal -> test", + alleycatsCore.native, + alleycatsLaws.native, + alleycatsTests.native % "test-internal -> test", + native + ) + .enablePlugins(ScalaNativePlugin) + +lazy val kernel = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("kernel")) .settings(moduleName := "cats-kernel", name := "Cats kernel") @@ -572,11 +624,13 @@ lazy val kernel = crossProject(JSPlatform, JVMPlatform) .settings(includeGeneratedSrc) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings ++ mimaSettings("cats-kernel")) + .nativeSettings(commonNativeSettings) + .settings(testingDependencies) .settings( libraryDependencies += "org.scalacheck" %%% "scalacheck" % scalaCheckVersion % Test ) -lazy val kernelLaws = crossProject(JSPlatform, JVMPlatform) +lazy val kernelLaws = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("kernel-laws")) .settings(moduleName := "cats-kernel-laws", name := "Cats kernel laws") .settings(commonSettings) @@ -587,8 +641,9 @@ lazy val kernelLaws = crossProject(JSPlatform, JVMPlatform) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings ++ mimaSettings("cats-kernel-laws", includeCats1 = false)) .dependsOn(kernel) + .nativeSettings(commonNativeSettings) -lazy val core = crossProject(JSPlatform, JVMPlatform) +lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Pure) .dependsOn(kernel) .settings(moduleName := "cats-core", name := "Cats core") @@ -608,8 +663,10 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) ) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings ++ mimaSettings("cats-core")) + .settings(testingDependencies) + .nativeSettings(commonNativeSettings) -lazy val laws = crossProject(JSPlatform, JVMPlatform) +lazy val laws = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Pure) .dependsOn(kernel, core, kernelLaws) .settings(moduleName := "cats-laws", name := "Cats laws") @@ -618,16 +675,18 @@ lazy val laws = crossProject(JSPlatform, JVMPlatform) .settings(testingDependencies) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings ++ mimaSettings("cats-laws", includeCats1 = false)) + .nativeSettings(commonNativeSettings) -lazy val free = crossProject(JSPlatform, JVMPlatform) +lazy val free = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Pure) .dependsOn(core, tests % "test-internal -> test") .settings(moduleName := "cats-free", name := "Cats Free") .settings(catsSettings) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings ++ mimaSettings("cats-free")) + .nativeSettings(commonNativeSettings) -lazy val tests = crossProject(JSPlatform, JVMPlatform) +lazy val tests = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Pure) .dependsOn(testkit % Test) .settings(moduleName := "cats-tests") @@ -637,8 +696,9 @@ lazy val tests = crossProject(JSPlatform, JVMPlatform) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings) .settings(scalacOptions in Test := (scalacOptions in Test).value.filter(_ != "-Xfatal-warnings")) + .nativeSettings(commonNativeSettings) -lazy val testkit = crossProject(JSPlatform, JVMPlatform) +lazy val testkit = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Pure) .dependsOn(core, laws) .enablePlugins(BuildInfoPlugin) @@ -649,8 +709,9 @@ lazy val testkit = crossProject(JSPlatform, JVMPlatform) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings ++ mimaSettings("cats-testkit", includeCats1 = false)) .settings(scalacOptions := scalacOptions.value.filter(_ != "-Xfatal-warnings")) + .nativeSettings(commonNativeSettings) -lazy val alleycatsCore = crossProject(JSPlatform, JVMPlatform) +lazy val alleycatsCore = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("alleycats-core")) .dependsOn(core) @@ -660,8 +721,9 @@ lazy val alleycatsCore = crossProject(JSPlatform, JVMPlatform) .settings(includeGeneratedSrc) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings ++ mimaSettings("alleycats-core", includeCats1 = false)) + .nativeSettings(commonNativeSettings) -lazy val alleycatsLaws = crossProject(JSPlatform, JVMPlatform) +lazy val alleycatsLaws = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("alleycats-laws")) .dependsOn(alleycatsCore, laws) @@ -672,8 +734,9 @@ lazy val alleycatsLaws = crossProject(JSPlatform, JVMPlatform) .settings(testingDependencies) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings ++ mimaSettings("alleycats-laws", includeCats1 = false)) + .nativeSettings(commonNativeSettings) -lazy val alleycatsTests = crossProject(JSPlatform, JVMPlatform) +lazy val alleycatsTests = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("alleycats-tests")) .dependsOn(alleycatsLaws, tests % "test-internal -> test") .settings(moduleName := "alleycats-tests") @@ -682,6 +745,7 @@ lazy val alleycatsTests = crossProject(JSPlatform, JVMPlatform) .jsSettings(commonJsSettings) .jvmSettings(commonJvmSettings) .settings(scalacOptions in Test := (scalacOptions in Test).value.filter(_ != "-Xfatal-warnings")) + .nativeSettings(commonNativeSettings) // bench is currently JVM-only @@ -726,6 +790,14 @@ lazy val js = project .settings(commonJsSettings) .enablePlugins(ScalaJSPlugin) +// cats-native is Native-only +lazy val native = project + .dependsOn(core.native, tests.native % "test-internal -> test") + .settings(moduleName := "cats-native") + .settings(catsSettings) + .settings(commonNativeSettings) + .enablePlugins(ScalaNativePlugin) + // cats-jvm is JVM-only lazy val jvm = project .dependsOn(core.jvm, tests.jvm % "test-internal -> test") @@ -830,14 +902,24 @@ addCommandAlias("buildTestsJVM", ";lawsJVM/test;testkitJVM/test;testsJVM/test;jv addCommandAlias("buildFreeJVM", ";freeJVM/test") addCommandAlias("buildAlleycatsJVM", ";alleycatsCoreJVM/test;alleycatsLawsJVM/test;alleycatsTestsJVM/test") addCommandAlias("buildJVM", ";buildKernelJVM;buildCoreJVM;buildTestsJVM;buildFreeJVM;buildAlleycatsJVM") -addCommandAlias("validateBC", ";binCompatTest/test;mimaReportBinaryIssues") +addCommandAlias("validateBC", ";binCompatTest/test;catsJVM/mimaReportBinaryIssues") addCommandAlias("validateJVM", ";fmtCheck;buildJVM;bench/test;validateBC;makeMicrosite") addCommandAlias("validateJS", ";testsJS/test;js/test") addCommandAlias("validateKernelJS", "kernelLawsJS/test") addCommandAlias("validateFreeJS", "freeJS/test") addCommandAlias("validateAlleycatsJS", "alleycatsTestsJS/test") addCommandAlias("validateAllJS", "all testsJS/test js/test kernelLawsJS/test freeJS/test alleycatsTestsJS/test") -addCommandAlias("validate", ";clean;validateJS;validateKernelJS;validateFreeJS;validateJVM") +addCommandAlias("validateNative", ";testsNative/test;native/test") +addCommandAlias("validateKernelNative", "kernelLawsNative/test") +addCommandAlias("validateFreeNative", "freeNative/test") +addCommandAlias("validateAlleycatsNative", "alleycatsTestsNative/test") +addCommandAlias("validateAllNative", + "all testsNative/test native/test kernelLawsNative/test freeNative/test alleycatsTestsNative/test" +) +addCommandAlias( + "validate", + ";clean;validateJS;validateKernelJS;validateFreeJS;validateNative;validateKernelNative;validateFreeNative;validateJVM" +) addCommandAlias("prePR", "fmt") diff --git a/kernel-laws/js/src/main/scala/cats/platform/Platform.scala b/kernel-laws/js/src/main/scala/cats/platform/Platform.scala index 13a9bb3b5d..7a6c16c9e3 100644 --- a/kernel-laws/js/src/main/scala/cats/platform/Platform.scala +++ b/kernel-laws/js/src/main/scala/cats/platform/Platform.scala @@ -5,5 +5,6 @@ private[cats] object Platform { // $COVERAGE-OFF$ final val isJvm = false final val isJs = true + final val isNative = false // $COVERAGE-ON$ } diff --git a/kernel-laws/jvm/src/main/scala/cats/platform/Platform.scala b/kernel-laws/jvm/src/main/scala/cats/platform/Platform.scala index fa14235e61..7a208487e9 100644 --- a/kernel-laws/jvm/src/main/scala/cats/platform/Platform.scala +++ b/kernel-laws/jvm/src/main/scala/cats/platform/Platform.scala @@ -5,5 +5,6 @@ private[cats] object Platform { // $COVERAGE-OFF$ final val isJvm = true final val isJs = false + final val isNative = false // $COVERAGE-ON$ } diff --git a/kernel-laws/native/src/main/scala/cats/platform/Platform.scala b/kernel-laws/native/src/main/scala/cats/platform/Platform.scala new file mode 100644 index 0000000000..699cdd6da2 --- /dev/null +++ b/kernel-laws/native/src/main/scala/cats/platform/Platform.scala @@ -0,0 +1,10 @@ +package cats.platform + +private[cats] object Platform { + // using `final val` makes compiler constant-fold any use of these values, dropping dead code automatically + // $COVERAGE-OFF$ + final val isJvm = false + final val isJs = false + final val isNative = true + // $COVERAGE-ON$ +} diff --git a/kernel-laws/shared/src/main/scala/cats/kernel/laws/SerializableLaws.scala b/kernel-laws/shared/src/main/scala/cats/kernel/laws/SerializableLaws.scala index ae2ee0d83b..ac2190de10 100644 --- a/kernel-laws/shared/src/main/scala/cats/kernel/laws/SerializableLaws.scala +++ b/kernel-laws/shared/src/main/scala/cats/kernel/laws/SerializableLaws.scala @@ -18,14 +18,13 @@ object SerializableLaws { // This part is a bit tricky. Basically, we only want to test // serializability on the JVM. // - // `Platform.isJs` is a constant expression, so we can rely on + // `Platform.isJvm` is a constant expression, so we can rely on // scalac to prune away the "other" branch. Thus, when Scala.js // looks at this method it won't "see" the branch which was removed, // and will avoid an error trying to support java.io.*. def serializable[A](a: A): Prop = - if (Platform.isJs) Prop(_ => Result(status = Proof)) - else + if (Platform.isJvm) { Prop { _ => import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream} @@ -48,4 +47,5 @@ object SerializableLaws { if (ois != null) ois.close() // scalastyle:ignore null } } + } else Prop(_ => Result(status = Proof)) } diff --git a/native/src/test/scala/cats/native/tests/FutureSuite.scala b/native/src/test/scala/cats/native/tests/FutureSuite.scala new file mode 100644 index 0000000000..3536b7a7a6 --- /dev/null +++ b/native/src/test/scala/cats/native/tests/FutureSuite.scala @@ -0,0 +1,65 @@ +package cats.native.tests + +import cats.kernel.laws.discipline.{MonoidTests => MonoidLawTests, SemigroupTests => SemigroupLawTests} +import cats.kernel.{Eq, Semigroup} +import cats.laws.discipline._ +import cats.laws.discipline.arbitrary._ +import cats.syntax.either._ +import cats.tests.{CatsSuite, ListWrapper} +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.rng.Seed +import org.scalacheck.{Arbitrary, Cogen} + +import scala.concurrent.{Await, ExecutionContextExecutor, Future} +import scala.concurrent.duration._ + +class FutureSuite extends CatsSuite { + val timeout = 3.seconds + + // TODO: We shouldn't do this! See: https://github.com/scala-js/scala-js/issues/2102 + implicit private object SynchronousExecutor extends ExecutionContextExecutor { + def execute(runnable: Runnable): Unit = + try { + runnable.run() + } catch { + case t: Throwable => reportFailure(t) + } + + def reportFailure(t: Throwable): Unit = + t.printStackTrace() + } + + def futureEither[A](f: Future[A]): Future[Either[Throwable, A]] = + f.map(Either.right[Throwable, A]).recover { case t => Either.left(t) } + + implicit def eqfa[A: Eq]: Eq[Future[A]] = + new Eq[Future[A]] { + def eqv(fx: Future[A], fy: Future[A]): Boolean = { + val fz = futureEither(fx).zip(futureEither(fy)) + Await.result(fz.map { case (tx, ty) => tx === ty }, timeout) + } + } + + implicit def cogen[A: Cogen]: Cogen[Future[A]] = + Cogen[Future[A]] { (seed: Seed, t: Future[A]) => + Cogen[A].perturb(seed, Await.result(t, timeout)) + } + + implicit val throwableEq: Eq[Throwable] = + Eq.by[Throwable, String](_.toString) + + // Need non-fatal Throwables for Future recoverWith/handleError + implicit val nonFatalArbitrary: Arbitrary[Throwable] = + Arbitrary(arbitrary[Exception].map(identity)) + + checkAll("Future with Throwable", MonadErrorTests[Future, Throwable].monadError[Int, Int, Int]) + checkAll("Future", MonadTests[Future].monad[Int, Int, Int]) + checkAll("Future", CoflatMapTests[Future].coflatMap[Int, Int, Int]) + + { + implicit val F: Semigroup[ListWrapper[Int]] = ListWrapper.semigroup[Int] + checkAll("Future[ListWrapper[Int]]", SemigroupLawTests[Future[ListWrapper[Int]]].semigroup) + } + + checkAll("Future[Int]", MonoidLawTests[Future[Int]].monoid) +} diff --git a/project/plugins.sbt b/project/plugins.sbt index c4ff5a0af9..3035dd84dc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -11,7 +11,9 @@ addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.5") addSbtPlugin("com.47deg" % "sbt-microsites" % "1.3.1") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.16") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.4.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.10.1")