packages feed

foundation-0.0.2: tests/Test/Foundation/Random.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Test.Foundation.Random
    ( testRandom
    ) where

import Imports
import Foundation
import Foundation.Primitive
import Foundation.Array
import Foundation.Collection
import Foundation.System.Entropy
import Foundation.Random
import Control.Monad
import qualified Prelude
import qualified Data.List
import GHC.ST

testRandom :: TestTree
testRandom = testGroup "random"
    [ testProperty "entropy" entropyCheck
    , testProperty "rngv1" rngv1Check
    ]
  where
    entropyCheck = monadicIO $ do
        v <- randomTest <$> run (getEntropy 1024)
        --run (putStrLn . fromList $ show v)

        unless (res_entropy v > 6.5 && res_entropy v <= 8) (failInfo v)
        unless (res_mean v >= 112 && res_mean v <= 144) (failInfo v)
        unless (res_compressionPercent v >= 0 && res_compressionPercent v <= 5.0) (failInfo v)

    rngv1Check = monadicIO $ do
        rng         <- run (randomNew :: IO RNG)
        --nbQueries  <- pick arbitrary
        let (l, _) = withRandomGenerator rng $ do
                mapM getRandomBytes [1,2,4,8,32,80,250,2139]
            v = randomTest (mconcat l)
        unless (res_entropy v > 6.5 && res_entropy v <= 8) (failInfo v)
        unless (res_mean v >= 112 && res_mean v <= 144) (failInfo v)
        unless (res_compressionPercent v >= 0 && res_compressionPercent v <= 5.0) (failInfo v)
        return ()

    failInfo v = do
        fail $ toList
             ("randomness assert failed: entropy=" <> show (res_entropy v)
                                      <> " chi^2=" <> show (res_chi_square v)
                                       <> " mean=" <> show (res_mean v)
                               <> " compression%=" <> show (res_compressionPercent v))

-------- generic random testing

data RandomTestResult = RandomTestResult
    { res_totalChars         :: Word64 -- ^ Total number of characters
    , res_entropy            :: Double -- ^ Entropy per byte
    , res_chi_square         :: Double -- ^ Chi Square
    , res_mean               :: Double -- ^ Arithmetic Mean
    , res_compressionPercent :: Double -- ^ Theorical Compression percent
    , res_probs              :: [Double] -- ^ Probability of every bucket
    } deriving (Show,Eq)

-- | Mutable random test State
newtype RandomTestState s = RandomTestState (MUArray Word64 (PrimState (ST s)))

-- | Initialize new state to run tests
randomTestInitialize :: ST s (RandomTestState s)
randomTestInitialize = do
    m <- mutNew 256
    forM_ [0..255] $ \i -> mutWrite m i 0
    return $ RandomTestState m

-- | Append random data to the test state
randomTestAppend :: RandomTestState s -> UArray Word8 -> ST s ()
randomTestAppend (RandomTestState buckets) = mapM_ (addVec 1 . fromIntegral) . toList
  where
    addVec a i = mutRead buckets i >>= \d -> mutWrite buckets i $! d+a

-- | Finalize random test state into some result
randomTestFinalize :: RandomTestState s -> ST s RandomTestResult
randomTestFinalize (RandomTestState buckets) = (calculate . toList) <$> freeze buckets

randomTest :: UArray Word8 -> RandomTestResult
randomTest a = runST $ do
    st <- randomTestInitialize
    randomTestAppend st a
    randomTestFinalize st

calculate :: [Word64] -> RandomTestResult
calculate buckets = RandomTestResult
    { res_totalChars = totalChars
    , res_entropy    = entropy
    , res_chi_square = chisq
    , res_mean       = fromIntegral datasum Prelude./ fromIntegral totalChars
    , res_compressionPercent = 100.0 * (8 - entropy) Prelude./ 8.0
    , res_probs      = probs
    }
  where totalChars = Prelude.sum buckets
        probs = fmap (\v -> fromIntegral v Prelude./ fromIntegral totalChars :: Double) buckets
        entropy = Data.List.foldl' accEnt 0.0 probs
        cexp    = fromIntegral totalChars Prelude./ 256.0 :: Double
        (datasum, chisq) = foldl' accMeanChi (0, 0.0) $ Prelude.zip [0..255] buckets
        --chip' = abs (sqrt (2.0 * chisq) - sqrt (2.0 * 255.0 - 1.0))

        accEnt ent pr
            | pr > 0.0  = ent + (pr * xlog (1 Prelude./ pr))
            | otherwise = ent
        xlog v = Prelude.logBase 10 v * (Prelude.log 10 Prelude./ Prelude.log 2)

        accMeanChi :: (Word64, Double) -> (Int, Word64) -> (Word64, Double)
        accMeanChi (dataSum, chiSq) (i, ccount) =
            let a      = fromIntegral ccount - cexp
             in (dataSum + fromIntegral i * ccount, chiSq + (a * a Prelude./ cexp))