From 63088d49fdbe12295669ef763497d9f9762d7827 Mon Sep 17 00:00:00 2001 From: Jeremy Smith Date: Mon, 27 Jun 2016 13:36:57 -0700 Subject: [PATCH] Fix binary decoders I did this because many of the binary decoders were incorrect. - Remove existing binary decoder tests and replace with integration tests against postgres (To verify that they do actually decode the actual PG binary representation) - Update existing binary decoders for date/timestamp/time, string, and json - Add hstore decoder - Add numeric decoder --- .gitignore | 3 + build.sbt | 2 + .../BinaryDecodersIntegrationSpec.scala | 104 +++++++++++ .../types/integrations/JsonGenerators.scala | 39 +++++ types/src/main/scala/roc/types/decoders.scala | 164 ++++++++++++++++-- types/src/main/scala/roc/types/package.scala | 1 + .../types/decoders/DateTimeDecodersSpec.scala | 6 - .../roc/types/decoders/DecodersSpec.scala | 7 - .../types/decoders/NumericDecodersSpec.scala | 10 -- 9 files changed, 302 insertions(+), 34 deletions(-) create mode 100644 types/src/it/scala/roc/types/integrations/BinaryDecodersIntegrationSpec.scala create mode 100644 types/src/it/scala/roc/types/integrations/JsonGenerators.scala diff --git a/.gitignore b/.gitignore index fc30c64..abe0a83 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ project/plugins/project/ # Scala-IDE specific .scala_dependencies .worksheet + +# IntelliJ +.idea diff --git a/build.sbt b/build.sbt index cac0022..d099d46 100644 --- a/build.sbt +++ b/build.sbt @@ -107,12 +107,14 @@ lazy val types = project ) ) .dependsOn(core) + .dependsOn(core % "it->it") lazy val benchmark = project .settings( description := "roc-benchmark", moduleName := "roc-benchmark" ) + .configs( IntegrationTest ) .settings(allSettings:_*) .settings(noPublishSettings) .enablePlugins(JmhPlugin) diff --git a/types/src/it/scala/roc/types/integrations/BinaryDecodersIntegrationSpec.scala b/types/src/it/scala/roc/types/integrations/BinaryDecodersIntegrationSpec.scala new file mode 100644 index 0000000..bb288a2 --- /dev/null +++ b/types/src/it/scala/roc/types/integrations/BinaryDecodersIntegrationSpec.scala @@ -0,0 +1,104 @@ +package roc.types.integrations + +import java.time._ +import java.time.temporal.{ChronoField, JulianFields} + +import org.scalacheck._ +import org.scalacheck.Prop.forAll +import org.scalacheck.Arbitrary.arbitrary +import org.specs2._ +import roc.integrations.Client +import roc.postgresql.{ElementDecoder, Request, Text} +import com.twitter.util.Await +import roc.types._ +import roc.types.decoders._ +import jawn.ast.{JNull, JParser, JValue} + +class BinaryDecodersIntegrationSpec extends Specification with Client with ScalaCheck { + + //need a more sensible BigDecimal generator, because ScalaCheck goes crazy with it and we can't even stringify them + //this will be sufficient to test the decoder + implicit val arbBD: Arbitrary[BigDecimal] = Arbitrary(for { + precision <- Gen.choose(1, 32) + scale <- Gen.choose(-32, 32) + digits <- Gen.listOfN[Char](precision, Gen.numChar) + } yield BigDecimal(BigDecimal(digits.mkString).bigDecimal.movePointLeft(scale))) + + implicit val arbCirce: Arbitrary[_root_.io.circe.Json] = JsonGenerators.arbJson + //arbitrary Json for Jawn + implicit val arbJson = Arbitrary[Json](Gen.resize(4, for { + circe <- arbitrary[_root_.io.circe.Json] + } yield JParser.parseFromString(circe.noSpaces).toOption.getOrElse(JNull))) + + implicit val arbDate = Arbitrary[Date](for { + julian <- Gen.choose(1721060, 5373484) //Postgres date parser doesn't like dates outside year range 0000-9999 + } yield LocalDate.now().`with`(JulianFields.JULIAN_DAY, julian)) + + implicit val arbTime = Arbitrary[Time](for { + usec <- Gen.choose(0L, 24L * 60 * 60 * 1000000 - 1) + } yield LocalTime.ofNanoOfDay(usec * 1000)) + + implicit val arbTimestampTz = Arbitrary[TimestampWithTZ](for { + milli <- Gen.posNum[Long] + } yield ZonedDateTime.ofInstant(Instant.ofEpochMilli(milli), ZoneId.systemDefault())) + + def is = sequential ^ s2""" + Decoders + must decode string from binary $testString + must decode short from binary $testShort + must decode int from binary $testInt + must decode long from binary $testLong + must decode float from binary $testFloat + must decode double from binary $testDouble + must decode numeric from binary $testNumeric + must decode boolean from binary $testBoolean + must decode json from binary $testJson + must decode jsonb from binary $testJsonb + must decode date from binary $testDate + must decode time from binary $testTime + must decode timestamptz from binary $testTimestampTz + """ + + def test[T : Arbitrary : ElementDecoder]( + send: String, + typ: String, + toStr: T => String = (t: T) => t.toString, + tester: (T, T) => Boolean = (a: T, b: T) => a == b) = forAll { + (t: T) => + //TODO: change this once prepared statements are available + val escaped = toStr(t).replaceAllLiterally("'", "\\'") + val query = s"SELECT $send('$escaped'::$typ) AS out" + val result = Await.result(Postgres.query(Request(query))) + val Text(name, oid, string) = result.head.get('out) + val bytes = string.stripPrefix("\\x").sliding(2,2).toArray.map(Integer.parseInt(_, 16)).map(_.toByte) + val out = implicitly[ElementDecoder[T]].binaryDecoder(bytes) + tester(t, out) + } + + def testString = test[String]("textsend", "text") + def testShort = test[Short]("int2send", "int2") + def testInt = test[Int]("int4send", "int4") + def testLong = test[Long]("int8send", "int8") + def testFloat = test[Float]("float4send", "float4") + def testDouble = test[Double]("float8send", "float8") + def testNumeric = test[BigDecimal]("numeric_send", "numeric", _.bigDecimal.toPlainString) + def testBoolean = test[Boolean]("boolsend", "boolean") + def testJson = test[Json]("json_send", "json") + def testJsonb = test[Json]("jsonb_send", "jsonb") + def testDate = test[Date]("date_send", "date") + def testTime = test[Time]("time_send", "time") + def testTimestampTz = test[TimestampWithTZ]( + "timestamptz_send", + "timestamptz", + ts => java.sql.Timestamp.from(ts.toInstant).toString, + (a, b) => a.getLong(ChronoField.MICRO_OF_DAY) == b.getLong(ChronoField.MICRO_OF_DAY) //postgres only keeps microsecond precision + ) + + //disabled - test server might not have hstore extension + def testHstore = test[HStore]("hstore_send", "hstore", { strMap => + strMap.map { + case (k, v) => + s""""${k.replaceAllLiterally("\"", "\\\"")}"=>"${v.getOrElse("NULL").replaceAllLiterally("\"", "\\\"")}"""" + }.mkString(",") + }) +} diff --git a/types/src/it/scala/roc/types/integrations/JsonGenerators.scala b/types/src/it/scala/roc/types/integrations/JsonGenerators.scala new file mode 100644 index 0000000..5e20bf5 --- /dev/null +++ b/types/src/it/scala/roc/types/integrations/JsonGenerators.scala @@ -0,0 +1,39 @@ +package roc.types.integrations + +import io.circe.{Json, JsonObject} +import io.circe.syntax._ +import org.scalacheck.Arbitrary._ +import org.scalacheck.{Arbitrary, Gen} + +object JsonGenerators { + + val genJsonScalarValue : Gen[Json] = Gen.oneOf( + arbitrary[Long] map (_.asJson), + Gen.choose(Int.MinValue.toDouble, Int.MaxValue.toDouble) map (_.asJson), //avoid parsing craziness + Gen.alphaStr map Json.fromString, + arbitrary[Boolean] map Json.fromBoolean + ) + + def genJsonArray(genValue: Gen[Json]) = Gen.listOf(genValue).map(Json.arr(_:_*)) + + def genJsonObject(genValue: Gen[Json]) = Gen.nonEmptyMap(for { + keySize <- Gen.choose(10, 20) + key <- Gen.resize(keySize, Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.mkString)) + value <- genValue + } yield (key, value)) map JsonObject.fromMap map Json.fromJsonObject + + def genJson(level: Int) : Gen[Json] = if(level == 0) + genJsonScalarValue + else Gen.oneOf( + genJsonScalarValue, + genJsonArray(genJson(level - 1)), + genJsonObject(genJson(level - 1))) + + implicit val arbJson : Arbitrary[Json] = Arbitrary(for { + size <- Gen.size + level <- Gen.choose(0, size) + json <- genJson(level) + } yield json) + + +} diff --git a/types/src/main/scala/roc/types/decoders.scala b/types/src/main/scala/roc/types/decoders.scala index d23c8dd..e211d28 100644 --- a/types/src/main/scala/roc/types/decoders.scala +++ b/types/src/main/scala/roc/types/decoders.scala @@ -3,15 +3,18 @@ package types import cats.data.{Validated, Xor} import io.netty.buffer.Unpooled -import java.nio.ByteBuffer +import java.nio.{BufferUnderflowException, ByteBuffer} import java.nio.charset.StandardCharsets import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder} -import java.time.temporal.ChronoField -import java.time.{LocalDate, LocalTime, ZonedDateTime} +import java.time.temporal.{ChronoField, JulianFields} +import java.time._ + import jawn.ast.JParser import roc.postgresql.ElementDecoder import roc.types.failures._ +import scala.util.Failure + object decoders { implicit def optionElementDecoder[A](implicit f: ElementDecoder[A]) = @@ -23,7 +26,7 @@ object decoders { implicit val stringElementDecoder: ElementDecoder[String] = new ElementDecoder[String] { def textDecoder(text: String): String = text - def binaryDecoder(bytes: Array[Byte]): String = bytes.map(_.toChar).mkString + def binaryDecoder(bytes: Array[Byte]): String = new String(bytes, StandardCharsets.UTF_8) def nullDecoder(): String = throw new NullDecodedFailure("STRING") } @@ -117,6 +120,78 @@ object decoders { def nullDecoder: Double = throw new NullDecodedFailure("DOUBLE") } + implicit val bigDecimalDecoder: ElementDecoder[BigDecimal] = new ElementDecoder[BigDecimal] { + + def textDecoder(text: String): BigDecimal = Xor.catchNonFatal(BigDecimal(text)).fold( + l => throw new ElementDecodingFailure("NUMERIC", l), + identity + ) + + private val NUMERIC_POS = 0x0000 + private val NUMERIC_NEG = 0x4000 + private val NUMERIC_NAN = 0xC000 + private val NUMERIC_NULL = 0xF000 + private val NumericHeaderSize = 16 + private val NumericDigitBaseExponent = 4 + private val NumericDigitBase = Math.pow(10, NumericDigitBaseExponent) // 10,000 + + /** + * Read only 16 bits, but returned the unsigned number as a 32-bit Integer + * + * @param buf The Byte Buffer + */ + private def getUnsignedShort(buf: ByteBuffer) = { + val high = buf.get().toInt + val low = buf.get() + (high << 8) | low + } + + def binaryDecoder(bytes: Array[Byte]): BigDecimal = Xor.catchNonFatal { + val buf = ByteBuffer.wrap(bytes) + val len = getUnsignedShort(buf) + val weight = buf.getShort() + val sign = getUnsignedShort(buf) + val displayScale = getUnsignedShort(buf) + + //digits are actually unsigned base-10000 numbers (not straight up bytes) + val digits = new Array[Short](len) + buf.asShortBuffer().get(digits) + val bdDigits = digits.map(BigDecimal(_)) + + if(bdDigits.length > 0) { + val unscaled = bdDigits.tail.foldLeft(bdDigits.head) { + case (accum, digit) => BigDecimal(accum.bigDecimal.scaleByPowerOfTen(NumericDigitBaseExponent)) + digit + } + + val firstDigitSize = + if (digits.head < 10) 1 + else if (digits.head < 100) 2 + else if (digits.head < 1000) 3 + else 4 + + val scaleFactor = if (weight >= 0) + weight * NumericDigitBaseExponent + firstDigitSize + else + weight * NumericDigitBaseExponent + firstDigitSize + val unsigned = unscaled.bigDecimal.movePointLeft(unscaled.precision).movePointRight(scaleFactor).setScale(displayScale) + + sign match { + case NUMERIC_POS => Xor.right(BigDecimal(unsigned)) + case NUMERIC_NEG => Xor.right(BigDecimal(unsigned.negate())) + case NUMERIC_NAN => Xor.left(new NumberFormatException("Value is NaN; cannot be represented as BigDecimal")) + case NUMERIC_NULL => Xor.left(new NumberFormatException("Value is NULL within NUMERIC")) + } + } else { + Xor.right(BigDecimal(0)) + } + }.flatMap(identity).fold( + l => throw new ElementDecodingFailure("NUMERIC", l), + identity + ) + + def nullDecoder: BigDecimal = throw new NullDecodedFailure("NUMERIC") + } + implicit val booleanElementDecoder: ElementDecoder[Boolean] = new ElementDecoder[Boolean] { def textDecoder(text: String): Boolean = Xor.catchNonFatal(text.head match { case 't' => true @@ -155,8 +230,18 @@ object decoders { {r => r } ) def binaryDecoder(bytes: Array[Byte]): Json = Validated.fromTry({ + //this could be either json or jsonb - if it's jsonb, the first byte is a number representing the jsonb version. + //currently the version is 1, but we can say that if the first byte is unprintable, it must be a jsonb version. + //otherwise we have gone through 31 versions of jsonb, which hopefully won't happen before this is updated! val buffer = ByteBuffer.wrap(bytes) - JParser.parseFromByteBuffer(buffer) + if(buffer.remaining() <= 0) + Failure(new BufferUnderflowException()) + else if(buffer.get(0) < 32) { + buffer.get() //advance one byte + JParser.parseFromByteBuffer(buffer) + } + else + JParser.parseFromByteBuffer(buffer) }).fold( {l => throw new ElementDecodingFailure("JSON", l)}, {r => r} @@ -170,8 +255,10 @@ object decoders { {r => r} ) def binaryDecoder(bytes: Array[Byte]): Date = Xor.catchNonFatal({ - val text = new String(bytes, StandardCharsets.UTF_8) - LocalDate.parse(text) + val buf = ByteBuffer.wrap(bytes) + // Postgres represents this as Julian Day since Postgres Epoch (2000-01-01) + val julianDay = buf.getInt() + LocalDate.now().`with`(JulianFields.JULIAN_DAY, julianDay + 2451545) }).fold( {l => throw new ElementDecodingFailure("DATE", l)}, {r => r} @@ -185,8 +272,14 @@ object decoders { {r => r} ) def binaryDecoder(bytes: Array[Byte]): Time = Xor.catchNonFatal({ - val text = new String(bytes, StandardCharsets.UTF_8) - LocalTime.parse(text) + val buf = ByteBuffer.wrap(bytes) + // Postgres uses either an 8-byte float representing seconds since midnight, or an 8-byte long representing + // microseconds since midnight. + // We don't know which one to use inside this decoder, so we'll just assume HAVE_INT64_TIMESTAMP + // Some design changes would be required to make the decision appropriately. + + val microSecs = buf.getLong() + LocalTime.ofNanoOfDay(microSecs * 1000) }).fold( {l => throw new ElementDecodingFailure("TIME", l)}, {r => r} @@ -196,24 +289,73 @@ object decoders { implicit val zonedDateTimeElementDecoders: ElementDecoder[TimestampWithTZ] = new ElementDecoder[TimestampWithTZ] { + + private val POSTGRES_TIMESTAMP_EPOCH = + OffsetDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant.toEpochMilli + + //Postgres uses milliseconds since its own custom epoch (see above) + private def timestampToInstant(microseconds: Long) = + Instant.ofEpochMilli(microseconds / 1000L).plusMillis(POSTGRES_TIMESTAMP_EPOCH) + private val zonedDateTimeFmt = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM-dd HH:mm:ss") .appendFraction(ChronoField.MICRO_OF_SECOND, 0, 6, true) .appendOptional(DateTimeFormatter.ofPattern("X")) .toFormatter() + def textDecoder(text: String): TimestampWithTZ = Xor.catchNonFatal({ ZonedDateTime.parse(text, zonedDateTimeFmt) }).fold( {l => throw new ElementDecodingFailure("TIMESTAMP WITH TIME ZONE", l)}, {r => r} ) + def binaryDecoder(bytes: Array[Byte]): TimestampWithTZ = Xor.catchNonFatal({ - val text = new String(bytes, StandardCharsets.UTF_8) - ZonedDateTime.parse(text, zonedDateTimeFmt) + val buf = ByteBuffer.wrap(bytes) + timestampToInstant(buf.getLong).atZone(ZoneId.systemDefault()) }).fold( {l => throw new ElementDecodingFailure("TIMESTAMP WITH TIME ZONE", l)}, {r => r} ) def nullDecoder: TimestampWithTZ = throw new NullDecodedFailure("TIMSTAMP WITH TIME ZONE") } + + implicit val hstoreDecoders: ElementDecoder[HStore] = new ElementDecoder[HStore] { + val hstoreStringRegex = """"([^"]*)"=>(?:NULL|"([^"]*))"""".r + def textDecoder(text: String): HStore = Xor.catchNonFatal { + hstoreStringRegex.findAllMatchIn(text).map { + m => m.group(1) -> Option(m.group(2)) + }.toMap + }.fold( + l => throw new ElementDecodingFailure("HSTORE", l), + identity + ) + + def nullDecoder(): HStore = throw new NullDecodedFailure("HSTORE") + + def binaryDecoder(bytes: Array[Byte]): HStore = { + val buf = ByteBuffer.wrap(bytes) + Xor.catchNonFatal { + val count = buf.getInt() + val charset = StandardCharsets.UTF_8 //TODO: are we going to support other charsets? + Array.fill(count) { + val keyLength = buf.getInt() + val key = new Array[Byte](keyLength) + buf.get(key) + val valueLength = buf.getInt() + val value = valueLength match { + case -1 => None + case l => + val valueBytes = new Array[Byte](l) + buf.get(valueBytes) + Some(valueBytes) + } + new String(key, charset) -> value.map(new String(_, charset)) + }.toMap + }.fold ( + l => throw new ElementDecodingFailure("HSTORE", l), + identity + ) + } + } } diff --git a/types/src/main/scala/roc/types/package.scala b/types/src/main/scala/roc/types/package.scala index a4ab722..85c16ae 100644 --- a/types/src/main/scala/roc/types/package.scala +++ b/types/src/main/scala/roc/types/package.scala @@ -8,4 +8,5 @@ package object types { type Date = LocalDate type Time = LocalTime type TimestampWithTZ = ZonedDateTime + type HStore = Map[String, Option[String]] } diff --git a/types/src/test/scala/roc/types/decoders/DateTimeDecodersSpec.scala b/types/src/test/scala/roc/types/decoders/DateTimeDecodersSpec.scala index 7093acb..36a76b3 100644 --- a/types/src/test/scala/roc/types/decoders/DateTimeDecodersSpec.scala +++ b/types/src/test/scala/roc/types/decoders/DateTimeDecodersSpec.scala @@ -19,22 +19,16 @@ final class DateTimeDecodersSpec extends Specification with ScalaCheck { def is LocalDate must correctly decode Text representation $testValidTextLocalDate must throw a ElementDecodingFailure when Text decoding an invalid LocalDate $testInvalidTextLocalDate - must correctly decode Binary representation $testValidBinaryLocalDate - must throw a ElementDecodingFailure when Binary decoding an invalid LocalDate $testInvalidBinaryLocalDate must throw a NullDecodedFailure when Null decoding LocalDate $testLocalDateNullDecoding LocalTime must correctly decode Text representation $testValidLocalTimeText must throw a ElementDecodingFailure when Text decoding an invalid LocalTime $testInvalidLocalTimeText - must correctly decode Binary representation $testValidLocalTimeBinary - must throw a ElementDecodingFailure when Binary decoding an invalid LocalTime $testInvalidLocalTimeBinary must throw a NullDecodedFailure when Null decoding LocalDate $testLocalTimeNullDecoding ZonedDateTime must correctly decode Text representation $testValidZonedDateTimeText must throw a ElementDecodingFailure when Text decoding an invalid ZonedDateTime $testInvalidZonedDateTimeText - must correctly decode Binary representation $testValidZonedDateTimeBinary - must throw a ElementDecodingFailure when Binary decoding an invalid ZonedDateTime $testInvalidZonedDateTimeBinary must throw a NullDecodedFailure when Null decoding LocalDate $testZonedDateTimeNullDecoding """ diff --git a/types/src/test/scala/roc/types/decoders/DecodersSpec.scala b/types/src/test/scala/roc/types/decoders/DecodersSpec.scala index 48808d3..e7860d4 100644 --- a/types/src/test/scala/roc/types/decoders/DecodersSpec.scala +++ b/types/src/test/scala/roc/types/decoders/DecodersSpec.scala @@ -13,28 +13,21 @@ final class DecodersSpec extends Specification with ScalaCheck { def is = s2""" StringDecoder must return the correct String when Text Decoding a valid String ${StringDecoder().testTextDecoding} - must return the correct String when Binary Decoding a valid String Byte Array ${StringDecoder().testBinaryDecoding} must throw a NullDecodedFailure when Null Decoding a String ${StringDecoder().testNullDecoding} BooleanDecoder must return the correct Boolean when Text Decoding a valid Boolean String ${BooleanDecoder().testValidTextDecoding} must throw a ElementDecodingFailure when Text Decoding an Invalid Boolean String ${BooleanDecoder().testInvalidTextDecoding} - must return the correct Boolean when Binary Decoding a valid Boolean Byte Array ${BooleanDecoder().testValidByteDecoding} - must throw a ElementDecodingFailure when Binary Decoding an invalid Boolean Byte Array ${BooleanDecoder().testInvalidByteDecoding} must throw a NullDecodedFailure when Null Decoding an Boolean ${BooleanDecoder().testNullDecoding} OptionDecoder must return Some(A) when Text Decoding a valid A ${OptionDecoder().testValidTextDecoding} must throw a ElementDecodingFailure when Text Decoding an invalid A ${OptionDecoder().testInvalidTextDecoding} - must return Some(A) when Binary Decoding a valid A ${OptionDecoder().testValidBinaryDecoding} - must throw a ElementDecodingFailure when Binary Decoding an invalid A ${OptionDecoder().testInvalidBinaryDecoding} must return None when Null Decoding a valid A ${OptionDecoder().testNullDecoding} CharDecoder must return the correct Char when Text Decoding a valid String ${CharDecoder().testValidTextDecoding} must throw a ElementDecodingFailure when Text Decoding an invalid String ${CharDecoder().testInvalidTextDecoding} - must return the correct Char when Binary Decoding a valid Array[Byte] ${CharDecoder().testValidBinaryDecoding} - must throw a ElementDecodingFailure when Binary Decoding an invalid Array[Byte] ${CharDecoder().testInvalidBinaryDecoding} must throw a NullDecodedFailure when Null Decoding a Char ${CharDecoder().testNullDecoding} """ diff --git a/types/src/test/scala/roc/types/decoders/NumericDecodersSpec.scala b/types/src/test/scala/roc/types/decoders/NumericDecodersSpec.scala index bee43c5..c6e7491 100644 --- a/types/src/test/scala/roc/types/decoders/NumericDecodersSpec.scala +++ b/types/src/test/scala/roc/types/decoders/NumericDecodersSpec.scala @@ -14,36 +14,26 @@ final class NumericDecodersSpec extends Specification with ScalaCheck { def is = Short Decoder must return the correct Short when Text Decoding a valid Short String ${ShortDecoder().testValidTextDecoding} must throw a ElementDecodingFailure when TextDecoding an Invalid Short String ${ShortDecoder().testInvalidTextDecoding} - must return the correct Short when Binary Decoding a valid Short Byte Array ${ShortDecoder().testValidBinaryDecoding} - must throw a ElementDecodingFailure when Binary Decoding an invalid Short Byte Array ${ShortDecoder().testInvalidBinaryDecoding} must throw a NullDecodedFailure when Null Decoding a Short ${ShortDecoder().testNullDecoding} Int Decoder must return the correct Int when Text Decoding a valid Int String ${IntDecoder().testValidTextDecoding} must throw a ElementDecodingFailure when Text Decoding an Invalid Int String ${IntDecoder().testInvalidTextDecoding} - must return the correct Int when Binary Decoding a valid Int Byte Array ${IntDecoder().testValidByteDecoding} - must throw a ElementDecodingFailure when Binary Decoding an invalid Int Byte Array ${IntDecoder().testInvalidByteDecoding} must throw a NullDecodedFailure when Null Decoding an Int ${IntDecoder().testNullDecoding} Long Decoder must return the correct Long when Text Decoding a valid Long String ${LongDecoder().testValidTextDecoding} must throw a ElementDecodingFailure when Text Decoding an Invalid Long String ${LongDecoder().testInvalidTextDecoding} - must return the correct Long when Binary Decoding a valid Long Byte Array ${LongDecoder().testValidByteDecoding} - must throw a ElementDecodingFailure when Binary Decoding an invalid Long Byte Array ${LongDecoder().testInvalidByteDecoding} must throw a NullDecodedFailure when Null Decoding a Long ${LongDecoder().testNullDecoding} Float Decoder must return the correct Float when Text Decoding a valid Float String ${FloatDecoder().testValidTextDecoding} must throw a ElementDecodingFailure when Text Decoding an Invalid Float String ${FloatDecoder().testInvalidTextDecoding} - must return the correct Float when Binary Decoding a valid Float Byte Array ${FloatDecoder().testValidByteDecoding} - must throw a ElementDecodingFailure when Binary Decoding an invalid Float Byte Array ${FloatDecoder().testInvalidByteDecoding} must throw a NullDecodedFailure when Null Decoding a Float ${FloatDecoder().testNullDecoding} Double Decoder must return the correct Double when Text Decoding a valid Double String ${DoubleDecoder().testValidTextDecoding} must throw a ElementDecodingFailure when Text Decoding an Invalid Double String ${DoubleDecoder().testInvalidTextDecoding} - must return the correct Double when Binary Decoding a valid Double Byte Array ${DoubleDecoder().testValidByteDecoding} - must throw a ElementDecodingFailure when Binary Decoding an invalid Double Byte Array ${DoubleDecoder().testInvalidByteDecoding} must throw a NullDecodedFailure when Null Decoding a Double ${DoubleDecoder().testNullDecoding} """