packages feed

rapidhash-0.1.0.0: test/RapidHashTest.hs

module RapidHashTest where

import Control.Monad (forM_)
import Data.ByteString qualified as BS
import Data.ByteString.Short qualified as SBS
import Data.Primitive.PrimArray (primArrayFromList)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Vector.Primitive qualified as PV
import Data.Vector.Storable qualified as SV
import Data.Word (Word64, Word8)
import Hedgehog (Property, forAll, property, (===))
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Numeric (readHex)
import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, (@?=))

import Data.Hash.RapidHash (
  RapidHash (RapidHash),
  RapidSeed (RapidSeed),
  rapidhash,
  rapidhashWithSeed,
 )

unit_showIsTaggedPaddedHex :: Assertion
unit_showIsTaggedPaddedHex =
  show (RapidHash 0xabc) @?= "rhv3:0000000000000abc"

unit_readRejectsForeignTags :: Assertion
unit_readRejectsForeignTags = do
  reads @RapidHash "rhv4:0000000000000abc" @?= [] -- future
  reads @RapidHash "fnv1a64:0000000000000abc" @?= [] -- other hash
  reads @RapidHash "rhv3:abc" @?= [] -- invalid hash length

hprop_showReadRoundtrip :: Property
hprop_showReadRoundtrip = property do
  w <- forAll (Gen.word64 Range.linearBounded)
  let h = RapidHash w
  read (show h) === h

-- | The Text entry hashes the UTF-8 payload in place; it must agree with
-- hashing the encoded bytes (and the ShortByteString copy of them) — at
-- the default seed, at a random seed (pinning the seed plumbing of the
-- ByteArray# entry points), and for a Text with a nonzero internal offset.
hprop_representationsAgree :: Property
hprop_representationsAgree = property do
  t <- forAll (Gen.text (Range.linear 0 300) Gen.unicode)
  s <- forAll (Gen.word64 Range.linearBounded)
  let bytes = TE.encodeUtf8 t
      seed = RapidSeed s
  rapidhash t === rapidhash bytes
  rapidhash (SBS.toShort bytes) === rapidhash bytes
  rapidhash (BS.copy bytes) === rapidhash bytes
  rapidhashWithSeed seed t === rapidhashWithSeed seed bytes
  rapidhashWithSeed seed (SBS.toShort bytes) === rapidhashWithSeed seed bytes
  let t' = T.drop 1 t
  rapidhash t' === rapidhash (TE.encodeUtf8 t')

hprop_vectorInstancesAgreeWithBytes :: Property
hprop_vectorInstancesAgreeWithBytes = property do
  ws <- forAll (Gen.list (Range.linear 0 64) (Gen.word64 Range.linearBounded))
  k <- forAll (Gen.int (Range.linear 0 (length ws)))
  let sv = SV.fromList ws
      pv = PV.fromList ws
      pvSlice = PV.drop k pv
      pvBytes (PV.Vector off len ba) = PV.Vector (8 * off) (8 * len) ba :: PV.Vector Word8
  rapidhash sv === rapidhash (SV.unsafeCast sv :: SV.Vector Word8)
  rapidhash pv === rapidhash (pvBytes pv)
  rapidhash sv === rapidhash pv
  rapidhash (primArrayFromList ws) === rapidhash pv
  rapidhash pvSlice === rapidhash (pvBytes pvSlice)

-- | Known-answer vectors generated from the vendored upstream header by
-- @test/pin/pin.c@ (verify or regenerate with @scripts/rapidhash-pin.sh@
-- in the project repository).
unit_matchesUpstreamCReference :: Assertion
unit_matchesUpstreamCReference = do
  golden <- lines <$> readFile "test/pin/rapidhash-v3-pin.txt"
  assertBool "golden file contains vectors" (not (null golden))
  forM_ golden \line -> case words line of
    ["default", lenS, hashS] ->
      assertEqual line (RapidHash (hex hashS)) $
        rapidhash (bufBS (read lenS))
    ["withSeed", lenS, seedS, hashS] ->
      assertEqual line (RapidHash (hex hashS)) $
        rapidhashWithSeed (RapidSeed (hex seedS)) (bufBS (read lenS))
    ["offset", offS, lenS, seedS, hashS] -> do
      let off = read offS
          len = read lenS
          slice = PV.drop off (PV.fromList (bufBytes (off + len)) :: PV.Vector Word8)
      assertEqual line (RapidHash (hex hashS)) $
        rapidhashWithSeed (RapidSeed (hex seedS)) slice
    _ -> assertFailure ("unparseable golden line: " <> line)
 where
  hex :: String -> Word64
  hex s = case readHex s of
    [(v, "")] -> v
    _ -> error ("bad hex field in golden file: " <> s)

  -- Deterministic filler; mirrors byte_at in test/pin/pin.c.
  bufBytes :: Int -> [Word8]
  bufBytes n = [fromIntegral (i * 167 + 13) | i <- [0 .. n - 1]]

  bufBS :: Int -> BS.ByteString
  bufBS = BS.pack . bufBytes