packages feed

mono-hash-0.1.1.0: test/Main.hs

{-# LANGUAGE TypeApplications #-}

module Main (main) where

import Data.Bits (Bits, shiftL, shiftR, (.&.))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.Char8 as BSLC
import Data.Hash.Mono.FNV (fnv1, fnv1a)
import Data.Hash.Mono.HalfSipHash
import Data.Hash.Mono.SipHash
import Data.Int (Int16, Int32, Int64, Int8)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.WideWord.Int128 (Int128 (..))
import Data.WideWord.Int256 (Int256 (..))
import Data.WideWord.Word128 (Word128)
import Data.WideWord.Word256 (Word256)
import Data.Word (Word16, Word32, Word64, Word8)
import GHC.Float (castDoubleToWord64, castFloatToWord32)
import qualified Test.Data.ByteFoldable
import qualified Test.Data.FNV
import qualified Test.Data.HalfSipHash
import qualified Test.Data.SipHash
import Test.Tasty (TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))

main :: IO ()
main = defaultMain tests

{- | Code points spanning the one- to four-byte UTF-8 ranges, including the
boundaries between them.
-}
charEncodingInput :: String
charEncodingInput = "\x00\x7f\x80\x7ff\x800\xffff\x10000\x1234\x1F600\x10FFFF"

-- | UTF-8 bytes of a 'Char'; lone surrogates use the 3-byte WTF-8 form.
charBytes :: Char -> [Word8]
charBytes c
    | n < 0x80 = [fromIntegral n]
    | n < 0x800 = [byte 0xc0 6, byte 0x80 0]
    | n < 0x10000 = [byte 0xe0 12, byte 0x80 6, byte 0x80 0]
    | otherwise = [byte 0xf0 18, byte 0x80 12, byte 0x80 6, byte 0x80 0]
  where
    n = fromEnum c
    byte mask shift = fromIntegral (mask + ((n `shiftR` shift) .&. 0x3f))

-- | UTF-8 bytes of a 'String'.
utf8 :: String -> BS.ByteString
utf8 = TE.encodeUtf8 . T.pack

-- | Big-endian bytes of a fixed-width word.
wordBytes :: (Bits a, Integral a) => Int -> a -> [Word8]
wordBytes width w =
    [fromIntegral (w `shiftR` (8 * i)) | i <- [width - 1, width - 2 .. 0]]

packWords :: (Bits a, Integral a) => Int -> [a] -> BS.ByteString
packWords width = BS.pack . concatMap (wordBytes width)

word16Samples :: [Word16]
word16Samples = [0x0102, 0x0304, 0xfffe]

word32Samples :: [Word32]
word32Samples = [0x01020304, 0x05060708, 0xdeadbeef]

word64Samples :: [Word64]
word64Samples = [0x0102030405060708, 0x1112131415161718, 0xdeadbeefcafed00d]

-- | Boundary values for the signed element types.
int8Samples :: [Int8]
int8Samples = [minBound, -1, 0, 1, maxBound]

int16Samples :: [Int16]
int16Samples = [minBound, -1, 0, 1, maxBound]

int32Samples :: [Int32]
int32Samples = [minBound, -1, 0, 1, maxBound]

int64Samples :: [Int64]
int64Samples = [minBound, -1, 0, 1, maxBound]

-- | Big-endian two's-complement encoding of the signed sample lists.
int8SampleBytes :: BS.ByteString
int8SampleBytes = BS.pack [0x80, 0xff, 0x00, 0x01, 0x7f]

int16SampleBytes :: BS.ByteString
int16SampleBytes =
    BS.pack [0x80, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x7f, 0xff]

int32SampleBytes :: BS.ByteString
int32SampleBytes =
    BS.pack
        [ 0x80
        , 0x00
        , 0x00
        , 0x00
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x01
        , 0x7f
        , 0xff
        , 0xff
        , 0xff
        ]

int64SampleBytes :: BS.ByteString
int64SampleBytes =
    BS.pack
        [ 0x80
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x00
        , 0x01
        , 0x7f
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        , 0xff
        ]

