packages feed

wireform-parquet-0.1.0.0: test/VariantRegression.hs

{-# LANGUAGE OverloadedStrings #-}

module VariantRegression (tests, values, malformed) where

import Control.Monad (forM_, unless)
import Data.Bits ((.|.))
import Data.ByteString qualified as BS
import Data.Either (isLeft)
import Data.Map.Strict qualified as M
import Data.Text qualified as T
import Data.Vector qualified as V
import GHC.Float (castWord32ToFloat, castWord64ToDouble)
import Hedgehog qualified as H
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Parquet.Variant


values :: V.Vector Variant
values =
  V.fromList
    [ VNull
    , VBool False
    , VBool True
    , VInt8 minBound
    , VInt8 maxBound
    , VInt16 minBound
    , VInt16 maxBound
    , VInt32 minBound
    , VInt32 maxBound
    , VInt64 minBound
    , VInt64 maxBound
    , VFloat 0
    , VFloat (-0)
    , VFloat (1 / 0)
    , VFloat (-1 / 0)
    , VFloat (castWord32ToFloat 0x7fc12345)
    , VDouble 0
    , VDouble (-0)
    , VDouble (1 / 0)
    , VDouble (-1 / 0)
    , VDouble (castWord64ToDouble 0x7ff8123456789012)
    , VDecimal4 0 0
    , VDecimal4 38 (-999999999)
    , VDecimal4 9 999999999
    , VDecimal8 38 (-999999999999999999)
    , VDecimal8 18 999999999999999999
    , VDecimal16 38 (negate (10 ^ (38 :: Int) - 1))
    , VDecimal16 0 (10 ^ (38 :: Int) - 1)
    , VDate 0
    , VDate (-40000)
    , VTimestamp (-1)
    , VTimestamp 2000000000000000
    , VTimestampNtz (-1)
    , VTimestampNtz 2000000000000000
    , VTimestampNanos minBound
    , VTimestampNanos maxBound
    , VTimestampNtzNanos minBound
    , VTimestampNtzNanos maxBound
    , VTime 0
    , VTime 86399999999
    , VBinary BS.empty
    , VBinary (BS.pack [0 .. 255])
    , VString ""
    , VString "a"
    , VString "λ🦊"
    , VString (T.replicate 63 "x")
    , VString (T.replicate 64 "x")
    , VString (T.replicate 128 "λ")
    , VUuid (BS.pack [0 .. 15])
    , VArray V.empty
    , VObject M.empty
    , VArray (V.fromList [VNull, VInt8 4, VString "five", VObject (M.singleton "nested" (VBool True))])
    , VObject (M.fromList [("a", VInt8 1), ("b", VArray (V.fromList [VNull, VString "λ"])), ("c", VObject (M.singleton "d" (VInt64 (-2))))])
    , VArray (V.replicate 300 (VString "wide"))
    , VObject (M.fromList (map (\n -> (T.replicate n "a", VInt32 (fromIntegral n))) [1 .. 300]))
    ]


-- Each case has a complete enclosing buffer. These specifically distinguish
-- validation from merely retrieving a prefix or parsing a leaf in isolation.
malformed :: [(String, BS.ByteString, BS.ByteString)]
malformed =
  map
    (\(name, metadata, value) -> (name, BS.pack metadata, BS.pack value))
    [ ("trailing-value", [1, 0, 0], [0, 255])
    , ("metadata-nonzero-start", [1, 1, 1, 2, 120, 97], [0])
    , ("metadata-unused-tail", [1, 0, 0, 120], [0])
    , ("metadata-false-sorted", [17, 2, 0, 1, 2, 98, 97], [0])
    , ("metadata-duplicate-sorted", [17, 2, 0, 1, 2, 97, 97], [0])
    , ("duplicate-object-key", [1, 1, 0, 1, 97], [2, 2, 0, 0, 0, 1, 2, 0, 0])
    , ("unordered-object-key", [1, 2, 0, 1, 2, 97, 98], [2, 2, 1, 0, 0, 1, 2, 0, 0])
    , ("shared-object-offset", [1, 2, 0, 1, 2, 97, 98], [2, 2, 0, 1, 0, 0, 1, 0])
    , ("object-offset-gap", [1, 1, 0, 1, 97], [2, 1, 0, 1, 2, 0, 0])
    , ("child-crosses-array-boundary", [1, 0, 0], [3, 1, 0, 1, 12, 127])
    , ("child-crosses-object-boundary", [1, 1, 0, 1, 97], [2, 1, 0, 0, 1, 12, 127])
    , ("array-child-trailing", [1, 0, 0], [3, 1, 0, 2, 0, 255])
    , ("descending-array-offsets", [1, 0, 0], [3, 2, 0, 2, 1, 0, 0])
    , ("decimal-bad-scale", [1, 0, 0], [32, 255, 0, 0, 0, 0])
    , ("decimal4-bad-precision", [1, 0, 0], [32, 0, 0, 202, 154, 59])
    , ("unknown-type", [1, 0, 0], [84])
    , ("invalid-short-utf8", [1, 0, 0], [5, 255])
    , ("invalid-long-utf8", [1, 0, 0], [64, 1, 0, 0, 0, 255])
    , ("huge-array-count", [1, 0, 0], [19, 255, 255, 255, 255])
    , ("huge-object-count", [1, 0, 0], [66, 255, 255, 255, 255])
    , ("huge-dictionary-count", [193, 255, 255, 255, 255], [0])
    , ("unsupported-version", [2, 0, 0], [0])
    ]


tests :: IO ()
tests = do
  let load = either fail pure
      check name condition = unless condition (fail ("Variant regression: " ++ name))
  V.forM_ values $ \value -> do
    encoded <- load (encodeVariant value)
    decoded <- load (decodeVariant (variantMetadata encoded) (variantValue encoded))
    reencoded <- load (encodeVariant decoded)
    -- Byte equality preserves NaN payloads and signed zero, unlike Eq Double.
    check "all primitive/container representations, including IEEE bits" (encoded == reencoded)
  forM_ malformed $ \(name, metadata, value) -> check name (isLeft (decodeVariant metadata value))
  let dictionary = BS.pack [1, 2, 0, 1, 2, 97, 98]
      shuffled = BS.pack [2, 2, 0, 1, 2, 0, 4, 12, 2, 12, 1]
      expected = VObject (M.fromList [("a", VInt8 1), ("b", VInt8 2)])
  check "nonmonotone object offsets remain valid" (decodeVariant dictionary shuffled == Right expected)
  check "reserved metadata and object bits ignored" (decodeVariant (BS.cons (BS.head dictionary .|. 32) (BS.tail dictionary)) (BS.cons (BS.head shuffled .|. 128) (BS.tail shuffled)) == Right expected)
  check "unsorted/nonunique dictionary permitted when not marked sorted" (decodeVariant (BS.pack [1, 3, 0, 1, 2, 3, 98, 97, 97]) (BS.pack [2, 1, 2, 0, 1, 0]) == Right (VObject (M.singleton "a" VNull)))
  forM_ [1 .. 4] $ \width -> do
    let word n = BS.pack (take width (n : repeat 0))
        metadata = BS.singleton (1 + fromIntegral ((width - 1) * 64)) <> word 1 <> word 0 <> word 1 <> "a"
        object = BS.singleton (2 + fromIntegral ((width - 1) * 4 + (width - 1) * 16)) <> BS.singleton 1 <> word 0 <> word 0 <> word 1 <> BS.singleton 0
    check "all legal ID/offset widths" (decodeVariant metadata object == Right (VObject (M.singleton "a" VNull)))
  forM_ [VUuid "short", VUuid (BS.replicate 17 0), VDecimal4 39 0, VDecimal4 0 1000000000, VDecimal8 0 1000000000000000000, VDecimal16 0 (10 ^ (38 :: Int)), VDecimal16 0 (negate (2 ^ (128 :: Int))), VTime (-1), VTime 86400000000] $ \value -> check "encoder never pads/truncates invalid values" (isLeft (encodeVariant value))
  let nested = VArray (V.singleton (VArray (V.singleton VNull)))
  encoded <- load (encodeVariant nested)
  check "depth override" (isLeft (decodeVariantWithLimits (VariantLimits 1 10 10) (variantMetadata encoded) (variantValue encoded)))
  check "aggregate value budget" (isLeft (decodeVariantWithLimits (VariantLimits 5 2 10) (variantMetadata encoded) (variantValue encoded)))
  check "exact budget accepted" (decodeVariantWithLimits (VariantLimits 2 3 0) (variantMetadata encoded) (variantValue encoded) == Right nested)
  check "encoder budget before collection" (isLeft (encodeVariantWithLimits (VariantLimits 1 10 10) nested))
  check "dictionary budget" (isLeft (decodeVariantWithLimits (VariantLimits 5 10 1) dictionary shuffled))
  shared <- load (buildVariantMetadata (V.fromList ["z", "a", "typed-only", "z"]))
  let fragment = VObject (M.singleton "z" (VInt8 42))
  fallback <- load (encodeVariantValue shared fragment)
  check "shredded fallback uses the full shared dictionary" (metadataKeys shared == V.fromList ["a", "typed-only", "z"] && decodeVariant (metadataBytes shared) fallback == Right fragment)
  check "undeclared fallback field fails instead of using a wrong field ID" (isLeft (encodeVariantValue shared (VObject (M.singleton "absent" VNull))))
  originalMetadata <- load (compileVariantMetadata (BS.pack [1, 3, 0, 1, 2, 3, 98, 97, 97]))
  preserved <- load (encodeVariantValue originalMetadata (VObject (M.singleton "a" VNull)))
  check "existing unsorted nonunique dictionary is preserved and indexed correctly" (decodeVariantValue originalMetadata preserved == Right (VObject (M.singleton "a" VNull)) && metadataBytes originalMetadata == BS.pack [1, 3, 0, 1, 2, 3, 98, 97, 97])
  let deep = foldr (\_ value -> VArray (V.singleton value)) VNull [1 .. 501 :: Int]
      raised = VariantLimits 501 502 0
  deepEncoded <- load (encodeVariantWithLimits raised deep)
  check "default depth limit is overridable for valid deep trees" (isLeft (encodeVariant deep) && isLeft (decodeVariant (variantMetadata deepEncoded) (variantValue deepEncoded)) && decodeVariantWithLimits raised (variantMetadata deepEncoded) (variantValue deepEncoded) == Right deep)
  properties <- sequence [H.check roundtripProperty, H.check mutatedProperty, H.check arbitraryBytesProperty]
  check "generated and adversarial binary properties" (and properties)
  putStrLn "Variant binary primitive/container/bounds regressions passed"


genVariant :: H.Gen Variant
genVariant =
  Gen.recursive
    Gen.choice
    [ Gen.element (take 49 (V.toList values))
    , VString <$> Gen.text (Range.linear 0 80) Gen.unicode
    , VBinary <$> Gen.bytes (Range.linear 0 80)
    , VInt64 <$> Gen.int64 Range.constantBounded
    ]
    [ VArray . V.fromList <$> Gen.list (Range.linear 0 8) genVariant
    , VObject . M.fromList <$> Gen.list (Range.linear 0 8) ((,) <$> Gen.text (Range.linear 0 20) Gen.unicode <*> genVariant)
    ]


roundtripProperty :: H.Property
roundtripProperty = H.withTests 1000 $ H.property $ do
  value <- H.forAll genVariant
  encoded <- H.evalEither (encodeVariant value)
  decoded <- H.evalEither (decodeVariant (variantMetadata encoded) (variantValue encoded))
  reencoded <- H.evalEither (encodeVariant decoded)
  reencoded H.=== encoded


mutatedProperty :: H.Property
mutatedProperty = H.withTests 3000 $ H.property $ do
  value <- H.forAll genVariant
  encoded <- H.evalEither (encodeVariant value)
  changeMetadata <- H.forAll Gen.bool
  replacement <- H.forAll (Gen.bytes (Range.linear 0 10))
  let target = if changeMetadata then variantMetadata encoded else variantValue encoded
  offset <- H.forAll (Gen.int (Range.linear 0 (BS.length target)))
  removed <- H.forAll (Gen.int (Range.linear 0 (BS.length target - offset)))
  let changed = BS.take offset target <> replacement <> BS.drop (offset + removed) target
      metadata = if changeMetadata then changed else variantMetadata encoded
      bytes = if changeMetadata then variantValue encoded else changed
  assertStable metadata bytes


arbitraryBytesProperty :: H.Property
arbitraryBytesProperty = H.withTests 3000 $ H.property $ do
  metadata <- H.forAll (Gen.bytes (Range.linear 0 128))
  bytes <- H.forAll (Gen.bytes (Range.linear 0 128))
  assertStable metadata bytes


assertStable :: BS.ByteString -> BS.ByteString -> H.PropertyT IO ()
assertStable metadata bytes = case decodeVariant metadata bytes of
  Left _ -> H.success
  Right value -> do
    encoded <- H.evalEither (encodeVariant value)
    decoded <- H.evalEither (decodeVariant (variantMetadata encoded) (variantValue encoded))
    stable <- H.evalEither (encodeVariant decoded)
    stable H.=== encoded