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 all 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
33 changes: 23 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.1.0"
val jawnVersion = "1.2.0"
val previousCirceFs2Version = "0.13.0"

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

val scala212 = "2.12.14"
val scala213 = "2.13.6"
Expand Down Expand Up @@ -57,23 +59,34 @@ val baseSettings = Seq(
)

val allSettings = baseSettings ++ publishSettings
val noPublishSettings = Seq(
publish := {},
publishLocal := {},
publishArtifact := false
)

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

val fs2 = project
.in(file("."))
lazy val root = project.in(file(".")).settings(allSettings).settings(noPublishSettings).aggregate(fs2.jvm, fs2.js)

lazy val fs2 = crossProject(JVMPlatform, JSPlatform)
.crossType(CrossType.Pure)
.in(file("fs2"))
.settings(allSettings)
.jsSettings(coverageEnabled := false)
.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
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.AsyncIOSpec
import cats.effect.testing.scalatest.EffectTestSupport
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

/**
* An opinionated stack of traits to improve consistency and reduce boilerplate in circe tests.
*/
trait CirceSuite
extends AnyFlatSpec
extends AsyncFlatSpec
with AsyncIOSpec
with AssertingSyntax
with EffectTestSupport
with ScalaCheckDrivenPropertyChecks
with AllInstances
with AllSyntax
Expand Down
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,61 +101,61 @@ 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))

"chunkDecoder" 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(chunkDecoder[IO, Foo])
.compile
.toVector
.attempt
.unsafeRunSync() === Right(foos.toVector)
)
}
val result = stream.through(stringArrayParser).through(chunkDecoder[IO, Foo]).compile.toVector.attempt

result.map(r => assert(r === Right(foos.toVector)))
}.check().map(r => assert(r.passed))

"chunkDecoder" should "maintain chunk size" in
forAll { (fooStdStream: StdStream[Foo], fooVector: Vector[Foo]) =>
PropF.forAllF { (fooStdStream: StdStream[Foo], fooVector: Vector[Foo]) =>
val chunkSize = 4
val stream = serializeFoos(AsyncParser.UnwrapArray, fooStream(fooStdStream, fooVector))
val x1 = stream.through(stringArrayParser).chunkMin(chunkSize).flatMap(c => Stream.chunk(c))
val chunkSizes = x1.through(chunkDecoder[IO, Foo]).chunks.map(_.size).compile.toList.unsafeRunSync()

if (chunkSizes.sum >= chunkSize)
assert(chunkSizes.head == chunkSize)
else
assert(chunkSizes.length <= 1)
}
x1.through(chunkDecoder[IO, Foo]).chunks.map(_.size).compile.toList.map { chunkSizes =>
if (chunkSizes.sum >= chunkSize)
assert(chunkSizes.head == chunkSize)
else
assert(chunkSizes.length <= 1)
}
}.check().map(r => assert(r.passed))

"stringArrayParser" should "return ParsingFailure" in {
testParsingFailure(_.through(stringArrayParser))
Expand All @@ -170,7 +182,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 @@ -181,13 +193,13 @@ 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))

"chunkDecoder" 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 @@ -198,28 +210,40 @@ 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)
}
}

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
}

}
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.7.0")