word128Sample :: Word128
word128Sample =
    (fromIntegral (0x0102030405060708 :: Word64) `shiftL` 64)
        + fromIntegral (0x1112131415161718 :: Word64)

word256Sample :: Word256
word256Sample =
    (fromIntegral (0x2122232425262728 :: Word64) `shiftL` 192)
        + (fromIntegral (0x3132333435363738 :: Word64) `shiftL` 128)
        + (fromIntegral (0x4142434445464748 :: Word64) `shiftL` 64)
        + fromIntegral (0x5152535455565758 :: Word64)

-- | Positive value with distinct limbs.
int128Sample :: Int128
int128Sample = Int128 0x0102030405060708 0x1112131415161718

-- | Negative value with distinct limbs, exercising sign extension.
int128Negative :: Int128
int128Negative = Int128 0xffffffffffffff80 0x0102030405060708

-- | Positive value with distinct limbs.
int256Sample :: Int256
int256Sample =
    Int256
        0x2122232425262728
        0x3132333435363738
        0x4142434445464748
        0x5152535455565758

-- | Negative value with distinct limbs, exercising sign extension.
int256Negative :: Int256
int256Negative =
    Int256
        0xffffffffffffffff
        0x2122232425262728
        0x3132333435363738
        0x4142434445464748

-- | The big-endian two's-complement bytes of @-1@, written out by hand.
int128MinusOneBytes :: BS.ByteString
int128MinusOneBytes = BS.pack (replicate 16 0xff)

int256MinusOneBytes :: BS.ByteString
int256MinusOneBytes = BS.pack (replicate 32 0xff)

-- | One byte per value, @0x00@ for 'False' and @0x01@ for 'True'.
boolSamples :: [Bool]
boolSamples = [False, True]

boolSampleBytes :: BS.ByteString
boolSampleBytes = BS.pack [0x00, 0x01]

{- | Bit patterns are written out rather than derived, so the test pins the
big-endian layout and the sign of zero. NaN is left out because its payload is
target-dependent; it is checked separately.
-}
floatSamples :: [Float]
floatSamples = [0.0, -0.0, 1.0, -1.0, 1 / 0, -(1 / 0), encodeFloat 1 (-149)]

floatSampleBytes :: BS.ByteString
floatSampleBytes =
    BS.concat
        [ BS.pack [0x00, 0x00, 0x00, 0x00] -- 0.0
        , BS.pack [0x80, 0x00, 0x00, 0x00] -- -0.0
        , BS.pack [0x3f, 0x80, 0x00, 0x00] -- 1.0
        , BS.pack [0xbf, 0x80, 0x00, 0x00] -- -1.0
        , BS.pack [0x7f, 0x80, 0x00, 0x00] -- +Inf
        , BS.pack [0xff, 0x80, 0x00, 0x00] -- -Inf
        , BS.pack [0x00, 0x00, 0x00, 0x01] -- smallest positive subnormal
        ]

doubleSamples :: [Double]
doubleSamples = [0.0, -0.0, 1.0, -1.0, 1 / 0, -(1 / 0), encodeFloat 1 (-1074)]

