{-# LANGUAGE TypeOperators #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE BangPatterns #-} module Main ( main ) where import Prelude hiding (all) import Control.Eff import Control.Eff.Lift import Control.Eff.State.Strict import System.Random.Effect import Control.Applicative import Control.Monad.ST import Data.Vector ( Vector ) import qualified Data.Vector as V import qualified Data.Vector.Mutable as MV import Data.Word import Test.Framework ( defaultMain, Test ) import Test.Framework.Providers.QuickCheck2 import Test.QuickCheck import Test.QuickCheck.Property ( morallyDubiousIOProperty ) main :: IO () main = defaultMain tests runWithSeed :: Word64 -> Eff (State Random :> ()) a -> a runWithSeed seed = run . runRandomState (mkRandom seed) runIOWithSeed :: Word64 -> Eff (State Random :> Lift IO :> ()) a -> IO a runIOWithSeed seed = runLift . runRandomState (mkRandom seed) checkRange :: (Integer, Integer) -> Integer -> Bool checkRange (low, high) x = x >= low && x <= high testUniformRandom :: Integer -> Integer -> Word64 -> Bool testUniformRandom a b seed = let low = min a b high = max a b in checkRange (low, high) . runWithSeed seed $ do uniformIntDist a b newtype DiscreteWeights = DW [Word64] deriving Show instance Arbitrary DiscreteWeights where arbitrary = DW <$> suchThat (listOf arbitrary) ((> 0) . sum) shrink (DW xs) = map DW (shrink xs) testDiscreteDistributionInRange :: DiscreteWeights -> Word64 -> Bool testDiscreteDistributionInRange (DW xs) seed = let ddh = buildDDH xs minVal = 0 maxVal = length xs - 1 in (\x -> x >= minVal && x <= maxVal) . runWithSeed seed $ discreteDist ddh testNoZeroDiscreteDistributionPick :: DiscreteWeights -> Word64 -> Bool testNoZeroDiscreteDistributionPick (DW xs) seed = let ddh = buildDDH xs in (\x -> (xs !! x) /= 0) . runWithSeed seed $ discreteDist ddh testUnsafeThaw :: DiscreteWeights -> Word64 -> Bool testUnsafeThaw (DW xs) seed = let ddh = buildDDH xs in runWithSeed seed $ do _ <- discreteDist ddh _ <- discreteDist ddh _ <- discreteDist ddh _ <- discreteDist ddh _ <- discreteDist ddh return True testUniformIntegralDist :: Integer -> Integer -> Word64 -> Bool testUniformIntegralDist a b seed = let r1 = runWithSeed seed $ uniformIntDist a b r2 = runWithSeed seed $ uniformIntegralDist a b in r1 == r2 testKnuthShuffle :: [Int] -> Word64 -> Bool testKnuthShuffle xs' seed = let xs = V.fromList xs' countIf f = V.length . V.filter f shuffled = runWithSeed seed (knuthShuffle xs) sameCount v1 v2 = V.all id $ V.map (\x -> countIf (== x) v1 == countIf (== x) v2) v1 in sameCount xs shuffled testKnuthShuffleM :: [Int] -> Word64 -> IO Bool testKnuthShuffleM xs' seed = do let xs = V.fromList xs' countIf f = V.length . V.filter f shuffled = do vs <- V.thaw xs runIOWithSeed seed (knuthShuffleM vs) V.freeze vs sameCount v1 v2 = V.all id $ V.map (\x -> countIf (== x) v1 == countIf (== x) v2) v1 shuf <- shuffled return (sameCount xs shuf) testKnuthShuffleEquivalence :: [Int] -> Word64 -> IO Bool testKnuthShuffleEquivalence xs seed = do let vs = V.fromList xs ks1 = runWithSeed seed (knuthShuffle vs) xs' <- V.thaw vs runIOWithSeed seed (knuthShuffleM xs') ks2 <- V.freeze xs' return (ks1 == ks2) testSecureRandom :: Integer -> Integer -> IO Bool testSecureRandom a b = do let low = min a b high = max a b runLift $ do rng <- mkSecureRandomIO return $ run $ runRandomState rng $ checkRange (low, high) <$> uniformIntDist a b (|>) :: b -> (b -> c) -> c (|>) = flip ($) histogram :: Vector Integer -> Vector Int histogram v = runST $ do mv <- MV.replicate (fromIntegral (V.maximum v + 1)) 0 V.forM_ v $ \i' -> do let i = (fromIntegral i') :: Int !x <- MV.read mv i MV.write mv i (x+1) V.unsafeFreeze mv -- check if all uniformly distributed numbers are within 10% of the mean. -- 10% was a number chosen arbitrarily. simpleUniformIntDistTest :: Word64 -> Bool simpleUniformIntDistTest seed = let nBuckets = 5 :: Int samplesPerBucket = 4000 :: Int nSamples = nBuckets * samplesPerBucket maxDelta = (fromIntegral samplesPerBucket) `div` 10 nums = uniformIntDist 0 (fromIntegral (nBuckets - 1)) |> V.replicateM nSamples |> runWithSeed seed hist = histogram nums in V.all (\x -> x >= samplesPerBucket - maxDelta && x <= samplesPerBucket + maxDelta) hist tests :: [Test] tests = [ testProperty "random range" testUniformRandom , testProperty "discrete dist range" testDiscreteDistributionInRange , testProperty "no non-zero discrete dist pick" testNoZeroDiscreteDistributionPick , testProperty "unsafeThaw is okay to use" testUnsafeThaw , testProperty "testUniformIntegralDist == testUniformIntDist" testUniformIntegralDist , testProperty "knuth shuffle" testKnuthShuffle , testProperty "knuth shuffle (monadic)" (\xs seed -> morallyDubiousIOProperty $ testKnuthShuffleM xs seed) , testProperty "knuth shuffle equivalence" (\xs seed -> morallyDubiousIOProperty $ testKnuthShuffleEquivalence xs seed) , testProperty "secure random" (\a b -> morallyDubiousIOProperty $ testSecureRandom a b) , testProperty "uniformIntDist is uniform-ish" simpleUniformIntDistTest ]