diff --git a/project/WeePicklePlugin.scala b/project/WeePicklePlugin.scala index 866b2bb0..dbd34c92 100644 --- a/project/WeePicklePlugin.scala +++ b/project/WeePicklePlugin.scala @@ -14,8 +14,8 @@ object WeePicklePlugin extends AutoPlugin { val scala211 = "2.11.12" val scala212 = "2.12.12" - val scala213 = "2.13.5" - val scala3 = "3.0.1" + val scala213 = "2.13.7" + val scala3 = "3.1.0" val supportedScala2Versions = Seq(scala211, scala212, scala213) val supportedScalaVersions = supportedScala2Versions :+ scala3 @@ -119,6 +119,14 @@ object WeePicklePlugin extends AutoPlugin { builder.result() }, + Test / scalacOptions ++= { + scalaBinaryVersion.value match { + case "3" => Seq( + "-Wconf:cat=deprecation:info" // scalacheck uses Stream + ) + case _ => Nil + } + }, testFrameworks += new TestFramework("utest.runner.Framework"), ) } diff --git a/weejson-jackson/test/resources/empty.json b/weejson-jackson/test/resources/empty.json deleted file mode 100644 index e69de29b..00000000 diff --git a/weejson-jackson/test/scala/com/rallyhealth/weejson/v1/jackson/GenValue.scala b/weejson-jackson/test/scala/com/rallyhealth/weejson/v1/jackson/GenValue.scala deleted file mode 100644 index 7c1d511c..00000000 --- a/weejson-jackson/test/scala/com/rallyhealth/weejson/v1/jackson/GenValue.scala +++ /dev/null @@ -1,53 +0,0 @@ -package com.rallyhealth.weejson.v1.jackson - -import com.rallyhealth.weejson.v1._ -import org.scalacheck.{Arbitrary, Gen} - -import scala.collection.mutable.ArrayBuffer - -trait GenValue { - - def genArray(depth: Int): Gen[Arr] = { - for { - n <- Gen.chooseNum(0, 10) - arr <- Gen.containerOfN[ArrayBuffer, Value](n, genValue(depth)).map(Arr(_)) - } yield arr - } - - def genObject(depth: Int): Gen[Obj] = - for { - n <- Gen.chooseNum(0, 10) - genKV = for { - k <- Gen.alphaNumStr - v <- genValue(depth) - } yield k -> v - obj <- Gen.buildableOfN[Map[String, Value], (String, Value)](n, genKV).map(Obj.from) - } yield obj - - def genValue(depth: Int): Gen[Value] = { - val nonRecursive = List( - Gen.alphaNumStr.map(Str), - Arbitrary.arbitrary[Double].map(Num(_)), - Arbitrary.arbitrary[Boolean].map(Bool(_)), - Gen.const(Null) - ) - - val maybeRecursive: List[Gen[Value]] = depth match { - case 0 => Nil - case _ => - List( - genArray(depth - 1), - genObject(depth - 1) - ) - } - - val generators: List[Gen[Value]] = nonRecursive ++ maybeRecursive - Gen.oneOf(generators(0), generators(1), generators.drop(2): _*) // scalacheck API :\ - } - - implicit val arbJsValue: Arbitrary[Value] = Arbitrary { - Gen - .chooseNum(0, 5) - .flatMap(depth => genValue(depth)) - } -} diff --git a/weejson-jackson/test/scala/com/rallyhealth/weejson/v1/jackson/JsonGeneratorOutputStreamSpec.scala b/weejson-jackson/test/scala/com/rallyhealth/weejson/v1/jackson/JsonGeneratorOutputStreamSpec.scala deleted file mode 100644 index d9a14947..00000000 --- a/weejson-jackson/test/scala/com/rallyhealth/weejson/v1/jackson/JsonGeneratorOutputStreamSpec.scala +++ /dev/null @@ -1,42 +0,0 @@ -package com.rallyhealth.weejson.v1.jackson - -import java.io.StringWriter -import java.nio.charset.StandardCharsets.UTF_8 - -import com.rallyhealth.weejson.v1.jackson.DefaultJsonFactory.Instance -import com.rallyhealth.weejson.v1.{Value, WeeJson} -import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.{FreeSpec, LoneElement, Matchers} -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -class JsonGeneratorOutputStreamSpec - extends FreeSpec - with Matchers - with ScalaCheckDrivenPropertyChecks - with GenValue - with LoneElement - with TypeCheckedTripleEquals { - - override implicit val generatorDrivenConfig: PropertyCheckConfiguration = PropertyCheckConfiguration( - minSuccessful = 500 - ) - - "like WeeJson" in { - forAll { input: Value => - val result: Value = { - var output = Seq.newBuilder[Value] - val writer = new StringWriter - val generator = Instance.createGenerator(writer) - val out = new JsonGeneratorOutputStream(generator) - - val strInput = WeeJson.write(input) - out.write(strInput.getBytes(UTF_8)) - out.close() - - WeeJson.read(writer.toString) - } - - result shouldBe input - } - } -} diff --git a/weejson-jackson/test/scala/com/rallyhealth/weejson/v1/jackson/WeeJacksonSpec.scala b/weejson-jackson/test/scala/com/rallyhealth/weejson/v1/jackson/WeeJacksonSpec.scala deleted file mode 100644 index 239de11d..00000000 --- a/weejson-jackson/test/scala/com/rallyhealth/weejson/v1/jackson/WeeJacksonSpec.scala +++ /dev/null @@ -1,47 +0,0 @@ -package com.rallyhealth.weejson.v1.jackson - -import java.io.{File, StringWriter} - -import com.fasterxml.jackson.core.io.JsonEOFException -import com.rallyhealth.weejson.v1._ -import com.rallyhealth.weejson.v1.jackson.DefaultJsonFactory.Instance -import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.{FreeSpec, Matchers} -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -class WeeJacksonSpec - extends FreeSpec - with Matchers - with ScalaCheckDrivenPropertyChecks - with GenValue - with TypeCheckedTripleEquals { - - override implicit val generatorDrivenConfig: PropertyCheckConfiguration = PropertyCheckConfiguration( - minSuccessful = 500 - ) - "parse" - { - "like WeeJson" in forAll { value: Value => - val str = WeeJson.write(value) - FromJson(str).transform(Value) should ===(value) - } - } - - "visitor" - { - "like WeeJson.write" in { - forAll { value: Value => - val writer = new StringWriter() - value.transform(JsonRenderer(Instance.createGenerator(writer))) - writer.toString should ===(value.transform(StringRenderer()).toString) - } - } - } - - "empty file input" - { - "to required" in { - val file = new File(getClass.getResource("/empty.json").getPath) - intercept[JsonEOFException] { - FromJson(file).transform(Value) - } - } - } -} diff --git a/weejson/src/test/scala/com/rallyhealth/weejson/v1/GenBufferedValue.scala b/weejson/src/test/scala/com/rallyhealth/weejson/v1/GenBufferedValue.scala index 1ae863dc..e04ad7a7 100644 --- a/weejson/src/test/scala/com/rallyhealth/weejson/v1/GenBufferedValue.scala +++ b/weejson/src/test/scala/com/rallyhealth/weejson/v1/GenBufferedValue.scala @@ -1,11 +1,11 @@ package com.rallyhealth.weejson.v1 -import org.scalacheck.{Arbitrary, Gen} +import org.scalacheck.{Arbitrary, Gen, Shrink} import scala.collection.mutable.ArrayBuffer trait GenBufferedValue { - import BufferedValue.{Arr, Obj, Str, Bool, Null, AnyNum} + import com.rallyhealth.weejson.v1.BufferedValue._ def genArray(depth: Int): Gen[Arr] = { for { @@ -52,6 +52,17 @@ trait GenBufferedValue { } implicit val arbNum: Arbitrary[AnyNum] = Arbitrary { - Arbitrary.arbitrary[Double].map(BigDecimal(_)).map(AnyNum(_)) + Arbitrary.arbitrary[Double].map(BigDecimal(_)).map(AnyNum(_)) // TODO open to all BigDecimals after #102 + } + + implicit val shrinkValue: Shrink[BufferedValue] = Shrink[BufferedValue] { + case Obj(map) => Shrink.shrink(map).map(Obj(_)) + case Arr(buf) => Shrink.shrink(buf).map(Arr(_)) + case NumDouble(d) => NumLong(d.longValue) +: Shrink.shrink(d).map(NumDouble(_)) + case NumLong(long) => Shrink.shrink(long).map(NumLong(_)) + case Str(s) => Shrink.shrink(s).map(Str(_)) + case Timestamp(s) => Shrink.shrink(s).map(Timestamp(_)) + case Ext(tag, bytes) => Shrink.shrink(bytes).map(Ext(tag, _)) + case _ => Stream.empty } } diff --git a/weejson/src/test/scala/com/rallyhealth/weejson/v1/GenValue.scala b/weejson/src/test/scala/com/rallyhealth/weejson/v1/GenValue.scala index daf49e07..f5ed438e 100644 --- a/weejson/src/test/scala/com/rallyhealth/weejson/v1/GenValue.scala +++ b/weejson/src/test/scala/com/rallyhealth/weejson/v1/GenValue.scala @@ -1,7 +1,7 @@ package com.rallyhealth.weejson.v1 import com.rallyhealth.weejson.v1._ -import org.scalacheck.{Arbitrary, Gen} +import org.scalacheck.{Arbitrary, Gen, Shrink} import scala.collection.mutable.ArrayBuffer @@ -54,4 +54,12 @@ trait GenValue { implicit val arbNum: Arbitrary[Num] = Arbitrary { Arbitrary.arbitrary[Double].map(Num(_)) } + + implicit val shrinkValue: Shrink[Value] = Shrink[Value] { + case Obj(map) => Shrink.shrink(map).map(Obj(_)) + case Arr(buf) => Shrink.shrink(buf).map(Arr(_)) + case Num(bd) => Shrink.shrink(bd).map(Num(_)) + case Str(s) => Shrink.shrink(s).map(Str(_)) + case _ => Stream.empty + } } diff --git a/weepickle-tests/src/test/scala/com/rallyhealth/weepickle/v1/MaxDepthVisitor.scala b/weepickle-tests/src/test/scala/com/rallyhealth/weepickle/v1/MaxDepthVisitor.scala new file mode 100644 index 00000000..65c44afc --- /dev/null +++ b/weepickle-tests/src/test/scala/com/rallyhealth/weepickle/v1/MaxDepthVisitor.scala @@ -0,0 +1,46 @@ +package com.rallyhealth.weepickle.v1 + +import com.rallyhealth.weepickle.v1.core.{ArrVisitor, NoOpVisitor, ObjArrVisitor, ObjVisitor, Visitor} + +/** + * Returns the max depth encountered. + * Useful for creating valid test inputs that can be sent to a parser that throws at high depth. + */ +class MaxDepthVisitor extends NoOpVisitor[Int](0) { + + private[this] var maxDepth = 0 + private[this] var currentDepth = 0 + + private def inc() = { + currentDepth += 1 + maxDepth = math.max(maxDepth, currentDepth) + } + + private def dec() = { + currentDepth -= 1 + } + + private trait ObjArrBase { + self: ObjArrVisitor[Any, Int] => + + override def subVisitor: Visitor[_, _] = { + inc() + MaxDepthVisitor.this.map(_ => maxDepth) + } + + override def visitValue(v: Any): Unit = dec() + + override def visitEnd(): Int = maxDepth + } + + override def visitObject(length: Int): ObjVisitor[Any, Int] = new ObjVisitor[Any, Int] with ObjArrBase { + override def visitKey(): Visitor[_, _] = { + inc() + NoOpVisitor + } + + override def visitKeyValue(v: Any): Unit = dec() + } + + override def visitArray(length: Int): ArrVisitor[Any, Int] = new ArrVisitor[Any, Int] with ObjArrBase +} diff --git a/weepickle-tests/src/test/scala/com/rallyhealth/weepickle/v1/ParserSpec.scala b/weepickle-tests/src/test/scala/com/rallyhealth/weepickle/v1/ParserSpec.scala new file mode 100644 index 00000000..24d8b55f --- /dev/null +++ b/weepickle-tests/src/test/scala/com/rallyhealth/weepickle/v1/ParserSpec.scala @@ -0,0 +1,68 @@ +package com.rallyhealth.weepickle.v1 + +import com.rallyhealth.weejson.v1.jackson.{FromJson, ToJson, ToPrettyJson} +import com.rallyhealth.weejson.v1.{BufferedValue, GenBufferedValue} +import com.rallyhealth.weepickle.v1.core.FromInput +import org.scalactic.TypeCheckedTripleEquals +import org.scalatest.freespec.AnyFreeSpec +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks + +import java.io.{ByteArrayInputStream, StringReader} +import java.nio.file.Files +import scala.language.{existentials, implicitConversions} + +abstract class ParserSpec(parse: Array[Byte] => FromInput, depthLimit: Int = 100) + extends AnyFreeSpec + with ScalaCheckPropertyChecks + with GenBufferedValue + with TypeCheckedTripleEquals { + + import com.rallyhealth.weejson.v1.BufferedValue._ + import com.rallyhealth.weejson.v1.BufferedValueOps._ + + private implicit def toBytes(s: String): Array[Byte] = s.getBytes + + "roundtrip" in testJson() + "deep arr" in testDepth(Arr(_)) + "deep obj" in testDepth(b => Obj("k" -> b)) + + private def testJson(tweak: BufferedValue => BufferedValue = identity) = { + forAll { (b: BufferedValue) => + val value = tweak(b) + val expected = value.transform(ToJson.string) + + def testInput(s: Array[Byte]) = assert(parse(s).transform(ToJson.string) === expected) + + testInput(expected) + testInput(s"\n $expected \n ") + testInput(value.transform(ToPrettyJson.bytes)) + + assert(parse(expected.getBytes()).transform(BufferedValue.Builder) === value) + } + } + + private def testDepth(f: BufferedValue => BufferedValue) = { + testJson { in => + val depth = in.transform(new MaxDepthVisitor) + val numPads = math.max(depthLimit - depth, 0) + (0 to numPads).foldLeft(in)((p, _) => f(p)) + } + } +} + +class FromJsonBytesSpec extends ParserSpec(FromJson(_)) + +class FromJsonStringSpec extends ParserSpec(b => FromJson(new String(b))) + +class FromJsonInputStreamSpec extends ParserSpec(b => FromJson(new ByteArrayInputStream(b))) + +class FromJsonReaderSpec extends ParserSpec(b => FromJson(new StringReader(new String(b)))) + +class FromJsonPathSpec extends ParserSpec( + b => FromJson(Files.write(Files.createTempFile("FromJsonPathSpec", ".json"), b)) +) + +class FromJsonFileSpec extends ParserSpec( + b => FromJson(Files.write(Files.createTempFile("FromJsonFileSpec", ".json"), b).toFile) +) +