diff --git a/build.sbt b/build.sbt index 461b4d9d5..3f3ee066b 100644 --- a/build.sbt +++ b/build.sbt @@ -32,14 +32,10 @@ libraryDependencies ++= Seq( ) testFrameworks ++= Seq( - new TestFramework("scalaprops.ScalapropsFramework"), new TestFramework("zio.test.sbt.ZTestFramework") ) -libraryDependencies += "com.github.scalaprops" %% "scalaprops" % "0.8.0" % "test" -parallelExecution in Test := false // scalaprops does not support parallel execution -libraryDependencies += "com.lihaoyi" %% "utest" % "0.7.2" % "test" -testFrameworks += new TestFramework("utest.runner.Framework") +parallelExecution in Test := true libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.5.0" % "test" libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.5.0" % "test" diff --git a/src/test/scala/zio/json/DecoderSpec.scala b/src/test/scala/zio/json/DecoderSpec.scala new file mode 100644 index 000000000..0fe707589 --- /dev/null +++ b/src/test/scala/zio/json/DecoderSpec.scala @@ -0,0 +1,301 @@ +package testzio.json + +import io.circe +import org.typelevel.jawn.{ ast => jawn } +import testzio.json.TestUtils._ +import testzio.json.data.googlemaps._ +import testzio.json.data.twitter._ +import zio.blocking._ +import zio.json._ +import zio.json.ast._ +import zio.test.Assertion._ +import zio.test.{ DefaultRunnableSpec, _ } +import zio.{ test => _, _ } + +import scala.collection.immutable + +object DecoderSpec extends DefaultRunnableSpec { + def spec = + suite("Decoder")( + test("primitives") { + // this big integer consumes more than 128 bits + assert("170141183460469231731687303715884105728".fromJson[java.math.BigInteger])( + isLeft(equalTo("(expected a 128 bit BigInteger)")) + ) + }, + test("eithers") { + val bernies = List("""{"a":1}""", """{"left":1}""", """{"Left":1}""") + val trumps = List("""{"b":2}""", """{"right":2}""", """{"Right":2}""") + + assert(bernies.map(_.fromJson[Either[Int, Int]]))( + forall(isRight(isLeft(equalTo(1)))) + ) && assert(trumps.map(_.fromJson[Either[Int, Int]]))( + forall(isRight(isRight(equalTo(2)))) + ) + }, + test("parameterless products") { + import exampleproducts._ + + // actually anything works... consider this a canary test because if only + // the empty object is supported that's fine. + assert("""{}""".fromJson[Parameterless])(isRight(equalTo(Parameterless()))) && + assert("""null""".fromJson[Parameterless])(isRight(equalTo(Parameterless()))) && + assert("""{"field":"value"}""".fromJson[Parameterless])(isRight(equalTo(Parameterless()))) + }, + test("no extra fields") { + import exampleproducts._ + + assert("""{"s":""}""".fromJson[OnlyString])(isRight(equalTo(OnlyString("")))) && + assert("""{"s":"","t":""}""".fromJson[OnlyString])(isLeft(equalTo("(invalid extra field)"))) + }, + test("sum encoding") { + import examplesum._ + + assert("""{"Child1":{}}""".fromJson[Parent])(isRight(equalTo(Child1()))) && + assert("""{"Child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2()))) && + assert("""{"type":"Child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) + }, + test("sum alternative encoding") { + import examplealtsum._ + + assert("""{"hint":"Cain"}""".fromJson[Parent])(isRight(equalTo(Child1()))) && + assert("""{"hint":"Abel"}""".fromJson[Parent])(isRight(equalTo(Child2()))) && + assert("""{"hint":"Samson"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && + assert("""{"Cain":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) + }, + testM("googleMapsNormal") { + getResourceAsStringM("google_maps_api_response.json").map { str => + assert(str.fromJson[DistanceMatrix])(matchesCirceDecoded[DistanceMatrix](str)) + } + }, + testM("googleMapsCompact") { + getResourceAsStringM("google_maps_api_compact_response.json").map { str => + assert(str.fromJson[DistanceMatrix])(matchesCirceDecoded[DistanceMatrix](str)) + } + }, + testM("googleMapsExtra") { + getResourceAsStringM("google_maps_api_extra.json").map { str => + assert(str.fromJson[DistanceMatrix])(matchesCirceDecoded[DistanceMatrix](str)) + } + }, + testM("googleMapsError") { + getResourceAsStringM("google_maps_api_error_response.json").map { str => + assert(str.fromJson[DistanceMatrix])(isLeft(equalTo(".rows[0].elements[0].distance.value(missing)"))) + } + }, + testM("googleMapsAst") { + val response = getResourceAsStringM("google_maps_api_response.json") + val compact = getResourceAsStringM("google_maps_api_compact_response.json") + + (response <&> compact).map { + case (response, compact) => + assert(response.fromJson[Json])(equalTo(compact.fromJson[Json])) + } + }, + testM("twitter") { + getResourceAsStringM("twitter_api_response.json").map { str => + assert(str.fromJson[List[Tweet]])(matchesCirceDecoded[List[Tweet]](str)) + } + }, + testM("geojson1") { + import testzio.json.data.geojson.generated._ + + getResourceAsStringM("che.geo.json").map { str => + assert(str.fromJson[GeoJSON])(matchesCirceDecoded[GeoJSON](str)) + } + }, + testM("geojson1 alt") { + import testzio.json.data.geojson.handrolled._ + + getResourceAsStringM("che.geo.json").map { str => + assert(str.fromJson[GeoJSON])(matchesCirceDecoded[GeoJSON](str)) + } + }, + testM("geojson2") { + import testzio.json.data.geojson.generated._ + + getResourceAsStringM("che-2.geo.json").map { str => + assert(str.fromJson[GeoJSON])(matchesCirceDecoded[GeoJSON](str)) + } + }, + testM("geojson2 lowlevel") { + import testzio.json.data.geojson.generated._ + // this uses a lower level Reader to ensure that the more general recorder + // impl is covered by the tests + + getResourceAsStringM("che-2.geo.json").flatMap { str => + ZManaged.fromAutoCloseable(Task(getResourceAsReader("che-2.geo.json"))).use { reader => + for { + circe <- ZIO.fromEither(circe.parser.decode[GeoJSON](str)) + got <- effectBlocking(JsonDecoder[GeoJSON].unsafeDecode(Chunk.empty, reader)) + } yield { + assert(got)(equalTo(circe)) + } + } + } + }, + test("unicode") { + assert(""""€🐵🥰"""".fromJson[String])(isRight(equalTo("€🐵🥰"))) + }, + test("Seq") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = Seq("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[Seq[String]])(isRight(equalTo(expected))) + }, + test("Vector") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = Vector("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[Vector[String]])(isRight(equalTo(expected))) + }, + test("SortedSet") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = immutable.SortedSet("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[immutable.SortedSet[String]])(isRight(equalTo(expected))) + }, + test("HashSet") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = immutable.HashSet("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[immutable.HashSet[String]])(isRight(equalTo(expected))) + }, + test("Set") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = Set("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[Set[String]])(isRight(equalTo(expected))) + }, + test("Map") { + val jsonStr = """{"5XL":3,"2XL":14,"XL":159}""" + val expected = Map("5XL" -> 3, "2XL" -> 14, "XL" -> 159) + + assert(jsonStr.fromJson[Map[String, Int]])(isRight(equalTo(expected))) + }, + test("zio.Chunk") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = Chunk("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[Chunk[String]])(isRight(equalTo(expected))) + }, + suite("jawn")( + testAst("bar"), + testAst("bla25"), + testAst("bla2"), + testAst("countries.geo"), + testAst("dkw-sample"), + testAst("foo"), + testAst("qux1"), + testAst("qux2"), + testAst("ugh10k") + ) + ) @@ TestAspect.parallel + + def testAst(label: String) = + testM(label) { + getResourceAsStringM(s"jawn/$label.json").flatMap { input => + val expected = jawn.JParser.parseFromString(input).toEither.map(fromJawn) + val got = input.fromJson[Json].map(normalize) + + def e2s[A, B](e: Either[A, B]) = + e match { + case Left(left) => left.toString + case Right(right) => right.toString + } + + if (expected != got) { + val gotf = s"${label}-got.json" + val expectedf = s"${label}-expected.json" + + for { + _ <- effectBlocking(writeFile(gotf, e2s(got))) + _ <- effectBlocking(writeFile(expectedf, e2s(expected))) + _ <- console.putStrLn(s"dumped .json files, use `cmp <(jq . ${expectedf}) <(jq . ${gotf})`") + } yield { + assert(got)(equalTo(expected.left.map(_.getMessage))) + } + } else ZIO.effectTotal(assertCompletes) + } + } + + def fromJawn(ast: jawn.JValue): Json = + ast match { + case jawn.JNull => Json.Null + case jawn.JTrue => Json.Bool(true) + case jawn.JFalse => Json.Bool(false) + case jawn.JString(s) => Json.Str(s) + case jawn.LongNum(i) => + Json.Num(new java.math.BigDecimal(java.math.BigInteger.valueOf(i))) + case jawn.DoubleNum(d) => Json.Num(new java.math.BigDecimal(d)) + case jawn.DeferLong(i) => + Json.Num(new java.math.BigDecimal(new java.math.BigInteger(i))) + case jawn.DeferNum(n) => Json.Num(new java.math.BigDecimal(n)) + case jawn.JArray(vs) => Json.Arr(Chunk.fromArray(vs).map(fromJawn)) + case jawn.JObject(es) => + Json.Obj(Chunk.fromIterable(es).sortBy(_._1).map { case (k, v) => (k, fromJawn(v)) }) + } + + // reorder objects to match jawn's lossy AST (and dedupe) + def normalize(ast: Json): Json = + ast match { + case Json.Obj(values) => + Json.Obj( + values + .distinctBy(_._1) + .map { case (k, v) => (k, normalize(v)) } + .sortBy(_._1) + ) + case Json.Arr(values) => Json.Arr(values.map(normalize(_))) + case other => other + } + + // Helper function because Circe and Zio-JSON’s Left differ, making tests unnecessarly verbose + def matchesCirceDecoded[A]( + expected: String + )(implicit cDecoder: circe.Decoder[A], eq: Eql[A, A]): Assertion[Either[String, A]] = { + import zio.test.Assertion.Render._ + + val cDecoded = circe.parser.decode(expected).left.map(_.toString) + + Assertion.assertion("matchesCirceDecoded")(param(cDecoded))(actual => actual == cDecoded) + } + + object exampleproducts { + case class Parameterless() + object Parameterless { + implicit val decoder: JsonDecoder[Parameterless] = + DeriveJsonDecoder.gen[Parameterless] + } + + @no_extra_fields + case class OnlyString(s: String) + object OnlyString { + implicit val decoder: JsonDecoder[OnlyString] = + DeriveJsonDecoder.gen[OnlyString] + } + } + + object examplesum { + sealed abstract class Parent + object Parent { + implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] + } + case class Child1() extends Parent + case class Child2() extends Parent + } + + object examplealtsum { + @discriminator("hint") + sealed abstract class Parent + object Parent { + implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] + } + + @hint("Cain") + case class Child1() extends Parent + + @hint("Abel") + case class Child2() extends Parent + } +} diff --git a/src/test/scala/zio/json/DecoderTest.scala b/src/test/scala/zio/json/DecoderTest.scala deleted file mode 100644 index 46937aa32..000000000 --- a/src/test/scala/zio/json/DecoderTest.scala +++ /dev/null @@ -1,329 +0,0 @@ -package testzio.json - -import scala.collection.immutable - -import zio.Chunk -import zio.json._ -import zio.json.ast._ -import io.circe -import TestUtils._ -import scalaprops._ -import Property.{ implies, prop, property } -import org.typelevel.jawn.{ ast => jawn } -import scala.collection.mutable - -import utest._ -import testzio.json.data.googlemaps._ -import testzio.json.data.twitter._ - -// testOnly *DecoderTest -object DecoderTest extends TestSuite { - - object exampleproducts { - case class Parameterless() - object Parameterless { - implicit val decoder: JsonDecoder[Parameterless] = - DeriveJsonDecoder.gen[Parameterless] - } - - @no_extra_fields - case class OnlyString(s: String) - object OnlyString { - implicit val decoder: JsonDecoder[OnlyString] = - DeriveJsonDecoder.gen[OnlyString] - } - } - - object examplesum { - - sealed abstract class Parent - object Parent { - implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] - } - case class Child1() extends Parent - case class Child2() extends Parent - } - - object examplealtsum { - - @discriminator("hint") - sealed abstract class Parent - object Parent { - implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] - } - @hint("Cain") - case class Child1() extends Parent - @hint("Abel") - case class Child2() extends Parent - } - - val tests = Tests { - test("primitives") { - // this big integer consumes more than 128 bits - "170141183460469231731687303715884105728".fromJson[java.math.BigInteger] ==> Left( - "(expected a 128 bit BigInteger)" - ) - } - - test("eithers") { - val bernies = List("""{"a":1}""", """{"left":1}""", """{"Left":1}""") - val trumps = List("""{"b":2}""", """{"right":2}""", """{"Right":2}""") - - bernies.foreach(s => s.fromJson[Either[Int, Int]] ==> Right(Left(1))) - - trumps.foreach(s => s.fromJson[Either[Int, Int]] ==> Right(Right(2))) - - } - - test("parameterless products") { - import exampleproducts._ - """{}""".fromJson[Parameterless] ==> Right(Parameterless()) - - // actually anything works... consider this a canary test because if only - // the empty object is supported that's fine. - """null""".fromJson[Parameterless] ==> Right(Parameterless()) - """{"field":"value"}""".fromJson[Parameterless] ==> Right( - Parameterless() - ) - } - - test("no extra fields") { - import exampleproducts._ - - """{"s":""}""".fromJson[OnlyString] ==> Right(OnlyString("")) - - """{"s":"","t":""}""".fromJson[OnlyString] ==> Left( - "(invalid extra field)" - ) - } - - test("sum encoding") { - import examplesum._ - """{"Child1":{}}""".fromJson[Parent] ==> Right(Child1()) - """{"Child2":{}}""".fromJson[Parent] ==> Right(Child2()) - """{"type":"Child1"}""".fromJson[Parent] ==> Left( - "(invalid disambiguator)" - ) - } - - test("sum alternative encoding") { - import examplealtsum._ - - """{"hint":"Cain"}""".fromJson[Parent] ==> Right(Child1()) - """{"hint":"Abel"}""".fromJson[Parent] ==> Right(Child2()) - """{"hint":"Samson"}""".fromJson[Parent] ==> Left( - "(invalid disambiguator)" - ) - """{"Cain":{}}""".fromJson[Parent] ==> Left( - "(missing hint 'hint')" - ) - } - - test("googleMapsNormal") { - val jsonString = getResourceAsString("google_maps_api_response.json") - jsonString.fromJson[DistanceMatrix] ==> - circe.parser.decode[DistanceMatrix](jsonString) - } - - test("googleMapsCompact") { - val jsonStringCompact = - getResourceAsString("google_maps_api_compact_response.json") - jsonStringCompact.fromJson[DistanceMatrix] ==> - circe.parser.decode[DistanceMatrix](jsonStringCompact) - } - - test("googleMapsExtra") { - val jsonStringExtra = getResourceAsString("google_maps_api_extra.json") - jsonStringExtra.fromJson[DistanceMatrix] ==> - circe.parser.decode[DistanceMatrix](jsonStringExtra) - } - - test("googleMapsError") { - val jsonStringErr = - getResourceAsString("google_maps_api_error_response.json") - jsonStringErr.fromJson[DistanceMatrix] == - Left(".rows[0].elements[0].distance.value(missing)") - } - - test("googleMapsAst") { - getResourceAsString("google_maps_api_response.json").fromJson[Json] ==> - getResourceAsString("google_maps_api_compact_response.json").fromJson[Json] - } - - test("twitter") { - val input = getResourceAsString("twitter_api_response.json") - val expected = circe.parser.decode[List[Tweet]](input) - val got = input.fromJson[List[Tweet]] - got ==> expected - } - - test("geojson1") { - import testzio.json.data.geojson.generated._ - val input = getResourceAsString("che.geo.json") - val expected = circe.parser.decode[GeoJSON](input) - val got = input.fromJson[GeoJSON] - got ==> expected - } - - test("geojson1 alt") { - import testzio.json.data.geojson.handrolled._ - val input = getResourceAsString("che.geo.json") - val expected = circe.parser.decode[GeoJSON](input) - val got = input.fromJson[GeoJSON] - got ==> expected - } - - test("geojson2") { - import testzio.json.data.geojson.generated._ - val input = getResourceAsString("che-2.geo.json") - val expected = circe.parser.decode[GeoJSON](input) - val got = input.fromJson[GeoJSON] - got ==> expected - } - - test("geojson2 lowlevel") { - import testzio.json.data.geojson.generated._ - // this uses a lower level Reader to ensure that the more general recorder - // impl is covered by the tests - val expected = - circe.parser.decode[GeoJSON](getResourceAsString("che-2.geo.json")) - val input = getResourceAsReader("che-2.geo.json") - val got = JsonDecoder[GeoJSON].unsafeDecode(Chunk.empty, input) - input.close() - Right(got) ==> expected - } - - test("unicode") { - """"€🐵🥰"""".fromJson[String] ==> Right("€🐵🥰") - } - - // collections tests contributed by Piotr Paradziński - test("Seq") { - val jsonStr = """["5XL","2XL","XL"]""" - val expected = Seq("5XL", "2XL", "XL") - jsonStr.fromJson[Seq[String]] ==> Right(expected) - } - - test("Vector") { - val jsonStr = """["5XL","2XL","XL"]""" - val expected = Vector("5XL", "2XL", "XL") - jsonStr.fromJson[Vector[String]] ==> Right(expected) - } - - test("SortedSet") { - val jsonStr = """["5XL","2XL","XL"]""" - val expected = immutable.SortedSet("5XL", "2XL", "XL") - jsonStr.fromJson[immutable.SortedSet[String]] ==> Right(expected) - } - - test("HashSet") { - val jsonStr = """["5XL","2XL","XL"]""" - val expected = immutable.HashSet("5XL", "2XL", "XL") - jsonStr.fromJson[immutable.HashSet[String]] ==> Right(expected) - } - - test("Set") { - val jsonStr = """["5XL","2XL","XL"]""" - val expected = Set("5XL", "2XL", "XL") - jsonStr.fromJson[Set[String]] ==> Right(expected) - } - - test("Map") { - val jsonStr = """{"5XL":3,"2XL":14,"XL":159}""" - val expected = Map("5XL" -> 3, "2XL" -> 14, "XL" -> 159) - jsonStr.fromJson[Map[String, Int]] ==> Right(expected) - } - - test("jawn test data: bar") { - testAst("bar") - } - - test("jawn test data: bla25") { - testAst("bla25") - } - - test("jawn test data: bla2") { - testAst("bla2") - } - - test("jawn test data: countries.geo") { - testAst("countries.geo") - } - - test("jawn test data: dkw-sample") { - testAst("dkw-sample") - } - - test("jawn test data: foo") { - testAst("foo") - } - - test("jawn test data: qux1") { - testAst("qux1") - } - - test("jawn test data: qux2") { - testAst("qux2") - } - - test("jawn test data: ugh10k") { - testAst("ugh10k") - } - - // TODO it would be good to test with https://github.com/nst/JSONTestSuite - } - - def testAst(name: String) = { - val input = getResourceAsString(s"jawn/${name}.json") - val expected = jawn.JParser.parseFromString(input).toEither.map(fromJawn) - val got = input.fromJson[Json].map(normalize) - val gotf = s"${name}-got.json" - val expectedf = s"${name}-expected.json" - - def e2s[A, B](e: Either[A, B]) = - e match { - case Left(left) => left.toString - case Right(right) => right.toString - } - if (expected != got) { - writeFile(gotf, e2s(got)) - writeFile(expectedf, e2s(expected)) - } - scala.Predef.assert( - got == expected, - s"dumped .json files, use `cmp <(jq . ${expectedf}) <(jq . ${gotf})`" - ) // errors are too big - } - - // reorder objects to match jawn's lossy AST (and dedupe) - def normalize(ast: Json): Json = - ast match { - case Json.Obj(values) => - Json.Obj( - values - .distinctBy(_._1) - .map { case (k, v) => (k, normalize(v)) } - .sortBy(_._1) - ) - case Json.Arr(values) => Json.Arr(values.map(normalize(_))) - case other => other - } - - def fromJawn(ast: jawn.JValue): Json = - ast match { - case jawn.JNull => Json.Null - case jawn.JTrue => Json.Bool(true) - case jawn.JFalse => Json.Bool(false) - case jawn.JString(s) => Json.Str(s) - case jawn.LongNum(i) => - Json.Num(new java.math.BigDecimal(java.math.BigInteger.valueOf(i))) - case jawn.DoubleNum(d) => Json.Num(new java.math.BigDecimal(d)) - case jawn.DeferLong(i) => - Json.Num(new java.math.BigDecimal(new java.math.BigInteger(i))) - case jawn.DeferNum(n) => Json.Num(new java.math.BigDecimal(n)) - case jawn.JArray(vs) => Json.Arr(Chunk.fromArray(vs).map(fromJawn)) - case jawn.JObject(es) => - Json.Obj(Chunk.fromIterable(es).sortBy(_._1).map { case (k, v) => (k, fromJawn(v)) }) - } - -} diff --git a/src/test/scala/zio/json/EncoderSpec.scala b/src/test/scala/zio/json/EncoderSpec.scala new file mode 100644 index 000000000..cc5f09413 --- /dev/null +++ b/src/test/scala/zio/json/EncoderSpec.scala @@ -0,0 +1,181 @@ +package testzio.json + +import io.circe +import testzio.json.TestUtils._ +import testzio.json.data.geojson.generated._ +import testzio.json.data.googlemaps._ +import testzio.json.data.twitter._ +import zio.json._ +import zio.test.Assertion._ +import zio.test.{ DefaultRunnableSpec, _ } + +object EncoderSpec extends DefaultRunnableSpec { + def spec = + suite("Encoder")( + suite("primitives")( + test("strings") { + assert("hello world".toJson)(equalTo("\"hello world\"")) && + assert("hello\nworld".toJson)(equalTo("\"hello\\nworld\"")) && + assert("hello\rworld".toJson)(equalTo("\"hello\\rworld\"")) && + assert("hello\u0000world".toJson)(equalTo("\"hello\\u0000world\"")) + }, + test("boolean") { + assert(true.toJson)(equalTo("true")) && + assert(false.toJson)(equalTo("false")) + }, + test("char") { + assert('c'.toJson)(equalTo("\"c\"")) && + assert(Symbol("c").toJson)(equalTo("\"c\"")) + }, + test("numerics") { + assert((1: Byte).toJson)(equalTo("1")) && + assert((1: Short).toJson)(equalTo("1")) && + assert((1: Int).toJson)(equalTo("1")) && + assert((1L).toJson)(equalTo("1")) && + assert((new java.math.BigInteger("1")).toJson)(equalTo("1")) && + assert((new java.math.BigInteger("170141183460469231731687303715884105728")).toJson)( + equalTo("170141183460469231731687303715884105728") + ) && + assert((1.0f).toJson)(equalTo("1.0")) && + assert((1.0d).toJson)(equalTo("1.0")) + }, + test("NaN / Infinity") { + assert(Float.NaN.toJson)(equalTo("\"NaN\"")) && + assert(Float.PositiveInfinity.toJson)(equalTo("\"Infinity\"")) && + assert(Float.NegativeInfinity.toJson)(equalTo("\"-Infinity\"")) && + assert(Double.NaN.toJson)(equalTo("\"NaN\"")) && + assert(Double.PositiveInfinity.toJson)(equalTo("\"Infinity\"")) && + assert(Double.NegativeInfinity.toJson)(equalTo("\"-Infinity\"")) + } + ), + test("options") { + assert((None: Option[Int]).toJson)(equalTo("null")) && + assert((Some(1): Option[Int]).toJson)(equalTo("1")) + }, + test("eithers") { + assert((Left(1): Either[Int, Int]).toJson)(equalTo("""{"Left":1}""")) && + assert((Right(1): Either[Int, Int]).toJson)(equalTo("""{"Right":1}""")) && + assert((Left(1): Either[Int, Int]).toJsonPretty)(equalTo("{\n \"Left\" : 1\n}")) && + assert((Right(1): Either[Int, Int]).toJsonPretty)(equalTo("{\n \"Right\" : 1\n}")) + }, + test("collections") { + assert(List[Int]().toJson)(equalTo("[]")) && + assert(List(1, 2, 3).toJson)(equalTo("[1,2,3]")) && + assert(Vector[Int]().toJson)(equalTo("[]")) && + assert(Vector(1, 2, 3).toJson)(equalTo("[1,2,3]")) && + assert(Map[String, String]().toJson)(equalTo("{}")) && + assert(Map("hello" -> "world").toJson)(equalTo("""{"hello":"world"}""")) && + assert(Map("hello" -> Some("world"), "goodbye" -> None).toJson)(equalTo("""{"hello":"world"}""")) && + assert(List[Int]().toJsonPretty)(equalTo("[]")) && + assert(List(1, 2, 3).toJsonPretty)(equalTo("[1, 2, 3]")) && + assert(Vector[Int]().toJsonPretty)(equalTo("[]")) && + assert(Vector(1, 2, 3).toJsonPretty)(equalTo("[1, 2, 3]")) && + assert(Map[String, String]().toJsonPretty)(equalTo("{}")) && + assert(Map("hello" -> "world").toJsonPretty)(equalTo("{\n \"hello\" : \"world\"\n}")) && + assert(Map("hello" -> Some("world"), "goodbye" -> None).toJsonPretty)(equalTo("{\n \"hello\" : \"world\"\n}")) + }, + test("parameterless products") { + import exampleproducts._ + + assert(Parameterless().toJson)(equalTo("{}")) && + assert(Parameterless().toJsonPretty)(equalTo("{}")) + }, + test("tuples") { + assert(("hello", "world").toJson)(equalTo("""["hello","world"]""")) && + assert(("hello", "world").toJsonPretty)(equalTo("""["hello", "world"]""")) + }, + test("products") { + import exampleproducts._ + + assert(OnlyString("foo").toJson)(equalTo("""{"s":"foo"}""")) && + assert(CoupleOfThings(-1, Some(10.0f), false).toJson)(equalTo("""{"j":-1,"f":10.0,"b":false}""")) && + assert(CoupleOfThings(0, None, true).toJson)(equalTo("""{"j":0,"b":true}""")) && + assert(OnlyString("foo").toJsonPretty)(equalTo("{\n \"s\" : \"foo\"\n}")) && + assert(CoupleOfThings(-1, Some(10.0f), false).toJsonPretty)( + equalTo("{\n \"j\" : -1,\n \"f\" : 10.0,\n \"b\" : false\n}") + ) && + assert(CoupleOfThings(0, None, true).toJsonPretty)(equalTo("{\n \"j\" : 0,\n \"b\" : true\n}")) + }, + test("sum encoding") { + import examplesum._ + + assert((Child1(): Parent).toJson)(equalTo("""{"Child1":{}}""")) && + assert((Child2(): Parent).toJson)(equalTo("""{"Cain":{}}""")) && + assert((Child1(): Parent).toJsonPretty)(equalTo("{\n \"Child1\" : {}\n}")) && + assert((Child2(): Parent).toJsonPretty)(equalTo("{\n \"Cain\" : {}\n}")) + }, + test("sum alternative encoding") { + import examplealtsum._ + + // note lack of whitespace on last line + assert((Child1(): Parent).toJson)(equalTo("""{"hint":"Child1"}""")) && + assert((Child2(None): Parent).toJson)(equalTo("""{"hint":"Abel"}""")) && + assert((Child2(Some("hello")): Parent).toJson)(equalTo("""{"hint":"Abel","s":"hello"}""")) && + assert((Child1(): Parent).toJsonPretty)(equalTo("{\n \"hint\" : \"Child1\"}")) && + assert((Child2(None): Parent).toJsonPretty)(equalTo("{\n \"hint\" : \"Abel\"}")) && + assert((Child2(Some("hello")): Parent).toJsonPretty)( + equalTo("{\n \"hint\" : \"Abel\",\n \"s\" : \"hello\"\n}") + ) + }, + suite("roundtrip")( + testRoundTrip[DistanceMatrix]("google_maps_api_response"), + testRoundTrip[List[Tweet]]("twitter_api_response"), + testRoundTrip[GeoJSON]("che.geo") + ) + ) + + def testRoundTrip[A: circe.Decoder: JsonEncoder](label: String) = + testM(label) { + getResourceAsStringM(s"$label.json").map { input => + val circeDecoded = circe.parser.decode[A](input) + val circeRecoded = circeDecoded.toOption.get.toJson + val recodedPretty = circeDecoded.toOption.get.toJson + + assert(circe.parser.decode[A](circeRecoded))(equalTo(circeDecoded)) && + assert(circe.parser.decode[A](recodedPretty))(equalTo(circeDecoded)) + } + } + + object exampleproducts { + case class Parameterless() + object Parameterless { + implicit val encoder: JsonEncoder[Parameterless] = + DeriveJsonEncoder.gen[Parameterless] + } + + case class OnlyString(s: String) + object OnlyString { + implicit val encoder: JsonEncoder[OnlyString] = + DeriveJsonEncoder.gen[OnlyString] + } + + case class CoupleOfThings(@field("j") i: Int, f: Option[Float], b: Boolean) + object CoupleOfThings { + implicit val encoder: JsonEncoder[CoupleOfThings] = + DeriveJsonEncoder.gen[CoupleOfThings] + } + } + + object examplesum { + + sealed abstract class Parent + object Parent { + implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] + } + case class Child1() extends Parent + @hint("Cain") + case class Child2() extends Parent + } + + object examplealtsum { + @discriminator("hint") + sealed abstract class Parent + object Parent { + implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] + } + + case class Child1() extends Parent + @hint("Abel") + case class Child2(s: Option[String]) extends Parent + } +} diff --git a/src/test/scala/zio/json/EncoderTest.scala b/src/test/scala/zio/json/EncoderTest.scala deleted file mode 100644 index c80f48bd9..000000000 --- a/src/test/scala/zio/json/EncoderTest.scala +++ /dev/null @@ -1,203 +0,0 @@ -package testzio.json - -import scala.collection.immutable - -import io.circe -import zio.json._ -import TestUtils._ -import scalaprops._ -import Property.{ implies, prop, property } -import scala.collection.mutable - -import utest._ -import testzio.json.data.googlemaps._ -import testzio.json.data.twitter._ - -// testOnly *EncoderTest -object EncoderTest extends TestSuite { - - object exampleproducts { - case class Parameterless() - object Parameterless { - implicit val encoder: JsonEncoder[Parameterless] = - DeriveJsonEncoder.gen[Parameterless] - } - - case class OnlyString(s: String) - object OnlyString { - implicit val encoder: JsonEncoder[OnlyString] = - DeriveJsonEncoder.gen[OnlyString] - } - - case class CoupleOfThings(@field("j") i: Int, f: Option[Float], b: Boolean) - object CoupleOfThings { - implicit val encoder: JsonEncoder[CoupleOfThings] = - DeriveJsonEncoder.gen[CoupleOfThings] - } - } - - object examplesum { - - sealed abstract class Parent - object Parent { - implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] - } - case class Child1() extends Parent - @hint("Cain") - case class Child2() extends Parent - } - - object examplealtsum { - - @discriminator("hint") - sealed abstract class Parent - object Parent { - implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] - } - case class Child1() extends Parent - @hint("Abel") - case class Child2(s: Option[String]) extends Parent - } - - val tests = Tests { - test("primitives") { - "hello world".toJson ==> "\"hello world\"" - "hello\nworld".toJson ==> "\"hello\\nworld\"" - "hello\rworld".toJson ==> "\"hello\\rworld\"" - "hello\u0000world".toJson ==> "\"hello\\u0000world\"" - - true.toJson ==> "true" - false.toJson ==> "false" - 'c'.toJson ==> "\"c\"" - Symbol("c").toJson ==> "\"c\"" - - (1: Byte).toJson ==> "1" - (1: Short).toJson ==> "1" - (1: Int).toJson ==> "1" - (1L).toJson ==> "1" - (new java.math.BigInteger("1")).toJson ==> "1" - (new java.math.BigInteger("170141183460469231731687303715884105728").toJson ==> "170141183460469231731687303715884105728") - - (1.0f).toJson ==> "1.0" - (1.0d).toJson ==> "1.0" - - Float.NaN.toJson ==> "\"NaN\"" - Float.PositiveInfinity.toJson ==> "\"Infinity\"" - Float.NegativeInfinity.toJson ==> "\"-Infinity\"" - - Double.NaN.toJson ==> "\"NaN\"" - Double.PositiveInfinity.toJson ==> "\"Infinity\"" - Double.NegativeInfinity.toJson ==> "\"-Infinity\"" - } - - test("options") { - (None: Option[Int]).toJson ==> "null" - (Some(1): Option[Int]).toJson ==> "1" - } - - test("eithers") { - (Left(1): Either[Int, Int]).toJson ==> """{"Left":1}""" - (Right(1): Either[Int, Int]).toJson ==> """{"Right":1}""" - - (Left(1): Either[Int, Int]).toJsonPretty ==> "{\n \"Left\" : 1\n}" - (Right(1): Either[Int, Int]).toJsonPretty ==> "{\n \"Right\" : 1\n}" - } - - test("collections") { - List[Int]().toJson ==> "[]" - List(1, 2, 3).toJson ==> "[1,2,3]" - Vector[Int]().toJson ==> "[]" - Vector(1, 2, 3).toJson ==> "[1,2,3]" - - Map[String, String]().toJson ==> "{}" - Map("hello" -> "world").toJson ==> """{"hello":"world"}""" - Map("hello" -> Some("world"), "goodbye" -> None).toJson ==> """{"hello":"world"}""" - - List[Int]().toJsonPretty ==> "[]" - List(1, 2, 3).toJsonPretty ==> "[1, 2, 3]" - Vector[Int]().toJsonPretty ==> "[]" - Vector(1, 2, 3).toJsonPretty ==> "[1, 2, 3]" - - Map[String, String]().toJsonPretty ==> "{}" - Map("hello" -> "world").toJsonPretty ==> "{\n \"hello\" : \"world\"\n}" - Map("hello" -> Some("world"), "goodbye" -> None).toJsonPretty ==> "{\n \"hello\" : \"world\"\n}" - } - - test("parameterless products") { - import exampleproducts._ - - Parameterless().toJson ==> "{}" - - Parameterless().toJsonPretty ==> "{}" - } - - test("tuples") { - ("hello", "world").toJson ==> """["hello","world"]""" - - ("hello", "world").toJsonPretty ==> """["hello", "world"]""" - } - - test("products") { - import exampleproducts._ - - OnlyString("foo").toJson ==> """{"s":"foo"}""" - - CoupleOfThings(-1, Some(10.0f), false).toJson ==> """{"j":-1,"f":10.0,"b":false}""" - CoupleOfThings(0, None, true).toJson ==> """{"j":0,"b":true}""" - - OnlyString("foo").toJsonPretty ==> "{\n \"s\" : \"foo\"\n}" - - CoupleOfThings(-1, Some(10.0f), false).toJsonPretty ==> "{\n \"j\" : -1,\n \"f\" : 10.0,\n \"b\" : false\n}" - CoupleOfThings(0, None, true).toJsonPretty ==> "{\n \"j\" : 0,\n \"b\" : true\n}" - } - - test("sum encoding") { - import examplesum._ - - (Child1(): Parent).toJson ==> """{"Child1":{}}""" - (Child2(): Parent).toJson ==> """{"Cain":{}}""" - - (Child1(): Parent).toJsonPretty ==> "{\n \"Child1\" : {}\n}" - (Child2(): Parent).toJsonPretty ==> "{\n \"Cain\" : {}\n}" - } - - test("sum alternative encoding") { - import examplealtsum._ - - (Child1(): Parent).toJson ==> """{"hint":"Child1"}""" - (Child2(None): Parent).toJson ==> """{"hint":"Abel"}""" - (Child2(Some("hello")): Parent).toJson ==> """{"hint":"Abel","s":"hello"}""" - - // note lack of whitespace on last line - (Child1(): Parent).toJsonPretty ==> "{\n \"hint\" : \"Child1\"}" - (Child2(None): Parent).toJsonPretty ==> "{\n \"hint\" : \"Abel\"}" - (Child2(Some("hello")): Parent).toJsonPretty ==> "{\n \"hint\" : \"Abel\",\n \"s\" : \"hello\"\n}" - } - - // using circe to avoid entwining this test on zio.JsonDecoder - def testRoundtrip[A: circe.Decoder: JsonEncoder](res: String) = { - val jsonString = getResourceAsString(res) - val decoded = circe.parser.decode[A](jsonString) - val recoded = decoded.toOption.get.toJson - circe.parser.decode[A](recoded) ==> decoded - - val recodedPretty = decoded.toOption.get.toJson - circe.parser.decode[A](recodedPretty) ==> decoded - } - - test("Google Maps") { - testRoundtrip[DistanceMatrix]("google_maps_api_response.json") - } - - test("Twitter") { - testRoundtrip[List[Tweet]]("twitter_api_response.json") - } - - test("GeoJSON") { - import testzio.json.data.geojson.generated._ - - testRoundtrip[GeoJSON]("che.geo.json") - } - } - -} diff --git a/src/test/scala/zio/json/RoundTripSpec.scala b/src/test/scala/zio/json/RoundTripSpec.scala new file mode 100644 index 000000000..5eb2efa02 --- /dev/null +++ b/src/test/scala/zio/json/RoundTripSpec.scala @@ -0,0 +1,61 @@ +package testzio.json + +import zio.json._ +import zio.json.ast.Json +import zio.random.Random +import zio.test.Assertion._ +import zio.test._ +import TestUtils._ + +object RoundTripSpec extends DefaultRunnableSpec { + def spec = + suite("RoundTrip")( + testM("booleans") { + check(Gen.boolean)(assertRoundtrips) + }, + testM("bytes") { + check(Gen.anyByte)(assertRoundtrips) + }, + testM("shorts") { + check(Gen.anyShort)(assertRoundtrips) + }, + testM("ints") { + check(Gen.anyInt)(assertRoundtrips) + }, + testM("longs") { + check(Gen.anyLong)(assertRoundtrips) + }, + testM("bigInts") { + check(genBigInteger)(assertRoundtrips) + }, + testM("floats") { + // NaN / Infinity is tested manually, because of == semantics + check(Gen.anyFloat.filter(_.isFinite))(assertRoundtrips) + }, + testM("doubles") { + // NaN / Infinity is tested manually, because of == semantics + check(Gen.anyDouble.filter(_.isFinite))(assertRoundtrips) + }, + testM("AST") { + check(genAst)(assertRoundtrips) + } + ) + + lazy val genAst: Gen[Random with Sized, Json] = + Gen.size.flatMap { size => + val entry = genUsAsciiString <*> genAst + val sz = 0 min (size - 1) + val obj = Gen.chunkOfN(sz)(entry).map(Json.Obj(_)) + val arr = Gen.chunkOfN(sz)(genAst).map(Json.Arr(_)) + val boo = Gen.boolean.map(Json.Bool(_)) + val str = genUsAsciiString.map(Json.Str(_)) + val num = genBigDecimal.map(Json.Num(_)) + val nul = Gen.const(Json.Null) + + Gen.oneOf(obj, arr, boo, str, num) + } + + private def assertRoundtrips[A: JsonEncoder: JsonDecoder](a: A) = + assert(a.toJson.fromJson[A])(isRight(equalTo(a))) && + assert(a.toJsonPretty.fromJson[A])(isRight(equalTo(a))) +} diff --git a/src/test/scala/zio/json/RoundtripTest.scala b/src/test/scala/zio/json/RoundtripTest.scala deleted file mode 100644 index 705f562b0..000000000 --- a/src/test/scala/zio/json/RoundtripTest.scala +++ /dev/null @@ -1,75 +0,0 @@ -package testzio.json - -import scala.collection.immutable - -import zio.Chunk -import zio.json._ -import zio.json.ast._ -import scalaprops._ -import Property.{ implies, prop, property } -import scala.collection.mutable - -// testOnly *RoundtripTest -object RoundtripTest extends Scalaprops { - - def roundtrip[A: JsonEncoder: JsonDecoder](a: A) = - prop(a.toJson.fromJson[A] == Right(a)) and - prop(a.toJsonPretty.fromJson[A] == Right(a)) - - // arbitrary strings are not guaranteed to roundtrip due to normalisation of - // some unicode characters, but we could still test this on a subset of - // strings if we wanted to create the Gens - - val booleans = property { i: Boolean => roundtrip(i) } - - val bytes = property { i: Byte => roundtrip(i) } - val shorts = property { i: Short => roundtrip(i) } - val ints = property { i: Int => roundtrip(i) } - val longs = property { i: Long => roundtrip(i) } - val bigints = property { i: java.math.BigInteger => implies(i.bitLength < 128, roundtrip(i)) } - - implicit val floatShrinker: Shrink[Float] = Shrink.empty - implicit val doubleShrinker: Shrink[Double] = Shrink.empty - - // NaN / Infinity is tested manually, because of == semantics - val floats = property { i: Float => implies(i.isFinite, roundtrip(i)) } - val doubles = property { i: Double => implies(i.isFinite, roundtrip(i)) } - - implicit lazy val astGen: Gen[Json] = Gen.sized { size => - val entry: Gen[(String, Json)] = Gen.delay(Gen.apply2(Gen.asciiString, astGen)((a, b) => (a, b))) - // objects and arrays should get smaller with depth to avoid infinite recursion - val size_ = 0 min (size - 1) - val obj: Gen[Json] = Gen.delay(Gen.listOfN(size_, entry)).map(Chunk.fromIterable(_)).map(Json.Obj(_)) - val arr: Gen[Json] = Gen.delay(Gen.listOfN(size_, astGen)).map(Chunk.fromIterable(_)).map(Json.Arr(_)) - val boo: Gen[Json] = Gen[Boolean].map(Json.Bool(_)) - val str: Gen[Json] = Gen.asciiString.map(Json.Str(_)) - val num: Gen[Json] = for { - num <- Gen[java.math.BigDecimal] - // fallback to null if we ever get a number that is too big - } yield - if (num.unscaledValue.bitLength > 128) Json.Null - else Json.Num(num) - - val nul: Gen[Json] = Gen.value(Json.Null) - - Gen.oneOf(obj, arr, boo, str, num) - } - - implicit val strShrinker: Shrink[String] = Shrink.shrink { txt => - if (txt.isEmpty) Stream.empty[String] - else Stream(txt.drop(1), txt.reverse.drop(1).reverse) - } - - implicit lazy val astShrinker: Shrink[Json] = Shrink.shrink { - case Json.Obj(entries) => - Shrink.list[(String, Json)].apply(entries.toList).map(Chunk.fromIterable(_)).map(Json.Obj(_)) - case Json.Arr(entries) => Shrink.list[Json].apply(entries.toList).map(Chunk.fromIterable(_)).map(Json.Arr(_)) - case Json.Bool(_) => Stream.empty[Json] - case Json.Str(txt) => strShrinker(txt).map(Json.Str(_)) - case Json.Num(_) => Stream.empty[Json] - case Json.Null => Stream.empty[Json] - } - - val asts = property { i: Json => roundtrip(i) } - -} diff --git a/src/test/scala/zio/json/TestUtils.scala b/src/test/scala/zio/json/TestUtils.scala index d6a52ee78..69bfd13d2 100644 --- a/src/test/scala/zio/json/TestUtils.scala +++ b/src/test/scala/zio/json/TestUtils.scala @@ -1,23 +1,34 @@ package testzio.json -import java.nio.CharBuffer +import zio._ +import zio.blocking._ +import zio.stream._ +import java.io.IOException +import java.io.FileNotFoundException + +import zio.test.Gen object TestUtils { - // by plokhotnyuk - def zeroHashCodeStrings: Iterator[String] = { - def charAndHash(h: Int): Iterator[(Char, Int)] = ('!' to '~').iterator.map(ch => (ch, (h + ch) * 31)) - - for { - (ch0, h0) <- charAndHash(0) - (ch1, h1) <- charAndHash(h0) - (ch2, h2) <- charAndHash(h1) if (((h2 + 32) * 923521) ^ ((h2 + 127) * 923521)) < 0 - (ch3, h3) <- charAndHash(h2) if (((h3 + 32) * 29791) ^ ((h3 + 127) * 29791)) < 0 - (ch4, h4) <- charAndHash(h3) if (((h4 + 32) * 961) ^ ((h4 + 127) * 961)) < 0 - (ch5, h5) <- charAndHash(h4) if (((h5 + 32) * 31) ^ ((h5 + 127) * 31)) < 0 - (ch6, h6) <- charAndHash(h5) if ((h6 + 32) ^ (h6 + 127)) < 0 - (ch7, _) <- charAndHash(h6) if h6 + ch7 == 0 - } yield new String(Array(ch0, ch1, ch2, ch3, ch4, ch5, ch6, ch7)) - } + val genBigInteger = + Gen + .bigInt((BigInt(2).pow(128) - 1) * -1, BigInt(2).pow(128) - 1) + .map(_.bigInteger) + .filter(_.bitLength < 128) + + val genBigDecimal = + Gen + .bigDecimal((BigDecimal(2).pow(128) - 1) * -1, BigDecimal(2).pow(128) - 1) + .map(_.bigDecimal) + .filter(_.toBigInteger.bitLength < 128) + + // Something seems to be up with zio-test’s Gen.usASCII, it returns + // strings like 'Chunk(<>)' (Chunk#toString?) containing any ASCII chars + // This generator matches ScalaProps + val genUsAsciiString = + Gen.string(Gen.oneOf(Gen.char('!', '~'))) + + val genAlphaLowerString = + Gen.string(Gen.oneOf(Gen.char('a', 'z'))) def writeFile(path: String, s: String): Unit = { val bw = new java.io.BufferedWriter(new java.io.FileWriter(path)) @@ -38,6 +49,19 @@ object TestUtils { } finally is.close() } + def getResourceAsStringM(res: String): ZIO[Blocking, IOException, String] = + ZStream.managed { + val acquire = effectBlockingIO(getClass.getClassLoader.getResourceAsStream(res)).flatMap { x => + if (x == null) + ZIO.fail(new FileNotFoundException(s"No such resource: '$res'")) + else + ZIO.succeed(x) + } + + ZManaged.fromAutoCloseable(acquire) + }.flatMap(inputStream => ZStream.fromInputStream(inputStream).transduce(ZTransducer.utf8Decode)) + .fold("")(_ ++ _) + def asChars(str: String): CharSequence = new zio.json.internal.FastCharSequence(str.toCharArray) diff --git a/src/test/scala/zio/json/compat/RefinedSpec.scala b/src/test/scala/zio/json/compat/RefinedSpec.scala new file mode 100644 index 000000000..40adf0865 --- /dev/null +++ b/src/test/scala/zio/json/compat/RefinedSpec.scala @@ -0,0 +1,30 @@ +package testzio.json.compat + +import zio.json._ +import zio.test._ + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.collection.NonEmpty +import eu.timepit.refined.auto._ +import zio.json.compat.refined._ +import zio.test.Assertion._ + +import zio.test._ + +object RefinedSpec extends DefaultRunnableSpec { + def spec = + suite("Refined")( + test("Refined") { + assert("""{"name":""}""".fromJson[Person])(isLeft(equalTo(".name(Predicate isEmpty() did not fail.)"))) && + assert("""{"name":"fommil"}""".fromJson[Person])(isRight(equalTo(Person("fommil")))) && + assert(Person("fommil").toJson)(equalTo("""{"name":"fommil"}""")) + } + ) + + case class Person(name: String Refined NonEmpty) + + object Person { + implicit val decoder: JsonDecoder[Person] = DeriveJsonDecoder.gen[Person] + implicit val encoder: JsonEncoder[Person] = DeriveJsonEncoder.gen[Person] + } +} diff --git a/src/test/scala/zio/json/compat/RefinedTest.scala b/src/test/scala/zio/json/compat/RefinedTest.scala deleted file mode 100644 index 9812b1f46..000000000 --- a/src/test/scala/zio/json/compat/RefinedTest.scala +++ /dev/null @@ -1,34 +0,0 @@ -package testzio.json.compat - -import zio.json -import zio.json._ -import testzio.json.TestUtils._ -import eu.timepit.refined.api.{ Refined } - -import eu.timepit.refined.api.Refined -import eu.timepit.refined.collection.NonEmpty -import eu.timepit.refined.auto._ - -import utest._ -import zio.json.compat.refined._ - -// testOnly *RefinedTest -object RefinedTest extends TestSuite { - - case class Person(name: String Refined NonEmpty) - - object Person { - implicit val decoder: JsonDecoder[Person] = DeriveJsonDecoder.gen[Person] - implicit val encoder: JsonEncoder[Person] = DeriveJsonEncoder.gen[Person] - } - - val tests = Tests { - test("Refined") { - """{"name":""}""".fromJson[Person] ==> Left(".name(Predicate isEmpty() did not fail.)") - """{"name":"fommil"}""".fromJson[Person] ==> Right(Person("fommil")) - - Person("fommil").toJson ==> """{"name":"fommil"}""" - } - } - -} diff --git a/src/test/scala/zio/json/compat/ScalazSpec.scala b/src/test/scala/zio/json/compat/ScalazSpec.scala new file mode 100644 index 000000000..21ea0ba86 --- /dev/null +++ b/src/test/scala/zio/json/compat/ScalazSpec.scala @@ -0,0 +1,21 @@ +package testzio.json.compat + +import _root_.scalaz._ + +import zio.json._ +import zio.json.compat.scalaz._ +import zio.test.Assertion._ +import zio.test._ + +object ScalazSpec extends DefaultRunnableSpec { + def spec = + suite("Scalaz")( + test("Scalaz") { + assert(IList[Int]().toJson)(equalTo("[]")) && + assert(IList(1, 2, 3).toJson)(equalTo("[1,2,3]")) && + assert(IList[Int]().toJsonPretty)(equalTo("[]")) && + assert(IList(1, 2, 3).toJsonPretty)(equalTo("[1, 2, 3]")) && + assert("""[1,2,3]""".fromJson[IList[Int]])(isRight(equalTo(IList(1, 2, 3)))) + } + ) +} diff --git a/src/test/scala/zio/json/compat/ScalazTest.scala b/src/test/scala/zio/json/compat/ScalazTest.scala deleted file mode 100644 index aa35c40e7..000000000 --- a/src/test/scala/zio/json/compat/ScalazTest.scala +++ /dev/null @@ -1,25 +0,0 @@ -package testzio.json.compat - -import zio.json._ -import testzio.json.TestUtils._ - -import utest._ -import _root_.scalaz._ -import zio.json.compat.scalaz._ - -// testOnly *ScalazTest -object ScalazTest extends TestSuite { - - val tests = Tests { - test("IList") { - IList[Int]().toJson ==> "[]" - IList(1, 2, 3).toJson ==> "[1,2,3]" - - IList[Int]().toJsonPretty ==> "[]" - IList(1, 2, 3).toJsonPretty ==> "[1, 2, 3]" - - """[1,2,3]""".fromJson[IList[Int]] ==> Right(IList(1, 2, 3)) - } - } - -} diff --git a/src/test/scala/zio/json/internal/SafeNumbersSpec.scala b/src/test/scala/zio/json/internal/SafeNumbersSpec.scala new file mode 100644 index 000000000..1d5ae83fc --- /dev/null +++ b/src/test/scala/zio/json/internal/SafeNumbersSpec.scala @@ -0,0 +1,256 @@ +package testzio.json.internal + +import zio.json.internal._ +import zio.test.Assertion._ +import zio.test.{ DefaultRunnableSpec, _ } +import testzio.json.TestUtils._ + +object SafeNumbersSpec extends DefaultRunnableSpec { + def spec = + suite("SafeNumbers")( + testM("valid big decimals") { + check(genBigDecimal)(i => assert(SafeNumbers.bigDecimal(i.toString, 2048))(isSome(equalTo(i)))) + }, + test("invalid big decimals") { + val invalidBigDecimalEdgeCases = List( + "N", + "Inf", + "-NaN", + "+NaN", + "e1", + "1.1.1", + "1 ", + "NaN", + "Infinity", + "+Infinity", + "-Infinity" + ).map(s => SafeNumbers.bigDecimal(s)) + + assert(invalidBigDecimalEdgeCases)(forall(isNone)) + }, + testM("valid big decimal edge cases") { + val invalidBigDecimalEdgeCases = List( + ".0", + "-.0", + "0", + "0.0", + "-0.0", // zeroes + "0000.1", + "0.00001", + "000.00001000" // various trailing zeros, should be preserved + ) + + check(Gen.fromIterable(invalidBigDecimalEdgeCases)) { s => + assert(SafeNumbers.bigDecimal(s).map(_.toString))( + isSome( + equalTo((new java.math.BigDecimal(s)).toString) + ) + ) + } + }, + testM("invalid BigDecimal text") { + check(genAlphaLowerString)(s => assert(SafeNumbers.bigDecimal(s))(isNone)) + }, + testM("valid BigInteger edge cases") { + val inputs = List( + "00", + "01", + "0000001", + "-9223372036854775807", + "9223372036854775806", + "-9223372036854775809", + "9223372036854775808" + ) + + check(Gen.fromIterable(inputs)) { s => + assert(SafeNumbers.bigInteger(s))( + isSome( + equalTo((new java.math.BigInteger(s))) + ) + ) + } + }, + testM("invalid BigInteger edge cases") { + val inputs = List("0foo", "01foo", "0.1", "", "1 ") + + check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInteger(s))(isNone)) + }, + testM("valid big Integer") { + check(genBigInteger)(i => assert(SafeNumbers.bigInteger(i.toString, 2048))(isSome(equalTo(i)))) + }, + testM("invalid BigInteger") { + check(genAlphaLowerString)(s => assert(SafeNumbers.bigInteger(s))(isNone)) + }, + testM("valid Byte") { + check(Gen.byte(Byte.MinValue, Byte.MaxValue)) { b => + assert(SafeNumbers.byte(b.toString))(equalTo(ByteSome(b))) + } + }, + testM("invalid Byte (numbers)") { + check(Gen.anyLong.filter(i => i < Byte.MinValue || i > Byte.MaxValue)) { b => + assert(SafeNumbers.byte(b.toString))(equalTo(ByteNone)) + } + }, + testM("invalid Byte (text)") { + check(genAlphaLowerString)(b => assert(SafeNumbers.byte(b.toString))(equalTo(ByteNone))) + }, + suite("Double")( + testM("valid") { + check(Gen.anyDouble.filterNot(_.isNaN)) { d => + assert(SafeNumbers.double(d.toString))(equalTo(DoubleSome(d))) + } + }, + testM("valid (from Int)") { + check(Gen.anyInt)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i)))) + }, + testM("valid (from Long)") { + check(Gen.anyLong)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i.toDouble)))) + }, + testM("invalid edge cases") { + val inputs = List("N", "Inf", "-NaN", "+NaN", "e1", "1.1.1", "1 ") + + check(Gen.fromIterable(inputs))(i => assert(SafeNumbers.double(i))(equalTo(DoubleNone))) + }, + testM("valid edge cases") { + val inputs = List( + ".0", + "-.0", + "0", + "0.0", + "-0.0", // zeroes + "0000.1", + "0.00001", + "000.00001000", // trailing zeros + "NaN", + "92233720368547758070", // overflows a Long significand + "Infinity", + "+Infinity", + "-Infinity", + "3.976210887433566E-281" // rounds if a naive scaling is used + ) + + check(Gen.fromIterable(inputs)) { s => + // better to do the comparison on strings to deal with NaNs + assert(SafeNumbers.double(s).toString)( + equalTo(DoubleSome(s.toDouble).toString) + ) + } + }, + test("valid magic doubles") { + assert(SafeNumbers.double("NaN"))(not(equalTo(DoubleNone))) && + assert(SafeNumbers.double("Infinity"))(not(equalTo(DoubleNone))) && + assert(SafeNumbers.double("+Infinity"))(not(equalTo(DoubleNone))) && + assert(SafeNumbers.double("-Infinity"))(not(equalTo(DoubleNone))) + }, + testM("invalid doubles (text)") { + check(genAlphaLowerString)(s => assert(SafeNumbers.double(s))(equalTo(DoubleNone))) + } + ), + suite("Float")( + testM("valid") { + check(Gen.anyFloat.filterNot(_.isNaN))(d => assert(SafeNumbers.float(d.toString))(equalTo(FloatSome(d)))) + }, + testM("valid (from Int)") { + check(Gen.anyInt)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.toFloat)))) + }, + testM("valid (from Long)") { + check(Gen.anyLong)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.toFloat)))) + }, + testM("invalid edge cases") { + val inputs = List("N", "Inf", "-NaN", "+NaN", "e1", "1.1.1") + + check(Gen.fromIterable(inputs))(i => assert(SafeNumbers.float(i))(equalTo(FloatNone))) + }, + testM("valid edge cases") { + val inputs = List( + ".0", + "-.0", + "0", + "0.0", + "-0.0", // zeroes + "0000.1", + "0.00001", + "000.00001000", // trailing zeros + "NaN", + "92233720368547758070", // overflows a Long significand + "Infinity", + "+Infinity", + "-Infinity" + ) + + check(Gen.fromIterable(inputs)) { s => + // better to do the comparison on strings to deal with NaNs + assert(SafeNumbers.float(s).toString)( + equalTo(FloatSome(s.toFloat).toString) + ) + } + }, + testM("valid (from Double)") { + check(Gen.anyDouble.filterNot(_.isNaN)) { d => + assert(SafeNumbers.float(d.toString))(equalTo(FloatSome(d.toFloat))) + } + }, + testM("invalid float (text)") { + check(genAlphaLowerString)(s => assert(SafeNumbers.float(s))(equalTo(FloatNone))) + } + ), + suite("Int")( + testM("valid") { + check(Gen.anyInt)(d => assert(SafeNumbers.int(d.toString))(equalTo(IntSome(d)))) + }, + testM("invalid (out of range)") { + check(Gen.anyLong.filter(i => i < Int.MinValue || i > Int.MaxValue))(d => + assert(SafeNumbers.int(d.toString))(equalTo(IntNone)) + ) + }, + testM("invalid (text)") { + check(genAlphaLowerString)(s => assert(SafeNumbers.int(s))(equalTo(IntNone))) + } + ), + suite("Long")( + testM("valid edge cases") { + val input = List("00", "01", "0000001", "-9223372036854775807", "9223372036854775806") + + check(Gen.fromIterable(input))(x => assert(SafeNumbers.long(x))(equalTo(LongSome(x.toLong)))) + }, + testM("in valid edge cases") { + val input = List( + "0foo", + "01foo", + "0.1", + "", + "1 ", + "-9223372036854775809", + "9223372036854775808" + ) + + check(Gen.fromIterable(input))(x => assert(SafeNumbers.long(x))(equalTo(LongNone))) + }, + testM("valid") { + check(Gen.anyLong)(d => assert(SafeNumbers.long(d.toString))(equalTo(LongSome(d)))) + }, + testM("invalid (out of range)") { + val outOfRange = genBigInteger + .filter(_.bitLength > 63) + + check(outOfRange)(x => assert(SafeNumbers.long(x.toString))(equalTo(LongNone))) + }, + testM("invalid (text)") { + check(genAlphaLowerString)(s => assert(SafeNumbers.long(s))(equalTo(LongNone))) + } + ), + suite("Short")( + testM("valid") { + check(Gen.anyShort)(d => assert(SafeNumbers.short(d.toString))(equalTo(ShortSome(d)))) + }, + testM("invalid (out of range)") { + check(Gen.anyLong.filter(i => i < Short.MinValue || i > Short.MaxValue))(d => + assert(SafeNumbers.short(d.toString))(equalTo(ShortNone)) + ) + }, + testM("invalid (text)") { + check(genAlphaLowerString)(s => assert(SafeNumbers.short(s))(equalTo(ShortNone))) + } + ) + ) +} diff --git a/src/test/scala/zio/json/internal/SafeNumbersTest.scala b/src/test/scala/zio/json/internal/SafeNumbersTest.scala deleted file mode 100644 index e7b78bdc3..000000000 --- a/src/test/scala/zio/json/internal/SafeNumbersTest.scala +++ /dev/null @@ -1,236 +0,0 @@ -package testzio.json.internal - -import zio.json.internal._ -import scalaprops._ -import Property.{ implies, prop, property } - -// testOnly *SafeNumbersTest* -object SafeNumbersTest extends Scalaprops { - implicit def shrinker[A]: Shrink[A] = Shrink.empty[A] - - val validBigDecimal = property { i: java.math.BigDecimal => - prop(SafeNumbers.bigDecimal(i.toString, 2048) == Some(i)) - } - - val invalidBigDecimalEdgeCases = - List( - "N", - "Inf", - "-NaN", - "+NaN", - "e1", - "1.1.1", - "1 ", - "NaN", - "Infinity", - "+Infinity", - "-Infinity" - ).map(s => prop(SafeNumbers.bigDecimal(s) == None)).reduce(_ and _) - - val validBigDecimalEdgeCases = - List( - ".0", - "-.0", - "0", - "0.0", - "-0.0", // zeroes - "0000.1", - "0.00001", - "000.00001000" // various trailing zeros, should be preserved - ).map { s => - (prop( - SafeNumbers.bigDecimal(s).toString == Some( - new java.math.BigDecimal(s) - ).toString - )) - }.reduce(_ and _) - - val invalidBigDecimalText = property { s: String => prop(SafeNumbers.bigDecimal(s) == None) }( - Gen.alphaLowerString, - Shrink.empty - ) - - val validBigIntegerEdgeCases = - List( - "00", - "01", - "0000001", - "-9223372036854775807", - "9223372036854775806", - "-9223372036854775809", - "9223372036854775808" - ).map(s => prop(SafeNumbers.bigInteger(s) == Some(new java.math.BigInteger(s)))) - .reduce(_ and _) - - val invalidBigIntegerEdgeCases = - prop( - List("0foo", "01foo", "0.1", "", "1 ") - .map(SafeNumbers.bigInteger(_)) - .forall(_.isEmpty) - ) - - val validBigInteger = property { i: java.math.BigInteger => - prop(SafeNumbers.bigInteger(i.toString, 2048) == Some(i)) - } - - val invalidBigIntegerText = property { s: String => prop(SafeNumbers.bigInteger(s) == None) }( - Gen.alphaLowerString, - Shrink.empty - ) - - val validByte = property { i: Byte => prop(SafeNumbers.byte(i.toString) == ByteSome(i)) } - - val invalidByte = property { i: Long => - implies( - i < Byte.MinValue || Byte.MaxValue < i, - prop(SafeNumbers.byte(i.toString) == ByteNone) - ) - } - - val invalidText = property { s: String => prop(SafeNumbers.byte(s) == ByteNone) }(Gen.alphaLowerString, Shrink.empty) - - val validDouble = property { i: Double => implies(!i.isNaN, prop(SafeNumbers.double(i.toString) == DoubleSome(i))) } - - val validDoubleFromInt = property { i: Int => prop(SafeNumbers.double(i.toString) == DoubleSome(i.toDouble)) } - - val validDoubleFromLong = property { i: Long => prop(SafeNumbers.double(i.toString) == DoubleSome(i.toDouble)) } - - val invalidDoubleEdgeCases = - List("N", "Inf", "-NaN", "+NaN", "e1", "1.1.1", "1 ") - .map(s => prop(SafeNumbers.double(s) == DoubleNone)) - .reduce(_ and _) - - val validDoubleEdgeCases = - List( - ".0", - "-.0", - "0", - "0.0", - "-0.0", // zeroes - "0000.1", - "0.00001", - "000.00001000", // trailing zeros - "NaN", - "92233720368547758070", // overflows a Long significand - "Infinity", - "+Infinity", - "-Infinity", - "3.976210887433566E-281" // rounds if a naive scaling is used - ).map { s => - // better to do the comparison on strings to deal with NaNs - (prop( - SafeNumbers.double(s).toString == DoubleSome(s.toDouble).toString - )) - }.reduce(_ and _) - - val validMagicDouble = - List("NaN", "Infinity", "+Infinity", "-Infinity") - .map(SafeNumbers.double(_)) - .forall(!_.isEmpty) - - val invalidDoubleText = property { s: String => prop(SafeNumbers.double(s) == DoubleNone) }( - Gen.alphaLowerString, - Shrink.empty - ) - - val validFloat = property { i: Float => implies(!i.isNaN, prop(SafeNumbers.float(i.toString) == FloatSome(i))) } - - val validFloatFromInt = property { i: Int => prop(SafeNumbers.float(i.toString) == FloatSome(i.toFloat)) } - - val validFloatFromLong = property { i: Long => prop(SafeNumbers.float(i.toString) == FloatSome(i.toFloat)) } - - // note that in a stream, 1.1.1 may parse "1.1" leaving ".1" - val invalidFloatEdgeCases = - List("N", "Inf", "-NaN", "+NaN", "e1", "1.1.1") - .map(s => prop(SafeNumbers.float(s) == FloatNone)) - .reduce(_ and _) - - val validFloatEdgeCases = - List( - ".0", - "-.0", - "0", - "0.0", - "-0.0", // zeroes - "0000.1", - "0.00001", - "000.00001000", // trailing zeros - "NaN", - "92233720368547758070", // overflows a Long significand - "Infinity", - "+Infinity", - "-Infinity" - ).map { s => - // better to do the comparison on strings to deal with NaNs - prop(SafeNumbers.float(s).toString == FloatSome(s.toFloat).toString) - }.reduce(_ and _) - - val validFloatFromDouble = property { i: Double => - implies( - !i.isNaN, - prop(SafeNumbers.float(i.toString) == FloatSome(i.toFloat)) - ) - } - - val invalidFloatText = property { s: String => prop(SafeNumbers.float(s) == FloatNone) }( - Gen.alphaLowerString, - Shrink.empty - ) - - val validInt = property { i: Int => prop(SafeNumbers.int(i.toString) == IntSome(i)) } - - val invalidInt = property { i: Long => - implies( - i < Int.MinValue || Int.MaxValue < i, - prop(SafeNumbers.int(i.toString) == IntNone) - ) - } - - val invalidIntText = property { s: String => prop(SafeNumbers.int(s) == IntNone) }(Gen.alphaLowerString, Shrink.empty) - - val validLongEdgeCases = - List("00", "01", "0000001", "-9223372036854775807", "9223372036854775806") - .map(s => prop(SafeNumbers.long(s) == LongSome(s.toLong))) - .reduce(_ and _) - - val invalidLongEdgeCases = - prop( - List( - "0foo", - "01foo", - "0.1", - "", - "1 ", - "-9223372036854775809", - "9223372036854775808" - ).map(SafeNumbers.long) - .forall(_.isEmpty) - ) - - val validLong = property { i: Long => prop(SafeNumbers.long(i.toString) == LongSome(i)) } - - val invalidLong = property { bi: BigInt => - val i = bi.underlying - implies(i.bitLength > 63, prop(SafeNumbers.long(i.toString) == LongNone)) - }(Gen.genLargeBigInt, Shrink.empty) - - val invalidLongText = property { s: String => prop(SafeNumbers.long(s) == LongNone) }( - Gen.alphaLowerString, - Shrink.empty - ) - - val validShort = property { i: Short => prop(SafeNumbers.short(i.toString) == ShortSome(i)) } - - val invalidShort = property { i: Long => - implies( - i < Short.MinValue || Short.MaxValue < i, - prop(SafeNumbers.short(i.toString) == ShortNone) - ) - } - - val invalidShortText = property { s: String => prop(SafeNumbers.short(s) == ShortNone) }( - Gen.alphaLowerString, - Shrink.empty - ) - -} diff --git a/src/test/scala/zio/json/internal/StringMatrixSpec.scala b/src/test/scala/zio/json/internal/StringMatrixSpec.scala new file mode 100644 index 000000000..7fe358976 --- /dev/null +++ b/src/test/scala/zio/json/internal/StringMatrixSpec.scala @@ -0,0 +1,72 @@ +package testzio.json.internal + +import zio.json.internal._ +import zio.test.Assertion._ +import zio.test.{ DefaultRunnableSpec, _ } + +object StringMatrixSpec extends DefaultRunnableSpec { + def spec = suite("StringMatrix")( + testM("positive succeeds") { + // Watch out: TestStrings were passed + check(genTestStrings) { xs => + val asserts = xs.map(s => matcher(xs, s).contains(s)) + + assert(asserts)(forall(isTrue)) + } + }, + testM("negative fails") { + check(genTestStrings.filterNot(_.startsWith("wibble"))) { xs => + val asserts = xs.map(s => matcher(xs, "wibble")) + + assert(asserts)(forall(isEmpty)) + } + }, + testM("substring fails") { + check(genTestStrings.filter(_.length > 1)) { xs => + val asserts = xs.map(s => matcher(xs, xs.mkString)) + + assert(asserts)(forall(isEmpty)) + } + }, + testM("trivial") { + check(genNonEmptyString)(s => assert(matcher(List(s), s))(equalTo(List(s)))) + }, + test("exact match is a substring") { + assert( + matcher( + List("retweeted_status", "retweeted"), + "retweeted" + ) + )(equalTo(List("retweeted"))) + } + ) + + val genNonEmptyString = + Gen.alphaNumericString.filter(_.nonEmpty) + + val genTestStrings = + for { + n <- Gen.int(1, 63) + xs <- Gen.listOfN(n)(genNonEmptyString) + } yield xs + + private def matcher(xs: List[String], test: String): List[String] = { + val m = new StringMatrix(xs.toArray) + var bs = test.zipWithIndex.foldLeft(m.initial) { + case (bs, (c, i)) => m.update(bs, i, c) + } + bs = m.exact(bs, test.length) + matches(xs, bs) + } + + private def matches(xs: List[String], bitset: Long): List[String] = { + var hits: List[String] = Nil + var i = 0 + while (i < xs.length) { + if (((bitset >>> i) & 1L) == 1L) + hits = xs(i) :: hits + i += 1 + } + hits + } +} diff --git a/src/test/scala/zio/json/internal/StringMatrixTest.scala b/src/test/scala/zio/json/internal/StringMatrixTest.scala deleted file mode 100644 index 0dcecdb54..000000000 --- a/src/test/scala/zio/json/internal/StringMatrixTest.scala +++ /dev/null @@ -1,66 +0,0 @@ -package testzio.json.internal - -import scalaprops._ -import Property.{ implies, prop, property } -import utest._ - -import zio.json.internal._ - -// testOnly *StringMatrix* -object StringMatrixProps extends Scalaprops { - val nonEmptyString: Gen[String] = Gen.nonEmptyString(Gen.alphaNumChar) - val testStrings: Gen[List[String]] = - Gen.choose(1, 63).flatMap(n => Gen.sequenceNList(n, nonEmptyString)) - - def matcher(xs: List[String], test: String): List[String] = { - val m = new StringMatrix(xs.toArray) - var bs = test.zipWithIndex.foldLeft(m.initial) { - case (bs, (c, i)) => m.update(bs, i, c) - } - bs = m.exact(bs, test.length) - matches(xs, bs) - } - - def matches(xs: List[String], bitset: Long): List[String] = { - var hits: List[String] = Nil - var i = 0 - while (i < xs.length) { - if (((bitset >>> i) & 1L) == 1L) - hits = xs(i) :: hits - i += 1 - } - hits - } - - val positiveSucceeds = property { xs: List[String] => xs.map(s => prop(matcher(xs, s).contains(s))).reduce(_ and _) }( - testStrings, - Shrink.empty - ) - - val negativeFails = property { xs: List[String] => - implies( - !xs.exists(_.startsWith("wibble")), - prop(matcher(xs, "wibble") == Nil) - ) - }(testStrings, Shrink.empty) - - val substringFails = property { xs: List[String] => implies(xs.length > 1, prop(matcher(xs, xs.mkString) == Nil)) }( - testStrings, - Shrink.empty - ) - - val trivial = property { s: String => prop(matcher(List(s), s) == List(s)) }(nonEmptyString, Shrink.empty) -} - -object StringMatrixTest extends TestSuite { - - val tests = Tests { - test("exact match is a substring") { - StringMatrixProps.matcher( - List("retweeted_status", "retweeted"), - "retweeted" - ) ==> List("retweeted") - } - } - -}