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 1 commit
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
23 changes: 14 additions & 9 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,
armanbilge marked this conversation as resolved.
Show resolved Hide resolved
"org.typelevel" %%% "jawn-parser" % jawnVersion
),
ghpagesNoJekyll := true,
docMappingsApiDir := "api",
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")
22 changes: 17 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,8 @@ 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
}

}