hs-bindgen-runtime-1.0.0.0: test/Test/HsBindgen/Runtime/Bitfield.hs
-- Record fields are just used to make QuickCheck failures easy to read.
{-# LANGUAGE DuplicateRecordFields #-}
module Test.HsBindgen.Runtime.Bitfield (tests) where
import Data.Bits (Bits (..), FiniteBits (..))
import Data.Int
import Data.Proxy
import Data.Word
import Foreign qualified
import Test.QuickCheck ((===))
import Test.QuickCheck qualified as QC
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.QuickCheck (testProperty)
import HsBindgen.Runtime.BitfieldPtr qualified as BitfieldPtr
import HsBindgen.Runtime.Marshal qualified as Marshal
import HsBindgen.Runtime.Support.Bitfield (Bitfield)
import HsBindgen.Runtime.Support.Bitfield qualified as Bitfield
{-------------------------------------------------------------------------------
Tests
-------------------------------------------------------------------------------}
tests :: TestTree
tests = testGroup "HsBindgen.Runtime.Support.Bitfield" [
testGroup "extend . narrow" [
testProperty "Word8" (unsigned_extend_narrow_prop @Word8)
, testProperty "Word16" (unsigned_extend_narrow_prop @Word16)
, testProperty "Word32" (unsigned_extend_narrow_prop @Word32)
, testProperty "Word64" (unsigned_extend_narrow_prop @Word64)
, testProperty "Int8" (signed_extend_narrow_prop @Int8)
, testProperty "Int16" (signed_extend_narrow_prop @Int16)
, testProperty "Int32" (signed_extend_narrow_prop @Int32)
, testProperty "Int64" (signed_extend_narrow_prop @Int64)
]
, testGroup "peek . poke" [
testProperty "Struct1" (peek_poke_prop @Struct1)
, testProperty "Struct2" (peek_poke_prop @Struct2)
, testProperty "Struct3" (peek_poke_prop @Struct3)
, testProperty "Struct4" (peek_poke_prop @Struct4)
, testProperty "Struct5" (peek_poke_prop @Struct5)
, testProperty "Struct8" (peek_poke_prop @Struct8)
, testProperty "Struct11" (peek_poke_prop @Struct11)
]
, testProperty
"getBitfieldLE . putBitfieldLE"
getBitfieldLE_putBitfieldLE_prop
, testProperty
"getBitfieldBE . putBitfieldBE"
getBitfieldBE_putBitfieldBE_prop
]
{-------------------------------------------------------------------------------
extend . narrow tests
-------------------------------------------------------------------------------}
-- | Unsigned value with a given bit-width
--
-- Constraints:
--
-- * @width@ is in range @[1, typeWidth]@
-- * Non-bit-field bits in @x@ are cleared
data UnsignedValue a = UnsignedValue {
x :: a
, width :: Int
}
deriving stock Show
-- | Construct an 'UnsignedValue'
mkUnsignedValue :: (FiniteBits a, Num a) => a -> Int -> UnsignedValue a
mkUnsignedValue x' width =
let x = x' .&. Bitfield.loMask width
in UnsignedValue x width
instance
(Bounded a, FiniteBits a, Integral a, QC.Arbitrary a)
=> QC.Arbitrary (UnsignedValue a)
where
arbitrary = do
let typeWidth = finiteBitSize @a 0
width <- QC.chooseInt (1, typeWidth)
x' <- QC.arbitrarySizedBoundedIntegral
return $ mkUnsignedValue x' width
-- Shrinks value, keeping width
shrink (UnsignedValue x width) =
map (`mkUnsignedValue` width) (QC.shrink x)
-- | Narrowing and then extending an unsigned value results in the same value
unsigned_extend_narrow_prop ::
(Bitfield a, Eq a, Show a)
=> UnsignedValue a
-> QC.Property
unsigned_extend_narrow_prop (UnsignedValue x width) =
Bitfield.extend (Bitfield.narrow x width) width === x
-- | Signed value with a given bit-width
--
-- Constraints:
--
-- * @width@ is in range @[1, typeWidth]@
-- * Non-bit-field bits in @x@ are filled/cleared, depending on the sign
data SignedValue a = SignedValue {
x :: a
, width :: Int
}
deriving stock Show
-- | Construct a 'SignedValue'
--
-- The most significant bit of the bit-field determines the sign.
mkSignedValue :: (FiniteBits a, Num a) => a -> Int -> SignedValue a
mkSignedValue x' width =
let negative = testBit x' (width - 1)
x | negative = x' .|. Bitfield.hiMask width
| otherwise = x' .&. Bitfield.loMask width
in SignedValue x width
instance
(Bounded a, FiniteBits a, Integral a, QC.Arbitrary a)
=> QC.Arbitrary (SignedValue a)
where
arbitrary = do
let typeWidth = finiteBitSize @a 0
width <- QC.chooseInt (1, typeWidth)
x' <- QC.arbitrarySizedBoundedIntegral
return $ mkSignedValue x' width
-- Shrinks value, keeping width and sign
shrink (SignedValue x width) =
let negative = x < 0
sameSign (SignedValue x' _) = (x' < 0) == negative
in filter sameSign $ map (`mkSignedValue` width) (QC.shrink x)
-- | Narrowing and then extending a signed value results in the same value
signed_extend_narrow_prop ::
(Bitfield a, Num a, Ord a, Show a)
=> SignedValue a
-> QC.Property
signed_extend_narrow_prop (SignedValue x width) =
QC.label (if x < 0 then "negative" else "non-negative") $
Bitfield.extend (Bitfield.narrow x width) width === x
{-------------------------------------------------------------------------------
peek . poke tests
-------------------------------------------------------------------------------}
-- | Bit-field offset (bits) and width (bits) within @struct@ @a@
--
-- These parameters are mutually dependent on the size of the @struct@.
--
-- Constraints:
--
-- * @off@ is in range @[0, structWidth)@
-- * @width@ is in range @[1, min structWidth 64]@
-- * @off + width <= structWidth@
data BitfieldParams a = BitfieldParams {
off :: Int
, width :: Int
}
deriving stock Show
instance Marshal.StaticSize a => QC.Arbitrary (BitfieldParams a) where
arbitrary = do
let structWidth = 8 * Marshal.staticSizeOf @a Proxy
width <- QC.chooseInt (1, min structWidth 64)
off <- QC.chooseInt (0, structWidth - width)
return $ BitfieldParams off width
-- Shrinks offset then width one at a time to ensure minimal counterexample
shrink (BitfieldParams off width) =
let shrinkOffs
| off == 0 = []
| otherwise = [BitfieldParams o width | o <- [0 .. off - 1]]
shrinkWidths
| width == 1 = []
| otherwise = [BitfieldParams off w | w <- [1 .. width - 1]]
in shrinkOffs ++ shrinkWidths
-- | Poking and then peeking gets the same value (when narrowed)
peek_poke_prop :: forall a.
Marshal.StaticSize a
=> BitfieldParams a -- ^ Bitfield offset and width
-> QC.Large Word64 -- ^ Test value
-> QC.Property
peek_poke_prop (BitfieldParams off width) (QC.Large x) =
QC.label label . QC.ioProperty $ do
y <- Foreign.allocaBytesAligned @a size 8 $ \ptr -> do
Foreign.fillBytes ptr 0xAA size
let bitfieldPtr = BitfieldPtr.mkBitfieldPtr ptr off width
BitfieldPtr.poke bitfieldPtr x
BitfieldPtr.peek bitfieldPtr
return $ y === Bitfield.narrow x width
where
size, structWidth :: Int
size = Marshal.staticSizeOf @a Proxy
structWidth = 8 * size
-- This is a simplification that assumes the following alignment:
--
-- > Marshal.staticAlignment @Word8 Proxy == 1
-- > Marshal.staticAlignment @Word16 Proxy == 2
-- > Marshal.staticAlignment @Word32 Proxy == 4
-- > Marshal.staticAlignment @Word64 Proxy == 8
--
-- This is only possible because the test ensures that the @struct@ pointer
-- is 64-bit aligned.
--
-- Labels may be inaccurate if run on an architecture where the alignment
-- differs, but the actual implementation does /not/ make such assumptions.
label :: String
label
| rem8 + width <= 8 = "aligned:1"
| rem16 + width <= 16 && 16 * quot16 + 16 <= structWidth = "aligned:2"
| rem32 + width <= 32 && 32 * quot32 + 32 <= structWidth = "aligned:4"
| rem64 + width <= 64 && 64 * quot64 + 64 <= structWidth = "aligned:8"
| otherwise = "bytes:" ++ show minBytes
rem8, quot16, rem16, quot32, rem32, quot64, rem64 :: Int
rem8 = rem off 8
(quot16, rem16) = quotRem off 16
(quot32, rem32) = quotRem off 32
(quot64, rem64) = quotRem off 64
minBytes :: Int
minBytes = case quotRem (rem8 + width) 8 of
(bytes, 0) -> bytes
(bytes, _) -> bytes + 1
{-------------------------------------------------------------------------------
getBitfield?E . putBitfield?E tests
-------------------------------------------------------------------------------}
-- | Bit-field value, normalized offset (bits), and width (bits)
--
-- Constraints:
--
-- * @off@ is in range @[0, 7)@
-- * @width@ is in range @[1, 64]@
-- * Non-bit-field bits in @x@ are cleared
data BitfieldEndianParams = BitfieldEndianParams {
x :: Word64
, off :: Int
, width :: Int
}
deriving stock Show
-- | Construct a 'BitfieldEndianParams'
mkBitfieldEndianParams :: Word64 -> Int -> Int -> BitfieldEndianParams
mkBitfieldEndianParams x' off width =
let x = x' .&. Bitfield.loMask width
in BitfieldEndianParams x off width
instance QC.Arbitrary BitfieldEndianParams where
arbitrary = do
off <- QC.chooseInt (0, 7)
width <- QC.chooseInt (1, 64)
x' <- QC.arbitrarySizedBoundedIntegral
return $ mkBitfieldEndianParams x' off width
-- Shrinks value, then offset then width one at a time to ensure minimal
-- counterexample
shrink (BitfieldEndianParams x off width) =
let shrinkValues
| x == 0 = []
| otherwise =
map (\x' -> mkBitfieldEndianParams x' off width) (QC.shrink x)
shrinkOffs
| off == 0 = []
| otherwise =
map (\o -> mkBitfieldEndianParams x o width) [0 .. off - 1]
shrinkWidths
| width == 1 = []
| otherwise =
map (\w -> mkBitfieldEndianParams x off w) [1 .. width - 1]
in shrinkValues ++ shrinkOffs ++ shrinkWidths
-- | Encoding a bit-field as bytes and then decoding the bytes results in the
-- same value (little endian)
getBitfieldLE_putBitfieldLE_prop :: BitfieldEndianParams -> QC.Property
getBitfieldLE_putBitfieldLE_prop (BitfieldEndianParams x off width) =
let bytes = Bitfield.putBitfieldLE off width 0xAA 0xAA x
eey = Bitfield.getBitfieldLE off width bytes
in eey === Right x
-- | Encoding a bit-field as bytes and then decoding the bytes results in the
-- same value (big endian)
getBitfieldBE_putBitfieldBE_prop :: BitfieldEndianParams -> QC.Property
getBitfieldBE_putBitfieldBE_prop (BitfieldEndianParams x off width) =
let bytes = Bitfield.putBitfieldBE off width 0xAA 0xAA x
eey = Bitfield.getBitfieldBE off width bytes
in eey === Right x
{-------------------------------------------------------------------------------
Auxiliary types
These types are needed in order to use the "HsBindgen.Runtime.BitfieldPtr"
API. They are just used to specify the size of a @struct@ that contains a
bit-field.
-------------------------------------------------------------------------------}
type Struct1 = Word8
type Struct2 = Word16
data Struct3
instance Marshal.StaticSize Struct3 where
staticSizeOf _ = 3
staticAlignment _ = 1
type Struct4 = Word32
data Struct5
instance Marshal.StaticSize Struct5 where
staticSizeOf _ = 5
staticAlignment _ = 1
type Struct8 = Word64
data Struct11
instance Marshal.StaticSize Struct11 where
staticSizeOf _ = 11
staticAlignment _ = 1