Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cross-build for Scala.js #272

Merged
merged 9 commits into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
run: sbt --client '++${{ matrix.scala }}; githubWorkflowCheck'

- name: Test
run: sbt --client '++${{ matrix.scala }}; clean; coverage; test; coverageReport; scalafmtCheckAll'
run: sbt --client '++${{ matrix.scala }}; clean; coverage; fs2JVM / test; fs2JS / test; fs2JVM / coverageReport; scalafmtCheckAll'

- name: Upload code coverage
uses: codecov/codecov-action@e156083f13aff6830c92fc5faa23505779fbf649
25 changes: 15 additions & 10 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ val compilerOptions = Seq(
"-Ywarn-numeric-widen"
)

val circeVersion = "0.14.1"
val circeVersion = "0.15.0-M1"
val fs2Version = "3.0.6"
val jawnVersion = "1.2.0"
val previousCirceFs2Version = "0.13.0"

val scalaTestVersion = "3.2.9"
val scalaTestPlusVersion = "3.2.9.0"
val catsEffectTestingVersion = "1.1.1"
val scalacheckEffectVersion = "1.0.2"

val scala212 = "2.12.14"
val scala213 = "2.13.6"
Expand Down Expand Up @@ -60,20 +62,23 @@ val allSettings = baseSettings ++ publishSettings

val docMappingsApiDir = settingKey[String]("Subdirectory in site target directory for API docs")

