packages feed

cuddle-1.8.1.0: test/Test/Codec/CBOR/Cuddle/CBOR/Canonical.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Codec.CBOR.Cuddle.CBOR.Canonical (spec) where

import Codec.CBOR.Cuddle.CBOR.Canonical (
  CanonicalTerm (..),
  fromNInt,
  nintMin,
  toCanonical,
  toNInt,
  uintMax,
 )
import Codec.CBOR.Term (Term (..))
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BSL
import Data.Text.Lazy qualified as TL
import Data.Word (Word64)
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck

spec :: Spec
spec = do
  describe "NInt" $ do
    it "toNInt accepts exactly [-2^64, -1]" $ do
      toNInt 0 `shouldBe` Nothing
      toNInt 1 `shouldBe` Nothing
      toNInt (nintMin - 1) `shouldBe` Nothing
      toNInt nintMin `shouldNotBe` Nothing
      toNInt (-1) `shouldNotBe` Nothing

    prop "toNInt . fromNInt = Just" $ \n ->
      toNInt (fromNInt n) === Just n

    it "boundary values roundtrip" $ do
      fmap fromNInt (toNInt (-1)) `shouldBe` Just (-1)
      fmap fromNInt (toNInt nintMin) `shouldBe` Just nintMin

    prop "Ord on NInt agrees with Ord on the represented Integer" $ \a b ->
      compare a b === compare (fromNInt a) (fromNInt b)

  describe "toCanonical" $ do
    describe "integer normalization" $ do
      prop "TInt and TInteger of the same value are equal" $ \i ->
        toCanonical (TInt i) === toCanonical (TInteger (toInteger i))

      prop "in-range positive bignum collapses to CTInt" $ \(w :: Word64) ->
        let n = toInteger w
         in toCanonical (TTagged 2 (TBytes (unsignedBE n)))
              === toCanonical (TInteger n)

      prop "in-range negative bignum collapses to CTNInt" $ \nint ->
        let n = fromNInt nint
         in toCanonical (TTagged 3 (TBytes (unsignedBE (-1 - n))))
              === toCanonical (TInteger n)

      it "bignum with leading zeros canonicalizes" $
        toCanonical (TTagged 2 (TBytes (BS.pack [0, 0, 5])))
          `shouldBe` CTInt 5

      prop "bignum from TBytesI matches TBytes" $ \(w :: Word64) ->
        let bs = unsignedBE (toInteger w)
         in toCanonical (TTagged 2 (TBytesI (BSL.fromStrict bs)))
              === toCanonical (TTagged 2 (TBytes bs))

      it "true bignum (above uintMax) stays tagged" $
        let n = uintMax + 1
         in toCanonical (TInteger n)
              `shouldBe` CTTagged 2 (CTBytes (unsignedBE n))

      it "true bignum (below nintMin) stays tagged" $
        let n = nintMin - 1
         in toCanonical (TInteger n)
              `shouldBe` CTTagged 3 (CTBytes (unsignedBE (-1 - n)))

    describe "definite/indefinite variants merge" $ do
      it "TBytes ≡ TBytesI" $
        toCanonical (TBytes "abc")
          `shouldBe` toCanonical (TBytesI (BSL.fromStrict "abc"))

      it "TString ≡ TStringI" $
        toCanonical (TString "abc")
          `shouldBe` toCanonical (TStringI (TL.fromStrict "abc"))

      it "TList ≡ TListI" $
        toCanonical (TList [TInt 1, TInt 2])
          `shouldBe` toCanonical (TListI [TInt 1, TInt 2])

      it "TMap ≡ TMapI" $
        toCanonical (TMap [(TInt 1, TInt 2)])
          `shouldBe` toCanonical (TMapI [(TInt 1, TInt 2)])

    describe "bool/null go through CTSimple" $ do
      it "TBool False ≡ TSimple 20" $
        toCanonical (TBool False) `shouldBe` toCanonical (TSimple 20)
      it "TBool True ≡ TSimple 21" $
        toCanonical (TBool True) `shouldBe` toCanonical (TSimple 21)
      it "TNull ≡ TSimple 22" $
        toCanonical TNull `shouldBe` toCanonical (TSimple 22)

    describe "maps" $ do
      it "key order is irrelevant" $
        toCanonical (TMap [(TInt 1, TInt 10), (TInt 2, TInt 20)])
          `shouldBe` toCanonical (TMap [(TInt 2, TInt 20), (TInt 1, TInt 10)])

    describe "floats stay distinct by width" $ do
      it "THalf 1.0 ≠ TFloat 1.0" $
        toCanonical (THalf 1.0) `shouldNotBe` toCanonical (TFloat 1.0)
      it "TFloat 1.0 ≠ TDouble 1.0" $
        toCanonical (TFloat 1.0) `shouldNotBe` toCanonical (TDouble 1.0)

-- | Encode a non-negative Integer as a big-endian byte string with no leading zeros.
unsignedBE :: Integer -> BS.ByteString
unsignedBE = BS.pack . reverse . go
  where
    go 0 = []
    go n = fromInteger (n `mod` 256) : go (n `div` 256)