riak-1.2.0.0: tests/CRDTProperties.hs
-- | module: CRDTProperties
-- copyright: (c) 2016 Sentenai
-- author: Antonio Nikishaev <me@lelf.lu>
-- license: Apache
--
{-# LANGUAGE OverloadedStrings, FlexibleContexts, TupleSections, ScopedTypeVariables,
GADTs, StandaloneDeriving, UndecidableInstances, PatternGuards, MultiParamTypeClasses, CPP #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module CRDTProperties (prop_counters,
prop_sets,
prop_maps,
tests) where
-- |
-- The idea: send arbitrary stream of commands to riak, collect each
-- command output to list :: [Maybe RiakReturnValue]. Then see it we
-- get the same list of results simulating Riak in this module.
#if __GLASGOW_HASKELL__ <= 708
import Control.Applicative
#endif
import Control.Monad.RWS
import Control.Monad.Fail
import Data.ByteString (ByteString)
import Data.Default.Class
import Data.List.NonEmpty
import qualified Data.Map as Map
import Data.Maybe
import Data.Proxy
import qualified Data.Set as Set
import Test.QuickCheck
import Test.QuickCheck.Monadic
import Utils
import Test.Tasty
import Test.Tasty.QuickCheck
import qualified Network.Riak.Basic as B
import qualified Network.Riak.CRDT as C
newtype BucketType = BucketType ByteString deriving (Show,Eq,Ord)
newtype Bucket = Bucket ByteString deriving (Show,Eq,Ord)
newtype Key = Key ByteString deriving (Show,Eq,Ord)
newtype Value = Value ByteString deriving Show
class Values a where values :: [a]
-- Not many. We want to hit each multiple times
instance Values BucketType where values = BucketType <$> ["sets","counters","maps"]
instance Values Bucket where values = Bucket <$> ["A","B"]
instance Values Key where values = Key <$> ["a","b","c"]
instance Values Value where values = Value <$> ["1","2","3","4"]
instance Arbitrary BucketType where arbitrary = elements values
instance Arbitrary Bucket where arbitrary = elements values
instance Arbitrary Key where arbitrary = elements values
instance Arbitrary Value where arbitrary = elements values
data Point = Point BucketType Bucket Key deriving (Show,Ord,Eq)
instance Values Point where
values = [ Point t b k | t <- values, b <- values, k <- values ]
instance Arbitrary Point where arbitrary = elements values
type RiakState = Map.Map Point C.DataType
-- | observe all current values we care about (instance 'Values') in
-- riak, gather them into a map.
--
-- As it turns out, observeRiak is not quite cheap operation after
-- /types/maps/… are populated. So first argument is proxy for the
-- (only) type we are interested in.
observeRiak :: Action a op => Proxy a -> IO RiakState
observeRiak p = Map.fromList . catMaybes <$> observeRiak' (BucketType $ bucketType p)
observeRiak' :: BucketType -> IO [Maybe (Point, C.DataType)]
observeRiak' bt@(BucketType t_) = withGlobalConn $ \c ->
sequence [ do r <- C.get c t_ b_ k_
pure . fmap (p,) $ r
| b <- values, k <- values,
let p@(Point _ (Bucket b_) (Key k_)) = Point bt b k
]
-- | We will supply a list of these operations:
--
-- For each Action a op => a,
data Op a op = Get Bucket Key -- ^ we can get a value
| Update Bucket Key op -- ^ we can update a value
deriving instance (Show op, C.CRDT a o) => Show (Op a op)
class (Show t, C.CRDT t op, Default t) => Action t op where
-- | bucket type for this type (assumed/hardcoded)
bucketType :: Proxy t -> ByteString
-- | extract a value from 'C.DataType', or throw an error
fromDT :: C.DataType -> t
-- | pack a value into 'C.DataType'
toDT :: t -> C.DataType
-- | a kludge (or two), see 'update': if there's no value at the
-- moment, having been provieded with an op, will riak create and
-- operate on a empty value?
updateCreates :: Proxy t -> Maybe C.DataType -> op -> Bool
-- | is operation target there?
targetThere :: Maybe C.DataType -> op -> Bool
instance Action C.Counter C.CounterOp where
bucketType _ = "counters"
fromDT (C.DTCounter c) = c
fromDT _ = error "expected counter" -- ok for tests
toDT = C.DTCounter
updateCreates _ _ _ = True
targetThere _ _ = True
instance Action C.Set C.SetOp where
bucketType _ = "sets"
fromDT (C.DTSet c) = c
fromDT _ = error "expected set"
toDT = C.DTSet
updateCreates _ _ C.SetAdd{} = True
updateCreates _ _ C.SetRemove{} = False
targetThere _ _ = True
instance Action C.Map C.MapOp where
bucketType _ = "maps"
fromDT (C.DTMap c) = c
fromDT _ = error "expected map"
toDT = C.DTMap
-- Riak will operate on default value inside map if it's not there.
-- Except while it's SetRemove.
updateCreates _ _ (C.MapUpdate _ (C.MapSetOp C.SetRemove{})) = False
updateCreates _ _ _ = True
targetThere (Just (C.DTMap m)) (C.MapUpdate path _)
| Just{} <- C.xlookup path C.MapSetTag m = True
targetThere _ _ = False
-- | Many Arbitrary instances.
-- TODO: 'Generic' arbitrary
instance (Arbitrary a, Arbitrary op) => Arbitrary (Op a op) where
arbitrary = oneof [
Get <$> arbitrary <*> arbitrary,
Update <$> arbitrary <*> arbitrary <*> arbitrary
]
instance Arbitrary C.Counter where
arbitrary = C.Counter <$> arbitrary
instance Arbitrary C.CounterOp where
arbitrary = C.CounterInc <$> choose (-16,16) -- https://github.com/basho/riak/issues/804
instance Arbitrary C.SetOp where
arbitrary = oneof [
C.SetAdd <$> arbitrary, C.SetRemove <$> arbitrary
]
instance Arbitrary C.FlagOp where
arbitrary = C.FlagSet <$> arbitrary
instance Arbitrary C.Set where
arbitrary = C.Set . Set.fromList <$> arbitrary
instance Arbitrary ByteString where
arbitrary = elements [ "foo", "bar", "baz" ]
instance Arbitrary C.MapOp where
arbitrary = C.MapUpdate <$> arbitrary <*> arbitrary
instance Arbitrary C.MapPath where
arbitrary = (\a b -> C.MapPath (a :| b)) <$> arbitrary <*> arbitrary
instance Arbitrary C.MapField where
arbitrary = C.MapField <$> arbitrary <*> arbitrary
instance Arbitrary C.MapEntryTag where
arbitrary = elements [ C.MapCounterTag ]
instance Arbitrary C.MapValueOp where
arbitrary = oneof [ C.MapCounterOp <$> arbitrary,
C.MapSetOp <$> arbitrary,
C.MapFlagOp <$> arbitrary ]
-- TODO Fix this
instance Arbitrary C.Map where
arbitrary = pure (C.Map Map.empty)
-- | Abstract machine.
-- Yields a value on 'Get', modifies state on 'Update'.
machine :: (MonadFail m,
MonadWriter [Maybe C.DataType] m,
MonadState s m,
Applicative m,
Action t op)
=> Proxy t -> [Op t op]
-> (Op t op -> s -> m (Either (Maybe C.DataType) s)) -> m ()
machine _ [] _ = pure ()
machine p (a@Get{} : as) onAct = do
v <- get
Left r <- onAct a v
tell [r]
machine p as onAct
machine p (a@Update{} : as) onAct = do
v <- get
Right r <- onAct a v
put r
machine p as onAct
-- | Riak version of the 'machine'.
-- State is 'B.Connection', get/update are IO-requests to riak.
riak :: (MonadFail m,
MonadWriter [Maybe C.DataType] m,
MonadState B.Connection m,
Applicative m, MonadIO m,
Action t op)
=> Proxy t -> [Op t op] -> m ()
riak p ops = machine p ops onAct
where onAct (Get (Bucket b) (Key k)) c
= liftIO $ Left <$> C.get c bt b k
onAct (Update (Bucket b) (Key k) op) c
= do liftIO $ C.sendModify c bt b k [op]
pure $ Right c
bt = bucketType p
-- | Haskell emulation version of the 'machine'.
-- State is 'RiakState', get/update try to match riak's behaviour.
pure_ :: (MonadFail m,
MonadWriter [Maybe C.DataType] m,
MonadState RiakState m,
Applicative m,
Action t op)
=> Proxy t -> [Op t op] -> m ()
pure_ p ops = machine p ops onAct
where
onAct (Get b k) v = pure . Left $ Map.lookup (point b k) v
onAct (Update b k op) v = pure . Right $ Map.alter (update op) (point b k) v
point b k = Point (BucketType (bucketType p)) b k
update :: forall a op. (Action a op) =>
op -> Maybe C.DataType -> Maybe C.DataType
update op dt
-- | Dear diary, this is getting out of hand. It'd be far easier to
-- not bother and assume Nothing ≡ Just mempty in test conditions.
--
-- …but let's continue to bother and gain more safety.
| Just d <- dt, targetThere dt op
= operate d
| updateCreates (Proxy :: Proxy a) dt op
-- it's sometimes ok to update even a non-set value in riak's mind
= operate $ maybe surrogate id dt
| otherwise
= dt
where fromDT' :: C.DataType -> a
fromDT' = fromDT
toDT' :: a -> C.DataType
toDT' = toDT
surrogate = toDT' def
operate = Just . toDT . C.modify op . fromDT'
doRiak :: Action a op =>
Proxy a -> [Op a op] -> IO [Maybe C.DataType]
doRiak p ops = withGlobalConn $ \conn -> do
--print ops
(_,_,r) <- runRWST (riak p ops) () conn
pure r
doPure :: (Action a op, Show op) =>
RiakState -> Proxy a -> [Op a op] -> PropertyM IO [Maybe C.DataType]
doPure stat p ops = do (_,_,r) <- runRWST (pure_ p ops) () stat
pure r
prop :: (Show op, Action a op) =>
Proxy a -> [Op a op] -> Property
prop p ops = monadicIO $ do
stat <- run $ observeRiak p
r1 <- doPure stat p ops
r2 <- run $ doRiak p ops
run . when (r1/=r2) $ print (r1,r2)
assert $ r1 == r2
prop_counters :: [Op C.Counter C.CounterOp] -> Property
prop_counters = prop (Proxy :: Proxy C.Counter)
prop_sets :: [Op C.Set C.SetOp] -> Property
prop_sets = prop (Proxy :: Proxy C.Set)
prop_maps :: [Op C.Map C.MapOp] -> Property
prop_maps = prop (Proxy :: Proxy C.Map)
tests :: TestTree
tests = testGroup "CRDT quickCheck" [
testProperty "counters" prop_counters,
testProperty "sets" prop_sets,
testProperty "maps" prop_maps
]