val fs2 = project
val fs2 = crossProject(JVMPlatform, JSPlatform)
.crossType(CrossType.Pure)
.in(file("."))
.settings(allSettings)
.settings(
moduleName := "circe-fs2",
mimaPreviousArtifacts := Set("io.circe" %% "circe-fs2" % previousCirceFs2Version),
libraryDependencies ++= Seq(
"co.fs2" %% "fs2-core" % fs2Version,
"io.circe" %% "circe-jawn" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion % Test,
"io.circe" %% "circe-testing" % circeVersion % Test,
"org.scalatest" %% "scalatest" % scalaTestVersion % Test,
"org.scalatestplus" %% "scalacheck-1-15" % scalaTestPlusVersion % Test,
"org.typelevel" %% "jawn-parser" % jawnVersion
"co.fs2" %%% "fs2-core" % fs2Version,
"io.circe" %%% "circe-jawn" % circeVersion,
"io.circe" %%% "circe-generic" % circeVersion % Test,
"io.circe" %%% "circe-testing" % circeVersion % Test,
"org.scalatest" %%% "scalatest" % scalaTestVersion % Test,
"org.scalatestplus" %%% "scalacheck-1-15" % scalaTestPlusVersion % Test,
"org.typelevel" %%% "cats-effect-testing-scalatest" % catsEffectTestingVersion % Test,
"org.typelevel" %%% "scalacheck-effect" % scalacheckEffectVersion % Test,
"org.typelevel" %%% "jawn-parser" % jawnVersion
),
ghpagesNoJekyll := true,
docMappingsApiDir := "api",
Expand Down Expand Up @@ -133,7 +138,7 @@ ThisBuild / githubWorkflowJavaVersions := Seq("adopt@1.8")
ThisBuild / githubWorkflowPublishTargetBranches := Seq.empty
ThisBuild / githubWorkflowBuild := Seq(
WorkflowStep.Sbt(
List("clean", "coverage", "test", "coverageReport", "scalafmtCheckAll"),
List("clean", "coverage", "fs2JVM / test", "fs2JS / test", "fs2JVM / coverageReport", "scalafmtCheckAll"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at e.g. the circe-optics build, this seems to be done via .jsSettings(coverageEnabled := false), which seems better, since it applies for people running the default commands locally, and not just in CI.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For whatever reason that doesn't seem to work here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh. I get why you're doing this, but this kind of thing is why I hate Scala.js so much.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the problems we are experiencing here are confounded by the fact this is a single-module project.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@travisbrown I think moving the project into its own folder and having a designated root fixes it.

id = None,
name = Some("Test")
),
Expand Down
2 changes: 2 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.8.2")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.6.0")
23 changes: 18 additions & 5 deletions src/test/scala/io/circe/fs2/CirceSuite.scala
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
package io.circe.fs2

import cats.effect.testing.scalatest.AssertingSyntax
import cats.effect.testing.scalatest.EffectTestSupport
import cats.effect.unsafe.IORuntime
import cats.instances.AllInstances
import cats.syntax.{ AllSyntax, EitherOps }
import io.circe.testing.{ ArbitraryInstances, EqInstances }
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatestplus.scalacheck.{ Checkers, ScalaCheckDrivenPropertyChecks }
import cats.syntax.AllSyntax
import cats.syntax.EitherOps
import io.circe.testing.ArbitraryInstances
import io.circe.testing.EqInstances
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatestplus.scalacheck.Checkers
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import org.typelevel.discipline.Laws

import scala.language.implicitConversions
import cats.effect.unsafe.IORuntimeConfig

/**
* An opinionated stack of traits to improve consistency and reduce boilerplate in circe tests.
*/
trait CirceSuite
extends AnyFlatSpec
extends AsyncFlatSpec
with AssertingSyntax
with EffectTestSupport
with ScalaCheckDrivenPropertyChecks
with AllInstances
with AllSyntax
Expand All @@ -23,6 +33,9 @@ trait CirceSuite

implicit def prioritizedCatsSyntaxEither[A, B](eab: Either[A, B]): EitherOps[A, B] = new EitherOps(eab)

implicit def ioRuntime =
IORuntime(executionContext, executionContext, IORuntime.global.scheduler, () => (), IORuntimeConfig())

def checkLaws(name: String, ruleSet: Laws#RuleSet): Unit = ruleSet.all.properties.zipWithIndex.foreach {
case ((id, prop), 0) => name should s"obey $id" in Checkers.check(prop)
case ((id, prop), _) => it should s"obey $id" in Checkers.check(prop)
Expand Down
148 changes: 89 additions & 59 deletions src/test/scala/io/circe/fs2/Fs2Suite.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package io.circe.fs2

import _root_.fs2.{ Pipe, Stream, text }
import _root_.fs2.Pipe
import _root_.fs2.Stream
import _root_.fs2.text
import cats.effect.IO
import cats.effect.unsafe.implicits._
import io.circe.{ DecodingFailure, Json, ParsingFailure }
import io.circe.DecodingFailure
import io.circe.Json
import io.circe.ParsingFailure
import io.circe.fs2.examples._
import io.circe.generic.auto._
import io.circe.syntax._
import org.scalacheck.Prop
import org.scalacheck.effect.PropF
import org.scalatest.compatible.Assertion
import org.scalatest.enablers.WheneverAsserting
import org.scalatest.exceptions.DiscardedEvaluationException
import org.typelevel.jawn.AsyncParser

import scala.collection.immutable.{ Stream => StdStream }

class Fs2Suite extends CirceSuite {
Expand All @@ -34,14 +43,21 @@ class Fs2Suite extends CirceSuite {
}

"stringParser" should "parse single value" in {
forAll { (foo: Foo) =>
PropF.forAllF { (foo: Foo) =>
val stream = serializeFoos(AsyncParser.SingleValue, Stream.emit(foo))
assert(
stream.through(stringParser(AsyncParser.SingleValue)).compile.toVector.attempt.unsafeRunSync() === Right(
Vector(foo.asJson)
stream
.through(stringParser(AsyncParser.SingleValue))
.compile
.toVector
.attempt
.map(r =>
assert(
r === Right(
Vector(foo.asJson)
)
)
)
)
}
}.check().map(r => assert(r.passed))
}

"byteArrayParser" should "parse bytes wrapped in array" in {
Expand All @@ -53,31 +69,27 @@ class Fs2Suite extends CirceSuite {
}

"byteParser" should "parse single value" in {
forAll { (foo: Foo) =>
PropF.forAllF { (foo: Foo) =>
val stream = serializeFoos(AsyncParser.SingleValue, Stream.emit(foo))
assert(
stream
.through(text.utf8Encode)
.through(byteParser(AsyncParser.SingleValue))
.compile
.toVector
.attempt
.unsafeRunSync() === Right(Vector(foo.asJson))
)
}
stream
.through(text.utf8Encode)
.through(byteParser(AsyncParser.SingleValue))
.compile
.toVector
.attempt
.map(r => assert(r === Right(Vector(foo.asJson))))
}.check().map(r => assert(r.passed))
}

"byteParser" should "parse single value, when run twice" in {
forAll { (foo: Foo) =>
PropF.forAllF { (foo: Foo) =>
val stream = serializeFoos(AsyncParser.SingleValue, Stream.emit(foo))

val parseOnce =
stream.through(text.utf8Encode).through(byteParser(AsyncParser.SingleValue)).compile.toVector

parseOnce.attempt.unsafeRunSync()

assert(parseOnce.attempt.unsafeRunSync() == Right(Vector(foo.asJson)))
}
(parseOnce.attempt >> parseOnce.attempt).map(r => assert(r == Right(Vector(foo.asJson))))
}.check().map(r => assert(r.passed))
}

"byteArrayParserC" should "parse bytes wrapped in array" in {
Expand All @@ -89,32 +101,38 @@ class Fs2Suite extends CirceSuite {
}

"byteParserC" should "parse single value" in {
forAll { (foo: Foo) =>
PropF.forAllF { (foo: Foo) =>
val stream = serializeFoos(AsyncParser.SingleValue, Stream.emit(foo))
assert(
stream
.through(text.utf8Encode)
.chunks
.through(byteParserC(AsyncParser.SingleValue))
.compile
.toVector
.attempt
.unsafeRunSync() === Right(Vector(foo.asJson))
)
}
stream
.through(text.utf8Encode)
.chunks
.through(byteParserC(AsyncParser.SingleValue))
.compile
.toVector
.attempt
.map(r => assert(r === Right(Vector(foo.asJson))))
}.check().map(r => assert(r.passed))
}

"decoder" should "decode enumerated JSON values" in
forAll { (fooStdStream: StdStream[Foo], fooVector: Vector[Foo]) =>
PropF.forAllF { (fooStdStream: StdStream[Foo], fooVector: Vector[Foo]) =>
val stream = serializeFoos(AsyncParser.UnwrapArray, fooStream(fooStdStream, fooVector))
val foos = fooStdStream ++ fooVector

assert(
stream.through(stringArrayParser).through(decoder[IO, Foo]).compile.toVector.attempt.unsafeRunSync() === Right(
foos.toVector
stream
.through(stringArrayParser)
.through(decoder[IO, Foo])
.compile
.toVector
.attempt
.map(r =>
assert(
r === Right(
foos.toVector
)
)
)
)
}
}.check().map(r => assert(r.passed))

"stringArrayParser" should "return ParsingFailure" in {
testParsingFailure(_.through(stringArrayParser))
Expand All @@ -141,7 +159,7 @@ class Fs2Suite extends CirceSuite {
}

"decoder" should "return DecodingFailure" in
forAll { (fooStdStream: StdStream[Foo], fooVector: Vector[Foo]) =>
PropF.forAllF { (fooStdStream: StdStream[Foo], fooVector: Vector[Foo]) =>
sealed trait Foo2
case class Bar2(x: String) extends Foo2

Expand All @@ -152,29 +170,41 @@ class Fs2Suite extends CirceSuite {
.compile
.toVector
.attempt
.unsafeRunSync()

assert(result.isLeft && result.left.get.isInstanceOf[DecodingFailure])
result.map(r => assert(r.isLeft && r.left.get.isInstanceOf[DecodingFailure]))
}
}
}.check().map(r => assert(r.passed))

private def testParser(mode: AsyncParser.Mode, through: Pipe[IO, String, Json]) =
forAll { (fooStdStream: StdStream[Foo], fooVector: Vector[Foo]) =>
PropF.forAllF { (fooStdStream: StdStream[Foo], fooVector: Vector[Foo]) =>
val stream = serializeFoos(mode, fooStream(fooStdStream, fooVector))
val foos = (fooStdStream ++ fooVector).map(_.asJson)

assert(stream.through(through).compile.toVector.attempt.unsafeRunSync() === Right(foos.toVector))
}
stream.through(through).compile.toVector.attempt.map(r => assert(r === Right(foos.toVector)))
}.check().asserting(r => assert(r.passed))

private def testParsingFailure(through: Pipe[IO, String, Json]) =
forAll { (stringStdStream: StdStream[String], stringVector: Vector[String]) =>
val result = Stream("}")
.append(stringStream(stringStdStream, stringVector))
.through(through)
.compile
.toVector
.attempt
.unsafeRunSync()
assert(result.isLeft && result.left.get.isInstanceOf[ParsingFailure])
PropF.forAllF { (stringStdStream: StdStream[String], stringVector: Vector[String]) =>
val result =
Stream("}").append(stringStream(stringStdStream, stringVector)).through(through).compile.toVector.attempt
result.map(result => assert(result.isLeft && result.left.get.isInstanceOf[ParsingFailure]))
}.check().asserting(r => assert(r.passed))

private implicit def assertionToProp: IO[Assertion] => PropF[IO] = { assertion =>
assertion.as(PropF.Result[IO](Prop.True, Nil, Set.empty, Set.empty): PropF[IO]).handleError {
case _: DiscardedEvaluationException => PropF.Result[IO](Prop.Undecided, Nil, Set.empty, Set.empty)
case t => PropF.Result[IO](Prop.Exception(t), Nil, Set.empty, Set.empty)
}
}
Comment on lines +232 to +237
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this still needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes?


private implicit def assertingNatureOfIO: WheneverAsserting[IO[Assertion]] { type Result = IO[Assertion] } =
new WheneverAsserting[IO[Assertion]] {
type Result = IO[Assertion]
def whenever(condition: Boolean)(fun: => IO[Assertion]): IO[Assertion] =
if (!condition)
IO.raiseError[Assertion](new DiscardedEvaluationException)
else
fun
}

}