packages feed

hs-bindgen-runtime-1.0.0.0: test/Test/HsBindgen/Runtime/SizedByteArray.hs

{-# OPTIONS_GHC -Wno-orphans #-}

module Test.HsBindgen.Runtime.SizedByteArray (tests) where

import Data.Primitive.ByteArray qualified as BA
import Data.Proxy (Proxy (..))
import Data.Word (Word8)
import Foreign.Marshal.Alloc (allocaBytes)
import Foreign.Ptr (Ptr, castPtr)
import Foreign.Storable (Storable (..))
import GHC.TypeNats (KnownNat)
import Test.QuickCheck (Arbitrary (..), Property, ioProperty, vectorOf, (===))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import Test.Tasty.QuickCheck (Testable, testProperty)

import HsBindgen.Runtime.Marshal
import HsBindgen.Runtime.Support.SizedByteArray (SizedByteArray (..))

import Test.Util.Orphans ()

{-------------------------------------------------------------------------------
  Helper functions
-------------------------------------------------------------------------------}

-- | Create a 'SizedByteArray' from a list of bytes
--
-- This number of bytes must match the size of the array.
fromBytes :: forall n m.
     (KnownNat n, KnownNat m)
  => [Word8]
  -> SizedByteArray n m
fromBytes bytes
    | length bytes == size = SizedByteArray $ BA.byteArrayFromList bytes
    | otherwise            = error "fromBytes length mismatch"
  where
    size :: Int
    size = staticSizeOf (Proxy :: Proxy (SizedByteArray n m))

-- | Extract bytes from a 'SizedByteArray'
toBytes :: SizedByteArray n m -> [Word8]
toBytes (SizedByteArray arr) = BA.foldrByteArray (:) [] arr

{-------------------------------------------------------------------------------
  Reference implementation
-------------------------------------------------------------------------------}

-- | Read bytes using 'Storable'
--
-- This does /not/ use the 'Storable' instance of 'SizedByteArray'.
peekSBA :: forall n m.
     (KnownNat n, KnownNat m)
  => Ptr (SizedByteArray n m)
  -> IO (SizedByteArray n m)
peekSBA ptr = do
    let ptr' = castPtr ptr
        size = staticSizeOf (Proxy :: Proxy (SizedByteArray n m))
    fromBytes <$> mapM (peekElemOff ptr') [0 .. size - 1]

-- | Write bytes using 'Storable'
--
-- This does /not/ use the 'Storable' instance of 'SizedByteArray'.
pokeSBA :: Ptr a -> (SizedByteArray n m) -> IO ()
pokeSBA ptr sba = do
    let ptr' = castPtr ptr
    mapM_ (uncurry (pokeElemOff ptr')) $ zip [0..] (toBytes sba)

{-------------------------------------------------------------------------------
  Arbitrary instances
-------------------------------------------------------------------------------}

instance (KnownNat n, KnownNat m) => Arbitrary (SizedByteArray n m) where
    arbitrary =
      let size = staticSizeOf (Proxy :: Proxy (SizedByteArray n m))
      in  fromBytes <$> vectorOf size (arbitrary @Word8)

    shrink sba = case span (== 0x00) (toBytes sba) of
      (ls, (_:rs)) -> [fromBytes (ls ++ 0x00 : rs)]
      (_,  [])     -> []

{-------------------------------------------------------------------------------
  Tests
-------------------------------------------------------------------------------}

tests :: TestTree
tests = testGroup "HsBindgen.Runtime.SizedByteArray" [
      testGroup "StaticSize" [
          test_staticSizeOf
        , test_staticAlignment
        ]
    , testGroup "ReadRaw+WriteRaw" [
          testGroup "readRaw reads" [
              aux8  prop_readRaw
            , aux16 prop_readRaw
            , aux32 prop_readRaw
            ]
        , testGroup "writeRaw writes" [
              aux8  prop_writeRaw
            , aux16 prop_writeRaw
            , aux32 prop_writeRaw
            ]
        , testGroup "read read (reading twice gets the same results)" [
              aux8  prop_readRead
            , aux16 prop_readRead
            , aux32 prop_readRead
            ]
        , testGroup "write read (you get back what you put in)" [
              aux8  prop_writeRead
            , aux16 prop_writeRead
            , aux32 prop_writeRead
            ]
        , testGroup "read write (putting back what you got out has no effect)" [
              aux8  prop_readWrite
            , aux16 prop_readWrite
            , aux32 prop_readWrite
            ]
        , testGroup "write write (putting twice is same as putting once)" [
              aux8  prop_writeWrite
            , aux16 prop_writeWrite
            , aux32 prop_writeWrite
            ]
        ]
    ]
  where
    aux8 :: Testable a => (Proxy (SizedByteArray 8 2) -> a) -> TestTree
    aux8 mkProp = testProperty "SizedByteArray 8 2" $ mkProp Proxy

    aux16 :: Testable a => (Proxy (SizedByteArray 16 4) -> a) -> TestTree
    aux16 mkProp = testProperty "SizedByteArray 16 4" $ mkProp Proxy

    aux32 :: Testable a => (Proxy (SizedByteArray 32 8) -> a) -> TestTree
    aux32 mkProp = testProperty "SizedByteArray 32 8" $ mkProp Proxy

{-------------------------------------------------------------------------------
  StaticSize
-------------------------------------------------------------------------------}

test_staticSizeOf :: TestTree
test_staticSizeOf = testGroup "staticSizeof" [
      testCase "4"  $ staticSizeOf (Proxy :: Proxy (SizedByteArray  4 2)) @?= 4
    , testCase "8"  $ staticSizeOf (Proxy :: Proxy (SizedByteArray  8 2)) @?= 8
    , testCase "16" $ staticSizeOf (Proxy :: Proxy (SizedByteArray 16 2)) @?= 16
    , testCase "32" $ staticSizeOf (Proxy :: Proxy (SizedByteArray 32 2)) @?= 32
    ]

test_staticAlignment :: TestTree
test_staticAlignment = testGroup "staticAlignment" [
      testCase "1" $ staticAlignment (Proxy :: Proxy (SizedByteArray 8 1)) @?= 1
    , testCase "2" $ staticAlignment (Proxy :: Proxy (SizedByteArray 8 2)) @?= 2
    , testCase "4" $ staticAlignment (Proxy :: Proxy (SizedByteArray 8 4)) @?= 4
    , testCase "8" $ staticAlignment (Proxy :: Proxy (SizedByteArray 8 8)) @?= 8
    ]

{-------------------------------------------------------------------------------
  ReadRaw+WriteRaw
-------------------------------------------------------------------------------}

prop_readRaw :: forall n m.
     (KnownNat n, KnownNat m)
  => Proxy (SizedByteArray n m)
  -> SizedByteArray n m
  -> Property
prop_readRaw proxy sba = ioProperty $
    allocaBytes (staticSizeOf proxy) $ \ptr -> do
      pokeSBA ptr sba
      sba' <- readRaw ptr
      return $ sba === sba'

prop_writeRaw :: forall n m.
     (KnownNat n, KnownNat m)
  => Proxy (SizedByteArray n m)
  -> SizedByteArray n m
  -> Property
prop_writeRaw proxy sba = ioProperty $
    allocaBytes (staticSizeOf proxy) $ \ptr -> do
      writeRaw ptr sba
      sba' <- peekSBA ptr
      return $ sba === sba'

{-------------------------------------------------------------------------------
  ReadRaw+WriteRaw: applicable laws from primLaws in quickcheck-classes
-------------------------------------------------------------------------------}

prop_readRead :: forall n m.
     (KnownNat n, KnownNat m)
  => Proxy (SizedByteArray n m)
  -> SizedByteArray n m
  -> Property
prop_readRead proxy sba = ioProperty $
    allocaBytes (staticSizeOf proxy) $ \ptr -> do
      pokeSBA (ptr :: Ptr (SizedByteArray 8 2)) sba
      sba2 <- readRaw ptr
      sba3 <- readRaw ptr
      return $ sba2 === sba3

prop_writeRead :: forall n m.
     (KnownNat n, KnownNat m)
  => Proxy (SizedByteArray n m)
  -> SizedByteArray n m
  -> Property
prop_writeRead proxy sba = ioProperty $
    allocaBytes (staticSizeOf proxy) $ \ptr -> do
      writeRaw ptr sba
      sba' <- readRaw ptr
      return $ sba === sba'

prop_readWrite :: forall n m.
     (KnownNat n, KnownNat m)
  => Proxy (SizedByteArray n m)
  -> SizedByteArray n m
  -> Property
prop_readWrite proxy sba = ioProperty $
    allocaBytes (staticSizeOf proxy) $ \ptr -> do
      pokeSBA ptr sba
      writeRaw ptr =<< readRaw ptr
      sba' <- peekSBA ptr
      return $ sba === sba'

prop_writeWrite :: forall n m.
     (KnownNat n, KnownNat m)
  => Proxy (SizedByteArray n m)
  -> SizedByteArray n m
  -> Property
prop_writeWrite proxy sba = ioProperty $
    allocaBytes (staticSizeOf proxy) $ \ptr -> do
      writeRaw ptr sba
      sba2 <- peekSBA ptr
      writeRaw ptr sba
      sba3 <- peekSBA ptr
      return $ sba2 === sba3