packages feed

falsify-0.4.0: src/Test/Falsify/Internal/Driver/ReplaySeed.hs

-- | Replay seeds
--
-- We need a seed/gamma pair to initialize a splitmix PRNG. This is however a
-- pretty low level implementation detail that I'd prefer not be be directly
-- visible. We therefore provide a thin layer on top, which provides an
-- "encoded" replay seed. This has the additional benefits that the length of
-- the replay seed is always the same (unlike just writing a 'Word64'), and we
-- could in principle at some point support other kinds of PRNGs.
module Test.Falsify.Internal.Driver.ReplaySeed (
    ReplaySeed(..)
  , parseReplaySeed
  , splitmixReplaySeed
  ) where

import Data.String
import Data.Word
import Data.Binary
import System.Random.SplitMix

import qualified Data.ByteString.Base16.Lazy as Lazy.Base16
import qualified Data.ByteString.Lazy.Char8  as Lazy.Char8

-- | Replay seed
--
-- By default, when we falsify a property we start with a PRNG initialized using
-- a random seed (produced using the system entropy; this relies on
-- 'System.Random.SplitMix.initSMGen' in @splitmix@). When a property /fails/,
-- we will report the exact seed used, so that the user can re-run the exact
-- same test again, if desired.
--
-- The definition of t'ReplaySeed' is part of @falsify's@ public API, to make it
-- possible to use custom drivers.
data ReplaySeed = ReplaySeed{
      replaySeed  :: Word64 -- ^ Seed
    , replayGamma :: Word64 -- ^ Gamma (must be odd)
    }

splitmixReplaySeed :: SMGen -> ReplaySeed
splitmixReplaySeed = uncurry ReplaySeed . unseedSMGen

instance Binary ReplaySeed where
  put ReplaySeed{replaySeed, replayGamma} = do
      putWord8 1
      put replaySeed
      put replayGamma

  get = do
      tag <- getWord8
      case tag of
        1 -> do replaySeed  <- get
                replayGamma <- get
                if odd replayGamma
                  then return $ ReplaySeed{replaySeed, replayGamma}
                  else fail $ "ReplaySeed: expected odd gamma for splitmix"
        n -> fail $ "ReplaySeed: invalid tag: " ++ show n

instance Show ReplaySeed where
  show = Lazy.Char8.unpack . Lazy.Base16.encode . encode

instance IsString ReplaySeed where
  fromString = aux . parseReplaySeed
    where
      aux :: Either String ReplaySeed -> ReplaySeed
      aux (Left  err)  = error $ "ReplaySeed: invalid seed: " ++ err
      aux (Right seed) = seed

-- | Parse t'ReplaySeed'
--
-- Returns 'Left' an error message if parsing failed.
parseReplaySeed :: String -> Either String ReplaySeed
parseReplaySeed str = do
    raw <- case Lazy.Base16.decode (Lazy.Char8.pack str) of
             Left err -> Left err
             Right x  -> Right x
    case decodeOrFail raw of
      Left  (_, _, err) -> Left err
      Right (_, _, x)   -> return x