cborg-0.2.1.0: tests/Tests/Boundary.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Tests.Boundary
( testTree -- :: TestTree
) where
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import Data.Either
import Data.Int
import Data.Word
import qualified Data.Text as T
import Codec.CBOR.Decoding
import Codec.CBOR.Encoding
import Codec.CBOR.Read
import Codec.CBOR.Write
import Tests.Util
import Test.Tasty
import Test.Tasty.QuickCheck
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
-- | CBOR can represent 64 bit negative and positive integers, hence we need
-- wrapper for Integer to represent the whole range.
newtype Int65 = Int65 Integer
deriving (Eq, Ord, Enum, Num, Integral, Real, Show)
instance Bounded Int65 where
maxBound = Int65 (2^(64 :: Int) - 1)
minBound = Int65 (-2^(64 :: Int))
instance Arbitrary Int65 where
arbitrary = arbitraryBoundedIntegral
encodeInt65 :: Int65 -> Encoding
encodeInt65 (Int65 n) = encodeInteger n
-- | Wrapper for bounded, integral type 'a' that potentially contains values
-- outside of range of 'a'.
newtype B a = B { unB :: BRep a }
type family BRep a where
BRep Word = Word64
BRep Word8 = Word64
BRep Word16 = Word64
BRep Word32 = Word64
BRep Word64 = Word64
BRep Int = Int65
BRep Int8 = Int64
BRep Int16 = Int64
BRep Int32 = Int64
BRep Int64 = Int65
instance Show (BRep a) => Show (B a) where
showsPrec p = showsPrec p . unB
instance (Arbitrary (BRep a), Num (BRep a), Bounded a, Integral a
) => Arbitrary (B a) where
arbitrary = B <$> arbitraryWithBounds (undefined::a)
-- | Check if deserialisation of values of type 'a' deals properly with the ones
-- out of range, i.e. fails to decode them.
boundaryTest
:: forall a rep. (Bounded a, Integral a, Show a, rep ~ BRep a,
Ord rep, Num rep)
=> (rep -> Encoding) -- ^ encode
-> (forall s. Decoder s a) -- ^ decode
-> B a
-> Property
boundaryTest enc dec a = if outsideRange
then collect "outside" $ isLeft a'
else collect "inside" $ isRight a'
where
a' = deserialiseFromBytes dec . toLazyByteString . enc $ unB a
-- Note that this is always true for a ~ rep.
outsideRange = unB a < fromIntegral (minBound :: a)
|| unB a > fromIntegral (maxBound :: a)
mkBoundaryTest
:: forall a rep. (Bounded a, Integral a, Show a, rep ~ BRep a,
Arbitrary rep, Ord rep, Show rep, Num rep)
=> String
-> (rep -> Encoding)
-> (forall s. Decoder s a)
-> (forall s. Decoder s a)
-> [TestTree]
mkBoundaryTest aName enc dec decCan =
[ testProperty aName $ boundaryTest enc dec
, testProperty (aName ++ " (canonical)") $ boundaryTest enc decCan
]
----------------------------------------
-- | Check if deserialisation of map/list length deals properly with the ones
-- out of range, i.e. fails to decode them.
lenBoundaryTest
:: (Word -> Encoding)
-> (forall s. Decoder s Int)
-> Length
-> Property
lenBoundaryTest enc dec a = if outsideRange
then collect "outside" $ isLeft a'
else collect "inside" $ isRight a'
where
a' = deserialiseFromBytes dec . toLazyByteString . enc $ unLength a
outsideRange = fromIntegral (unLength a) < (0::Int)
mkLenBoundaryTest
:: String
-> (Word -> Encoding)
-> (forall s. Decoder s Int)
-> (forall s. Decoder s Int)
-> [TestTree]
mkLenBoundaryTest aName enc dec decCan =
[ testProperty aName $ lenBoundaryTest enc dec
, testProperty (aName ++ " (canonical)") $ lenBoundaryTest enc decCan
]
----------------------------------------
data StringLengthPrefix = StringLP Word BSL.ByteString
deriving Show
instance Arbitrary StringLengthPrefix where
arbitrary = (\l -> StringLP (unLength l) (mkLengthPrefix True l)) <$> arbitrary
data BytesLengthPrefix = BytesLP Word BSL.ByteString
deriving Show
instance Arbitrary BytesLengthPrefix where
arbitrary = (\l -> BytesLP (unLength l) (mkLengthPrefix False l)) <$> arbitrary
-- | Test that positive length prefixes of string/bytes are parsed successfully,
-- whereas negative are not.
stringBytesBoundaryTest :: [TestTree]
stringBytesBoundaryTest =
[ testProperty "String" $ \(StringLP w bs) ->
case deserialiseFromBytes decodeString bs of
Right (_rest, string) -> w == 0 && T.length string == 0
Left (DeserialiseFailure _ msg) -> if fromIntegral w < (0::Int)
then msg == "expected string"
else msg == "end of input"
, testProperty "Bytes" $ \(BytesLP w bs) ->
case deserialiseFromBytes decodeBytes bs of
Right (_rest, bytes) -> w == 0 && BS.length bytes == 0
Left (DeserialiseFailure _ msg) -> if fromIntegral w < (0::Int)
then msg == "expected bytes"
else msg == "end of input"
]
----------------------------------------
testTree :: TestTree
testTree = localOption (QuickCheckTests 1000) . testGroup "Boundary checks" $ concat
[ mkBoundaryTest "Word" encodeWord64 decodeWord decodeWordCanonical
, mkBoundaryTest "Word8" encodeWord64 decodeWord8 decodeWord8Canonical
, mkBoundaryTest "Word16" encodeWord64 decodeWord16 decodeWord16Canonical
, mkBoundaryTest "Word32" encodeWord64 decodeWord32 decodeWord32Canonical
, mkBoundaryTest "Word64" encodeWord64 decodeWord64 decodeWord64Canonical
, mkBoundaryTest "Int" encodeInt65 decodeInt decodeIntCanonical
, mkBoundaryTest "Int8" encodeInt64 decodeInt8 decodeInt8Canonical
, mkBoundaryTest "Int16" encodeInt64 decodeInt16 decodeInt16Canonical
, mkBoundaryTest "Int32" encodeInt64 decodeInt32 decodeInt32Canonical
, mkBoundaryTest "Int64" encodeInt65 decodeInt64 decodeInt64Canonical
, mkLenBoundaryTest "ListLen" encodeListLen decodeListLen decodeListLenCanonical
, mkLenBoundaryTest "MapLen" encodeMapLen decodeMapLen decodeMapLenCanonical
, stringBytesBoundaryTest
]