doubleSampleBytes :: BS.ByteString
doubleSampleBytes =
    BS.concat
        [ BS.pack [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -- 0.0
        , BS.pack [0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -- -0.0
        , BS.pack [0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -- 1.0
        , BS.pack [0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -- -1.0
        , BS.pack [0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -- +Inf
        , BS.pack [0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -- -Inf
        , BS.pack [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] -- smallest positive subnormal
        ]

tests :: TestTree
tests =
    testGroup
        "Tests"
        [ goldenSamples
        , inputEncoding
        , encodingCollisions
        , Test.Data.ByteFoldable.tests
        ]

{- | Hashes must match the reference vectors in 'Test.Data.SipHash',
'Test.Data.HalfSipHash', and 'Test.Data.FNV'.
-}
goldenSamples :: TestTree
goldenSamples =
    testGroup
        "Golden Samples"
        [ testGroup
            "FNV"
            [ testCase "FNV1  Word32 String matches UTF-8 bytes" $ fmap (fnv1 @Word32) (Test.Data.FNV.inputs @String) @?= fmap (fnv1 @Word32 . utf8) (Test.Data.FNV.inputs @String)
            , testCase "FNV1a Word32 String matches UTF-8 bytes" $ fmap (fnv1a @Word32) (Test.Data.FNV.inputs @String) @?= fmap (fnv1a @Word32 . utf8) (Test.Data.FNV.inputs @String)
            , testCase "FNV1  Word64 String matches UTF-8 bytes" $ fmap (fnv1 @Word64) (Test.Data.FNV.inputs @String) @?= fmap (fnv1 @Word64 . utf8) (Test.Data.FNV.inputs @String)
            , testCase "FNV1a Word64 String matches UTF-8 bytes" $ fmap (fnv1a @Word64) (Test.Data.FNV.inputs @String) @?= fmap (fnv1a @Word64 . utf8) (Test.Data.FNV.inputs @String)
            , testCase "FNV1  Word32 Text matches UTF-8 bytes" $ fmap (fnv1 @Word32) (Test.Data.FNV.inputs @Text) @?= fmap (fnv1 @Word32 . TE.encodeUtf8) (Test.Data.FNV.inputs @Text)
            , testCase "FNV1a Word32 Text matches UTF-8 bytes" $ fmap (fnv1a @Word32) (Test.Data.FNV.inputs @Text) @?= fmap (fnv1a @Word32 . TE.encodeUtf8) (Test.Data.FNV.inputs @Text)
            , testCase "FNV1  Word64 Text matches UTF-8 bytes" $ fmap (fnv1 @Word64) (Test.Data.FNV.inputs @Text) @?= fmap (fnv1 @Word64 . TE.encodeUtf8) (Test.Data.FNV.inputs @Text)
            , testCase "FNV1a Word64 Text matches UTF-8 bytes" $ fmap (fnv1a @Word64) (Test.Data.FNV.inputs @Text) @?= fmap (fnv1a @Word64 . TE.encodeUtf8) (Test.Data.FNV.inputs @Text)
            , testCase "FNV1a Word32 non-ASCII String" $ fmap (fnv1a @Word32) Test.Data.FNV.utf8Inputs @?= Test.Data.FNV.fnv1aVectorsForWord32Utf8
            , testCase "FNV1a Word32 non-ASCII Text" $ fmap ((fnv1a @Word32) . T.pack) Test.Data.FNV.utf8Inputs @?= Test.Data.FNV.fnv1aVectorsForWord32Utf8
            , testCase "FNV1a Word64 non-ASCII String" $ fmap (fnv1a @Word64) Test.Data.FNV.utf8Inputs @?= Test.Data.FNV.fnv1aVectorsForWord64Utf8
            , testCase "FNV1a Word64 non-ASCII Text" $ fmap ((fnv1a @Word64) . T.pack) Test.Data.FNV.utf8Inputs @?= Test.Data.FNV.fnv1aVectorsForWord64Utf8
            , testCase "FNV1  Word32 ByteString" $ fmap (fnv1 @Word32) (Test.Data.FNV.inputs @BS.ByteString) @?= Test.Data.FNV.fnv1VectorsForWord32
            , testCase "FNV1a Word32 ByteString" $ fmap (fnv1a @Word32) (Test.Data.FNV.inputs @BS.ByteString) @?= Test.Data.FNV.fnv1aVectorsForWord32
            , testCase "FNV1  Word64 ByteString" $ fmap (fnv1 @Word64) (Test.Data.FNV.inputs @BS.ByteString) @?= Test.Data.FNV.fnv1VectorsForWord64
            , testCase "FNV1a Word64 ByteString" $ fmap (fnv1a @Word64) (Test.Data.FNV.inputs @BS.ByteString) @?= Test.Data.FNV.fnv1aVectorsForWord64
            , testCase "FNV1  Word32 Lazy ByteString" $ fmap (fnv1 @Word32) (Test.Data.FNV.inputs @BSL.ByteString) @?= Test.Data.FNV.fnv1VectorsForWord32
            , testCase "FNV1a Word32 Lazy ByteString" $ fmap (fnv1a @Word32) (Test.Data.FNV.inputs @BSL.ByteString) @?= Test.Data.FNV.fnv1aVectorsForWord32
            , testCase "FNV1  Word64 Lazy ByteString" $ fmap (fnv1 @Word64) (Test.Data.FNV.inputs @BSL.ByteString) @?= Test.Data.FNV.fnv1VectorsForWord64
            , testCase "FNV1a Word64 Lazy ByteString" $ fmap (fnv1a @Word64) (Test.Data.FNV.inputs @BSL.ByteString) @?= Test.Data.FNV.fnv1aVectorsForWord64
            , testCase "FNV1  Word32 ByteString Char8" $ fmap (fnv1 @Word32) (Test.Data.FNV.inputs @BSC.ByteString) @?= Test.Data.FNV.fnv1VectorsForWord32
            , testCase "FNV1a Word32 ByteString Char8" $ fmap (fnv1a @Word32) (Test.Data.FNV.inputs @BSC.ByteString) @?= Test.Data.FNV.fnv1aVectorsForWord32
            , testCase "FNV1  Word64 ByteString Char8" $ fmap (fnv1 @Word64) (Test.Data.FNV.inputs @BSC.ByteString) @?= Test.Data.FNV.fnv1VectorsForWord64
            , testCase "FNV1a Word64 ByteString Char8" $ fmap (fnv1a @Word64) (Test.Data.FNV.inputs @BSC.ByteString) @?= Test.Data.FNV.fnv1aVectorsForWord64
            , testCase "FNV1  Word32 Lazy ByteString Char8" $ fmap (fnv1 @Word32) (Test.Data.FNV.inputs @BSLC.ByteString) @?= Test.Data.FNV.fnv1VectorsForWord32
            , testCase "FNV1a Word32 Lazy ByteString Char8" $ fmap (fnv1a @Word32) (Test.Data.FNV.inputs @BSLC.ByteString) @?= Test.Data.FNV.fnv1aVectorsForWord32
            , testCase "FNV1  Word64 Lazy ByteString Char8" $ fmap (fnv1 @Word64) (Test.Data.FNV.inputs @BSLC.ByteString) @?= Test.Data.FNV.fnv1VectorsForWord64
            , testCase "FNV1a Word64 Lazy ByteString Char8" $ fmap (fnv1a @Word64) (Test.Data.FNV.inputs @BSLC.ByteString) @?= Test.Data.FNV.fnv1aVectorsForWord64
            ]
        , testGroup
            "SipHash"
            [ testCase "SipHash-2-4 Word64 String" $ fmap (sipHash @Word64 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @String) @?= Test.Data.SipHash.sipHash24VectorsForWord64
            , testCase "SipHash-2-4 Word64 Text" $ fmap (sipHash @Word64 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @Text) @?= Test.Data.SipHash.sipHash24VectorsForWord64
            , testCase "SipHash-2-4 Word64 ByteString" $ fmap (sipHash @Word64 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @BS.ByteString) @?= Test.Data.SipHash.sipHash24VectorsForWord64
            , testCase "SipHash-2-4 Word64 Lazy ByteString" $ fmap (sipHash @Word64 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @BSL.ByteString) @?= Test.Data.SipHash.sipHash24VectorsForWord64
            , testCase "SipHash-2-4 Word64 ByteString Char8" $ fmap (sipHash @Word64 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @BSC.ByteString) @?= Test.Data.SipHash.sipHash24VectorsForWord64
            , testCase "SipHash-2-4 Word64 Lazy ByteString Char8" $ fmap (sipHash @Word64 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @BSLC.ByteString) @?= Test.Data.SipHash.sipHash24VectorsForWord64
            , testCase "SipHash-2-4 Word128 String" $ fmap (sipHash @Word128 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @String) @?= Test.Data.SipHash.sipHash24VectorsForWord128
            , testCase "SipHash-2-4 Word128 Text" $ fmap (sipHash @Word128 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @Text) @?= Test.Data.SipHash.sipHash24VectorsForWord128
            , testCase "SipHash-2-4 Word128 ByteString" $ fmap (sipHash @Word128 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @BS.ByteString) @?= Test.Data.SipHash.sipHash24VectorsForWord128
            , testCase "SipHash-2-4 Word128 Lazy ByteString" $ fmap (sipHash @Word128 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @BSL.ByteString) @?= Test.Data.SipHash.sipHash24VectorsForWord128
            , testCase "SipHash-2-4 Word128 ByteString Char8" $ fmap (sipHash @Word128 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @BSC.ByteString) @?= Test.Data.SipHash.sipHash24VectorsForWord128
            , testCase "SipHash-2-4 Word128 Lazy ByteString Char8" $ fmap (sipHash @Word128 2 4 Test.Data.SipHash.key) (Test.Data.SipHash.inputs @BSLC.ByteString) @?= Test.Data.SipHash.sipHash24VectorsForWord128
            , testCase "SipHash-2-4 Word64 boundary lengths" $ fmap (sipHash @Word64 2 4 Test.Data.SipHash.key) Test.Data.SipHash.boundaryInputs @?= Test.Data.SipHash.boundaryVectorsForWord64
            ]
        , testGroup
            "HalfSipHash"
            [ testCase "HalfSipHash-2-4 Word32 String" $ fmap (halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @String) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord32
            , testCase "HalfSipHash-2-4 Word32 Text" $ fmap (halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @Text) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord32
            , testCase "HalfSipHash-2-4 Word32 ByteString" $ fmap (halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @BS.ByteString) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord32
            , testCase "HalfSipHash-2-4 Word32 Lazy ByteString" $ fmap (halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @BSL.ByteString) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord32
            , testCase "HalfSipHash-2-4 Word32 ByteString Char8" $ fmap (halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @BSC.ByteString) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord32
            , testCase "HalfSipHash-2-4 Word32 Lazy ByteString Char8" $ fmap (halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @BSLC.ByteString) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord32
            , testCase "HalfSipHash-2-4 Word64 String" $ fmap (halfSipHash @Word64 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @String) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord64
            , testCase "HalfSipHash-2-4 Word64 Text" $ fmap (halfSipHash @Word64 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @Text) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord64
            , testCase "HalfSipHash-2-4 Word64 ByteString" $ fmap (halfSipHash @Word64 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @BS.ByteString) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord64
            , testCase "HalfSipHash-2-4 Word64 Lazy ByteString" $ fmap (halfSipHash @Word64 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @BSL.ByteString) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord64
            , testCase "HalfSipHash-2-4 Word64 ByteString Char8" $ fmap (halfSipHash @Word64 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @BSC.ByteString) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord64
            , testCase "HalfSipHash-2-4 Word64 Lazy ByteString Char8" $ fmap (halfSipHash @Word64 2 4 Test.Data.HalfSipHash.key) (Test.Data.HalfSipHash.inputs @BSLC.ByteString) @?= Test.Data.HalfSipHash.halfSipHash24VectorsForWord64
            , testCase "HalfSipHash-2-4 Word32 boundary lengths" $ fmap (halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key) Test.Data.HalfSipHash.boundaryInputs @?= Test.Data.HalfSipHash.boundaryVectorsForWord32
            ]
        ]

{- | Every element type must feed the hash the same bytes its explicit
byte encoding does, so typed inputs and byte inputs agree.
-}
inputEncoding :: TestTree
inputEncoding =
    testGroup
        "Input Encoding"
        [ testGroup
            "FNV"
            [ testCase "FNV1a Word64 Char encoding matches bytes" $ do
                let bytes = BS.pack (concatMap charBytes charEncodingInput)
                bytes @?= TE.encodeUtf8 (T.pack charEncodingInput)
                fnv1a @Word64 charEncodingInput @?= fnv1a @Word64 bytes
                fnv1a @Word64 (T.pack charEncodingInput) @?= fnv1a @Word64 bytes
                fnv1a @Word64 "\xD800" @?= fnv1a @Word64 (BS.pack [0xed, 0xa0, 0x80])
            , testCase "FNV1a Word64 word elements match bytes" $ do
                fnv1a @Word64 [word128Sample] @?= fnv1a @Word64 (BS.pack (wordBytes 16 word128Sample))
                fnv1a @Word64 [word256Sample] @?= fnv1a @Word64 (BS.pack (wordBytes 32 word256Sample))
            , testCase "FNV1a Word64 wide signed elements match two's complement bytes" $ do
                fnv1a @Word64 [int128Sample] @?= fnv1a @Word64 (BS.pack (wordBytes 16 int128Sample))
                fnv1a @Word64 [int128Negative] @?= fnv1a @Word64 (BS.pack (wordBytes 16 int128Negative))
                fnv1a @Word64 [int256Sample] @?= fnv1a @Word64 (BS.pack (wordBytes 32 int256Sample))
                fnv1a @Word64 [int256Negative] @?= fnv1a @Word64 (BS.pack (wordBytes 32 int256Negative))
                fnv1a @Word64 [-1 :: Int128] @?= fnv1a @Word64 int128MinusOneBytes
                fnv1a @Word64 [-1 :: Int256] @?= fnv1a @Word64 int256MinusOneBytes
            , testCase "FNV1a Word64 Bool elements match bytes" $
                fnv1a @Word64 boolSamples @?= fnv1a @Word64 boolSampleBytes
            , testCase "FNV1a Word64 Float elements match IEEE-754 bytes" $
                fnv1a @Word64 floatSamples @?= fnv1a @Word64 floatSampleBytes
            , testCase "FNV1a Word64 Double elements match IEEE-754 bytes" $
                fnv1a @Word64 doubleSamples @?= fnv1a @Word64 doubleSampleBytes
            , testCase "FNV1a Word64 float NaN bits are preserved" $ do
                let nanF = 0 / 0 :: Float
                    nanD = 0 / 0 :: Double
                fnv1a @Word64 [nanF] @?= fnv1a @Word64 (BS.pack (wordBytes 4 (castFloatToWord32 nanF)))
                fnv1a @Word64 [nanD] @?= fnv1a @Word64 (BS.pack (wordBytes 8 (castDoubleToWord64 nanD)))
            , testCase "FNV1 Word64 small word elements match bytes" $ do
                fnv1 @Word64 word16Samples @?= fnv1 @Word64 (packWords 2 word16Samples)
                fnv1 @Word64 word32Samples @?= fnv1 @Word64 (packWords 4 word32Samples)
                fnv1 @Word64 word64Samples @?= fnv1 @Word64 (packWords 8 word64Samples)
            , testCase "FNV1a Word64 small word elements match bytes" $ do
                fnv1a @Word64 word16Samples @?= fnv1a @Word64 (packWords 2 word16Samples)
                fnv1a @Word64 word32Samples @?= fnv1a @Word64 (packWords 4 word32Samples)
                fnv1a @Word64 word64Samples @?= fnv1a @Word64 (packWords 8 word64Samples)
            , testCase "FNV1a Word64 signed elements match two's complement bytes" $ do
                fnv1a @Word64 int8Samples @?= fnv1a @Word64 int8SampleBytes
                fnv1a @Word64 int16Samples @?= fnv1a @Word64 int16SampleBytes
                fnv1a @Word64 int32Samples @?= fnv1a @Word64 int32SampleBytes
                fnv1a @Word64 int64Samples @?= fnv1a @Word64 int64SampleBytes
            ]
        , testGroup
            "SipHash"
            [ testCase "SipHash-2-4 Word64 Char encoding matches bytes" $ do
                let bytes = BS.pack (concatMap charBytes charEncodingInput)
                bytes @?= TE.encodeUtf8 (T.pack charEncodingInput)
                sipHash @Word64 2 4 Test.Data.SipHash.key charEncodingInput @?= sipHash @Word64 2 4 Test.Data.SipHash.key bytes
                sipHash @Word64 2 4 Test.Data.SipHash.key (T.pack charEncodingInput) @?= sipHash @Word64 2 4 Test.Data.SipHash.key bytes
            , testCase "SipHash-2-4 Word64 word elements match bytes" $ do
                sipHash @Word64 2 4 Test.Data.SipHash.key [word128Sample] @?= sipHash @Word64 2 4 Test.Data.SipHash.key (BS.pack (wordBytes 16 word128Sample))
                sipHash @Word64 2 4 Test.Data.SipHash.key [word256Sample] @?= sipHash @Word64 2 4 Test.Data.SipHash.key (BS.pack (wordBytes 32 word256Sample))
            , testCase "SipHash-2-4 Word64 wide signed elements match two's complement bytes" $ do
                sipHash @Word64 2 4 Test.Data.SipHash.key [int128Sample] @?= sipHash @Word64 2 4 Test.Data.SipHash.key (BS.pack (wordBytes 16 int128Sample))
                sipHash @Word64 2 4 Test.Data.SipHash.key [int128Negative] @?= sipHash @Word64 2 4 Test.Data.SipHash.key (BS.pack (wordBytes 16 int128Negative))
                sipHash @Word64 2 4 Test.Data.SipHash.key [int256Sample] @?= sipHash @Word64 2 4 Test.Data.SipHash.key (BS.pack (wordBytes 32 int256Sample))
                sipHash @Word64 2 4 Test.Data.SipHash.key [int256Negative] @?= sipHash @Word64 2 4 Test.Data.SipHash.key (BS.pack (wordBytes 32 int256Negative))
                sipHash @Word64 2 4 Test.Data.SipHash.key [-1 :: Int128] @?= sipHash @Word64 2 4 Test.Data.SipHash.key int128MinusOneBytes
                sipHash @Word64 2 4 Test.Data.SipHash.key [-1 :: Int256] @?= sipHash @Word64 2 4 Test.Data.SipHash.key int256MinusOneBytes
            , testCase "SipHash-2-4 Word64 Bool elements match bytes" $
                sipHash @Word64 2 4 Test.Data.SipHash.key boolSamples @?= sipHash @Word64 2 4 Test.Data.SipHash.key boolSampleBytes
            , testCase "SipHash-2-4 Word64 Float elements match IEEE-754 bytes" $
                sipHash @Word64 2 4 Test.Data.SipHash.key floatSamples @?= sipHash @Word64 2 4 Test.Data.SipHash.key floatSampleBytes
            , testCase "SipHash-2-4 Word64 Double elements match IEEE-754 bytes" $
                sipHash @Word64 2 4 Test.Data.SipHash.key doubleSamples @?= sipHash @Word64 2 4 Test.Data.SipHash.key doubleSampleBytes
            , testCase "SipHash-2-4 Word64 small word elements match bytes" $ do
                sipHash @Word64 2 4 Test.Data.SipHash.key word16Samples @?= sipHash @Word64 2 4 Test.Data.SipHash.key (packWords 2 word16Samples)
                sipHash @Word64 2 4 Test.Data.SipHash.key word32Samples @?= sipHash @Word64 2 4 Test.Data.SipHash.key (packWords 4 word32Samples)
                sipHash @Word64 2 4 Test.Data.SipHash.key word64Samples @?= sipHash @Word64 2 4 Test.Data.SipHash.key (packWords 8 word64Samples)
            , testCase "SipHash-2-4 Word64 signed elements match two's complement bytes" $ do
                sipHash @Word64 2 4 Test.Data.SipHash.key int8Samples @?= sipHash @Word64 2 4 Test.Data.SipHash.key int8SampleBytes
                sipHash @Word64 2 4 Test.Data.SipHash.key int16Samples @?= sipHash @Word64 2 4 Test.Data.SipHash.key int16SampleBytes
                sipHash @Word64 2 4 Test.Data.SipHash.key int32Samples @?= sipHash @Word64 2 4 Test.Data.SipHash.key int32SampleBytes
                sipHash @Word64 2 4 Test.Data.SipHash.key int64Samples @?= sipHash @Word64 2 4 Test.Data.SipHash.key int64SampleBytes
            ]
        , testGroup
            "HalfSipHash"
            [ testCase "HalfSipHash-2-4 Word32 Char encoding matches bytes" $ do
                let bytes = BS.pack (concatMap charBytes charEncodingInput)
                bytes @?= TE.encodeUtf8 (T.pack charEncodingInput)
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key charEncodingInput @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key bytes
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (T.pack charEncodingInput) @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key bytes
            , testCase "HalfSipHash-2-4 Word32 word elements match bytes" $ do
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key [word128Sample] @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (BS.pack (wordBytes 16 word128Sample))
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key [word256Sample] @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (BS.pack (wordBytes 32 word256Sample))
            , testCase "HalfSipHash-2-4 Word32 wide signed elements match two's complement bytes" $ do
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key [int128Sample] @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (BS.pack (wordBytes 16 int128Sample))
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key [int128Negative] @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (BS.pack (wordBytes 16 int128Negative))
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key [int256Sample] @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (BS.pack (wordBytes 32 int256Sample))
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key [int256Negative] @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (BS.pack (wordBytes 32 int256Negative))
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key [-1 :: Int128] @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int128MinusOneBytes
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key [-1 :: Int256] @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int256MinusOneBytes
            , testCase "HalfSipHash-2-4 Word32 Bool elements match bytes" $
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key boolSamples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key boolSampleBytes
            , testCase "HalfSipHash-2-4 Word32 Float elements match IEEE-754 bytes" $
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key floatSamples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key floatSampleBytes
            , testCase "HalfSipHash-2-4 Word32 Double elements match IEEE-754 bytes" $
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key doubleSamples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key doubleSampleBytes
            , testCase "HalfSipHash-2-4 Word32 small word elements match bytes" $ do
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key word16Samples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (packWords 2 word16Samples)
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key word32Samples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (packWords 4 word32Samples)
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key word64Samples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key (packWords 8 word64Samples)
            , testCase "HalfSipHash-2-4 Word32 signed elements match two's complement bytes" $ do
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int8Samples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int8SampleBytes
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int16Samples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int16SampleBytes
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int32Samples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int32SampleBytes
                halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int64Samples @?= halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key int64SampleBytes
            ]
        ]

{- | Distinct texts that the old, non-prefix-free 'Char' encoding mapped to
the same bytes must hash differently.
-}
encodingCollisions :: TestTree
encodingCollisions =
    testGroup
        "Encoding Collisions"
        [ testGroup
            "FNV"
            [ testCase "FNV1a Word64 String" $ checkCollisions (fnv1a @Word64) id
            , testCase "FNV1a Word64 Text" $ checkCollisions (fnv1a @Word64) T.pack
            ]
        , testGroup
            "SipHash"
            [ testCase "SipHash-2-4 Word64 String" $ checkCollisions (sipHash @Word64 2 4 Test.Data.SipHash.key) id
            , testCase "SipHash-2-4 Word64 Text" $ checkCollisions (sipHash @Word64 2 4 Test.Data.SipHash.key) T.pack
            ]
        , testGroup
            "HalfSipHash"
            [ testCase "HalfSipHash-2-4 Word32 String" $ checkCollisions (halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key) id
            , testCase "HalfSipHash-2-4 Word32 Text" $ checkCollisions (halfSipHash @Word32 2 4 Test.Data.HalfSipHash.key) T.pack
            ]
        ]

checkCollisions :: (Eq a, Show a) => (t -> a) -> (String -> t) -> IO ()
checkCollisions h mk =
    mapM_
        (\(label, x, y) -> assertBool label (h (mk x) /= h (mk y)))
        collisionPairs

collisionPairs :: [(String, String, String)]
collisionPairs =
    [ ("U+0100 and U+0001 U+0000 must not collide", "\x0100", "\x01\x00")
    , ("U+1234 and U+0012 U+0034 must not collide", "\x1234", "\x12\x34")
    , ("U+1F600 and U+0001 U+00F6 U+0000 must not collide", "\x1F600", "\x01\xF6\x00")
    ]