{-# LANGUAGE CPP #-} #if !MIN_VERSION_base(4,8,0) import Data.Foldable (foldMap) #endif import Data.List (nub) import Data.Monoid (All(..)) import Data.Word (Word32, Word64) import Test.Tasty import Test.Tasty.Runners import Test.Tasty.Ingredients.Rerun import Test.Tasty.QuickCheck as QC import Test.Tasty.HUnit as HU hiding (assert) import Test.QuickCheck.Monadic import Database.Redis import Data.RedisBloom import Data.RedisBloom.Hash.FNV import Data.RedisBloom.Suggestions import Common main :: IO () main = defaultMainWithIngredients [ rerunningTests [ consoleTestReporter ] ] tests exec :: Redis a -> IO a exec x = do conn <- connect defaultConnectInfo runRedis conn x encode :: Num a => Bool -> a encode False = 0 encode True = 1 valBound :: Int valBound = 2 ^ (12 :: Int) capacityBounds :: Gen Int capacityBounds = choose (1, 2 ^ (16 :: Int)) elementGen :: Gen Int elementGen = choose (negate valBound, valBound :: Int) elementGenL :: Gen [Int] elementGenL = listOf1 elementGen elementGenLNubbed :: Monad m => PropertyM m [Int] elementGenLNubbed = fmap nub . pick $ elementGenL epsilonBounds :: Gen Double epsilonBounds = choose (1e-6, 0.5 :: Double) bfg :: Monad m => PropertyM m (Bloom Int, Double) bfg = do cap <- pick capacityBounds epsilon <- pick epsilonBounds return (suggestCreate cap epsilon test_key, epsilon) limit :: RealFrac a => Int -> a -> a limit n = let k = 10 ^ n in (/k) . fromIntegral . round . (*k) collectLabeled :: Monad m => String -> Double -> PropertyM m () collectLabeled name x = monitor . label $ name ++ show (limit 1 x) tests, treeFNV, treeFixed, treeVar, treeConsistency, treeEpsilonVar, treeEpsilon :: TestTree tests = testGroup "Tests" [treeFNV, treeConsistency, treeEpsilon] treeFNV = testGroup "Fowler-Noll-Vo hash function" [ testGroup "FNV primes" [ testCase "32 bits" $ 16777619 @=? (fnvPrime :: Word32), testCase "64 bits" $ 1099511628211 @=? (fnvPrime :: Word64) ]] treeConsistency = testGroup "consistency" [treeFixed, treeVar] treeEpsilon = testGroup "false positive rate" [treeEpsilonVar] treeFixed = testGroup "fixed configuration" [ QC.testProperty "single element" $ monadicIO $ do x <- pick elementGen b <- run . exec $ createAddQuery test_bf x assert b, QC.testProperty "multiple elements" $ monadicIO $ do lx <- elementGenLNubbed b <- run . exec $ fmap (getAll . foldMap All) (createAddQueryL test_bf lx) assert b ] treeVar = testGroup "variable configuration" [ QC.testProperty "single element" $ monadicIO $ do (blt, _) <- bfg x <- pick elementGen b <- run . exec $ createAddQuery blt x assert b, QC.testProperty "multiple elements" $ monadicIO $ do (blt, _) <- bfg len <- pick . choose $ (2^2, 2^8) lx <- pick $ vectorOf (min (fromIntegral $ capacity blt) len) elementGen let lx' = nub lx b <- run . exec $ fmap (getAll . foldMap All) (createAddQueryL blt lx') assert b ] treeEpsilonVar = testGroup "variable configuration" [ QC.testProperty "multiple elements" $ monadicIO $ do (blt, epsilon) <- bfg let (Capacity c) = capacity blt c' = fromIntegral c :: Double len <- pick . choose $ (4, min (2^8) c) notlen <- pick . choose $ (4, 2 * len) let vec = vectorOf len elementGen check l = not . flip elem l vecn l = vectorOf notlen . flip suchThat (check l) $ elementGen lx <- pick vec let lx' = nub lx lxnot <- pick $ vecn lx' let lxnot' = nub lxnot run . exec . createAddL blt $ lx' falsePositives <- run . exec . queryL blt $ lxnot' let notlen' = fromIntegral . length $ lxnot' fp = sum . fmap encode $ falsePositives ratio = fp / notlen' cl = c' / fromIntegral len monitor . counterexample $ "Length: " ++ show len monitor . counterexample $ "Capacity: " ++ show c monitor . counterexample $ "Capacity / Length: " ++ show cl monitor . counterexample $ "False Positives: " ++ show fp monitor . counterexample $ "Actual False-Positive Ratio (α): " ++ show ratio monitor . counterexample $ "Expected False-Positive Ratio (ɛ): " ++ show epsilon collectLabeled "α = " ratio collectLabeled "α ∕ ɛ = " $ ratio / epsilon assert $ ratio <= epsilon ]