packages feed

hedis 0.2 → 0.3

raw patch · 10 files changed

+825/−97 lines, 10 filesdep +HUnitdep ~basedep ~mtldep ~resource-poolnew-component:exe:hedis-testPVP ok

version bump matches the API change (PVP)

Dependencies added: HUnit

Dependency ranges changed: base, mtl, resource-pool, time

API changes (from Hackage documentation)

- Database.Redis: Service :: String -> PortID
+ Database.Redis: ConnectionLost :: ConnectionLostException
+ Database.Redis: class RedisResult a
+ Database.Redis: connectMaxConnections :: ConnectInfo -> Int
+ Database.Redis: connectMaxIdleTime :: ConnectInfo -> NominalDiffTime
+ Database.Redis: data ConnectionLostException
+ Database.Redis: decode :: RedisResult a => Reply -> Either Reply a
+ Database.Redis: sendRequest :: RedisResult a => [ByteString] -> Redis (Either Reply a)
- Database.Redis: ConnInfo :: HostName -> PortID -> Maybe ByteString -> ConnectInfo
+ Database.Redis: ConnInfo :: HostName -> PortID -> Maybe ByteString -> Int -> NominalDiffTime -> ConnectInfo
- Database.Redis: blpop :: [ByteString] -> Integer -> Redis (Either Reply (ByteString, ByteString))
+ Database.Redis: blpop :: [ByteString] -> Integer -> Redis (Either Reply (Maybe (ByteString, ByteString)))
- Database.Redis: brpop :: [ByteString] -> Integer -> Redis (Either Reply (ByteString, ByteString))
+ Database.Redis: brpop :: [ByteString] -> Integer -> Redis (Either Reply (Maybe (ByteString, ByteString)))

Files

benchmark/Benchmark.hs view
@@ -10,7 +10,7 @@ import Text.Printf  nRequests, nClients :: Int-nRequests = 10000+nRequests = 100000 nClients  = 50  @@ -33,34 +33,43 @@     start <- newEmptyMVar     done  <- newEmptyMVar     replicateM_ nClients $ forkIO $ do-        c <- connect defaultConnectInfo-        runRedis c $ forever $ do+        runRedis conn $ forever $ do             action <- liftIO $ takeMVar start-            replicateM_ (nRequests `div` nClients) $ action+            action             liftIO $ putMVar done () -    let timeAction name action = do+    let timeAction name nActions action = do         startT <- getCurrentTime-        replicateM_ nClients $ putMVar start action+        -- each clients runs ACTION nRepetitions times+        let nRepetitions = nRequests `div` nClients `div` nActions+        replicateM_ nClients $ putMVar start (replicateM_ nRepetitions action)         replicateM_ nClients $ takeMVar done         stopT <- getCurrentTime-        let deltaT    = realToFrac $ diffUTCTime stopT startT-            rqsPerSec = fromIntegral nRequests / deltaT :: Double-        putStrLn $ printf "%-10s %.2f Req/s" (name :: String) rqsPerSec+        let deltaT     = realToFrac $ diffUTCTime stopT startT+            -- the real # of reqs send. We might have lost some due to 'div'.+            actualReqs = nRepetitions * nActions * nClients+            rqsPerSec  = fromIntegral actualReqs / deltaT :: Double+        putStrLn $ printf "%-20s %10.2f Req/s" (name :: String) rqsPerSec      ----------------------------------------------------------------------     -- Benchmarks     ---    timeAction "ping" $ do+    timeAction "ping" 1 $ do         Right Pong <- ping         return ()         -    timeAction "get" $ do+    timeAction "get" 1 $ do         Right Nothing <- get "key"         return ()     -    timeAction "mget" $ do+    timeAction "mget" 1 $ do         Right vs <- mget ["k1","k2","k3","k4","k5"]         let expected = map Just ["v1","v2","v3","v4","v5"]         True <- return $ vs == expected+        return ()+    +    timeAction "ping (pipelined)" 100 $ do+        pongs <- replicateM 100 ping+        let expected = replicate 100 (Right Pong)+        True <- return $ pongs == expected         return ()
hedis.cabal view
@@ -1,5 +1,5 @@ name:               hedis-version:            0.2+version:            0.3 synopsis:     Client library for the Redis datastore: supports full command set,       pipelining.@@ -10,19 +10,28 @@     datastore. Compared to other Haskell client libraries it has some     advantages:     .-    [Complete Redis 2.4 command set:] All Redis commands -        (<http://redis.io/commands>) are available as haskell functions. The -        exceptions to the rule are the MONITOR and SYNC commands.+    [Complete Redis 2.4 command set:] All Redis commands+        (<http://redis.io/commands>) are available as haskell functions, except+        for the MONITOR and SYNC commands. Additionally, a low-level API is+        exposed that  makes it easy for the library user to implement additional+        commands, such as new commands from an experimental Redis version.     .-    [Pipelining \"Just Works\":] Commands are pipelined +    [Automatic Optimal Pipelining:] Commands are pipelined         (<http://redis.io/topics/pipelining>) as much as possible without any-        work by the user.+        work by the user. See+        <http://informatikr.com/2012/redis-pipelining.html> for a+        technical explanation of automatic optimal pipelining.     .     [Enforced Pub\/Sub semantics:] When subscribed to the Redis Pub\/Sub server         (<http://redis.io/topics/pubsub>), clients are not allowed to issue         commands other than subscribing to or unsubscribing from channels. This         library uses the type system to enforce the correct behavior.     .+    [Connect via TCP or Unix Domain Socket:] TCP sockets are the default way to+        connect to a Redis server. For connections to a server on the same+        machine, Unix domain sockets offer higher performance than the standard+        TCP connection.+    .     For detailed documentation, see the "Database.Redis" module. license:            BSD3 license-file:       LICENSE@@ -43,9 +52,16 @@     description: Build the benchmark executable.     default: False +flag test+    description: Build the test suite.+    default: False+ library   hs-source-dirs:   src-  ghc-options:      -Wall+  if flag(test)+      ghc-options:  -Wall+  else+      ghc-options:  -Wall   ghc-prof-options: -auto-all   exposed-modules:  Database.Redis   build-depends:    attoparsec == 0.10.*,@@ -54,7 +70,8 @@                     bytestring-lexing == 0.2.*,                     mtl == 2.*,                     network == 2.*,-                    resource-pool == 0.2.0.*+                    resource-pool == 0.2.1.*,+                    time    other-modules:    Database.Redis.Core,                     Database.Redis.Connection@@ -69,11 +86,28 @@     main-is: benchmark/Benchmark.hs     if flag(benchmark)         build-depends:-            base >= 4,-            mtl == 2.0.*,+            base == 4.*,+            mtl == 2.*,             hedis,-            time >= 1.2        +            time >= 1.2     else         buildable: False     ghc-options: -Wall -rtsopts     ghc-prof-options: -auto-all++executable hedis-test+    main-is: test/Test.hs+    if flag(test)+        build-depends:+            base == 4.*,+            bytestring == 0.9.*,+            hedis,+            HUnit == 1.2.*,+            mtl == 2.*,+            time+    else+        buildable: False+    -- We use -O0 here, since GHC takes *very* long to compile so many constants+    ghc-options: -O0 -Wall -rtsopts -fno-warn-unused-do-bind+    ghc-prof-options: -auto-all+    
src/Database/Redis.hs view
@@ -1,6 +1,38 @@ {-# LANGUAGE OverloadedStrings #-}  module Database.Redis (+    -- * How To Use This Module+    -- |+    -- Connect to a Redis server:+    --+    -- @+    -- -- connects to localhost:6379+    -- conn <- 'connect' 'defaultConnectInfo'+    -- @+    --+    -- Send commands to the server:+    -- +    -- @+    -- 'runRedis' conn $ do+    --      'set' \"hello\" \"hello\"+    --      set \"world\" \"world\"+    --      hello <- 'get' \"hello\"+    --      world <- get \"world\"+    --      liftIO $ print (hello,world)+    -- @++    -- ** Error Behavior+    -- |+    --  [Operations against keys holding the wrong kind of value:] If the Redis+    --    server returns an 'Error', command functions will return 'Left' the+    --    'Reply'. The library user can inspect the error message to gain +    --    information on what kind of error occured.+    --+    --  [Connection to the server lost:] In case of a lost connection, command+    --    functions throw a +    --    'ConnectionLostException'. It can only be caught outside of+    --    'runRedis', to make sure the connection pool can properly destroy the+    --    connection.          -- * The Redis Monad     Redis(), runRedis,@@ -16,8 +48,19 @@     -- * Pub\/Sub     module Database.Redis.PubSub, -    -- * Redis Return Types-    Reply(..),Status(..)+    -- * Low-Level Command API+    sendRequest,+    -- |'sendRequest' can be used to implement commands from experimental+    --  versions of Redis. An example of how to implement a command is given+    --  below.+    --+    -- @+    -- -- |Redis DEBUG OBJECT command+    -- debugObject :: ByteString -> 'Redis' (Either 'Reply' ByteString)+    -- debugObject key = 'sendRequest' [\"DEBUG\", \"OBJECT\", 'encode' key]+    -- @+    --+    Reply(..),Status(..),RedisResult(..),ConnectionLostException(..),      ) where 
src/Database/Redis/Commands.hs view
@@ -160,7 +160,10 @@ watch, -- |Watch the given keys to determine execution of the MULTI/EXEC block (<http://redis.io/commands/watch>).  -- * Unimplemented Commands--- |These commands are not implemented, as of now.+-- |These commands are not implemented, as of now. Library+--  users can implement these or other commands from+--  experimental Redis versions by using the 'sendRequest'+--  function. -- -- * EVAL (<http://redis.io/commands/eval>) --@@ -255,7 +258,7 @@ blpop     :: [ByteString] -- ^ key     -> Integer -- ^ timeout-    -> Redis (Either Reply (ByteString,ByteString))+    -> Redis (Either Reply (Maybe (ByteString,ByteString))) blpop key timeout = sendRequest (["BLPOP"] ++ map encode key ++ [encode timeout] )  sdiffstore@@ -379,7 +382,7 @@ brpop     :: [ByteString] -- ^ key     -> Integer -- ^ timeout-    -> Redis (Either Reply (ByteString,ByteString))+    -> Redis (Either Reply (Maybe (ByteString,ByteString))) brpop key timeout = sendRequest (["BRPOP"] ++ map encode key ++ [encode timeout] )  hgetall@@ -764,15 +767,4 @@ persist key = sendRequest (["PERSIST"] ++ [encode key] )  --- * Unimplemented Commands--- |These commands are not implemented, as of now.------ * EVAL (<http://redis.io/commands/eval>)--------- * MONITOR (<http://redis.io/commands/monitor>)--------- * SYNC (<http://redis.io/commands/sync>)--- 
src/Database/Redis/Connection.hs view
@@ -1,25 +1,37 @@-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE RecordWildCards, DeriveDataTypeable #-}  module Database.Redis.Connection (-    HostName,PortID(..),+    HostName,PortID(PortNumber,UnixSocket),     ConnectInfo(..),defaultConnectInfo,-    Connection(), connect+    Connection(), connect,+    ConnectionLostException(..) ) where +import Prelude hiding (catch) import Control.Applicative import Control.Monad.Reader import Control.Concurrent+import Control.Exception (Exception, throwIO)+import qualified Data.Attoparsec as P import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy.Char8 as LB import Data.IORef import Data.Pool+import Data.Time+import Data.Typeable import Network (HostName, PortID(..), connectTo)-import System.IO (hClose, hIsOpen, hSetBinaryMode)+import System.IO (Handle, hClose, hIsOpen, hSetBinaryMode, hFlush)+import System.IO.Error+import System.IO.Unsafe (unsafeInterleaveIO)  import Database.Redis.Core import Database.Redis.Commands (auth) import Database.Redis.Reply +data ConnectionLostException = ConnectionLost+    deriving (Show, Typeable)++instance Exception ConnectionLostException+ -- |Information for connnecting to a Redis server. -- -- It is recommended to not use the 'ConnInfo' data constructor directly.@@ -33,44 +45,91 @@ -- @ -- data ConnectInfo = ConnInfo-    { connectHost :: HostName-    , connectPort :: PortID-    , connectAuth :: Maybe B.ByteString+    { connectHost           :: HostName+    , connectPort           :: PortID+    , connectAuth           :: Maybe B.ByteString+    -- ^ When the server is protected by a password, set 'connectAuth' to 'Just'+    --   the password. Each connection will then authenticate by the 'auth'+    --   command.+    , connectMaxConnections :: Int+    -- ^ Maximum number of connections to keep open. The smallest acceptable+    --   value is 1.+    , connectMaxIdleTime    :: NominalDiffTime+    -- ^ Amount of time for which an unused connection is kept open. The+    --   smallest acceptable value is 0.5 seconds.     }  -- |Default information for connecting: -- -- @---  connectHost = \"localhost\"---  connectPort = PortNumber 6379 -- Redis default port---  connectAuth = Nothing         -- No password+--  connectHost           = \"localhost\"+--  connectPort           = PortNumber 6379 -- Redis default port+--  connectAuth           = Nothing         -- No password+--  connectMaxConnections = 50              -- Up to 50 connections+--  connectMaxIdleTime    = 30              -- Keep open for 30 seconds -- @ -- defaultConnectInfo :: ConnectInfo defaultConnectInfo = ConnInfo-    { connectHost = "localhost"-    , connectPort = PortNumber 6379-    , connectAuth = Nothing+    { connectHost           = "localhost"+    , connectPort           = PortNumber 6379+    , connectAuth           = Nothing+    , connectMaxConnections = 50+    , connectMaxIdleTime    = 30     } --- |Opens a connection to a Redis server designated by the given 'ConnectInfo'.+-- |Opens a 'Connection' to a Redis server designated by the given+--  'ConnectInfo'. connect :: ConnectInfo -> IO Connection-connect ConnInfo{..} = do-    let maxIdleTime    = 10-        maxConnections = 50-    Conn <$> createPool create destroy 1 maxIdleTime maxConnections+connect ConnInfo{..} = Conn <$>+    createPool create destroy 1 connectMaxIdleTime connectMaxConnections   where     create = do         h   <- connectTo connectHost connectPort-        rs' <- parseReply <$> {-# SCC "LB.hgetContents" #-} LB.hGetContents h-        rs  <- newIORef rs'+        rs  <- hGetReplies h >>= newIORef+        hSetBinaryMode h True         let conn = (h,rs)         maybe (return ())             (\pass -> runRedisInternal conn (auth pass) >> return ())             connectAuth-        hSetBinaryMode h True         newMVar conn      destroy conn = withMVar conn $ \(h,_) -> do         open <- hIsOpen h         when open (hClose h)++-- |Read all the 'Reply's from the Handle and return them as a lazy list.+--+--  The actual reading and parsing of each 'Reply' is deferred until the spine+--  of the list is evaluated up to that 'Reply'. Each 'Reply' is cons'd in front+--  of the (unevaluated) list of all remaining replies.+--+--  'unsafeInterleaveIO' only evaluates it's result once, making this function +--  thread-safe. 'Handle' as implemented by GHC is also threadsafe, it is safe+--  to call 'hFlush' here. The list constructor '(:)' must be called from+--  /within/ unsafeInterleaveIO, to keep the replies in correct order.+hGetReplies :: Handle -> IO [Reply]+hGetReplies h = lazyRead (Right B.empty)+  where+    lazyRead rest = unsafeInterleaveIO $ do+        parseResult <- either continueParse readAndParse rest+        case parseResult of+            P.Fail _ _ _   -> error "Hedis: reply parse failed"+            P.Partial cont -> lazyRead (Left cont)+            P.Done rest' r -> do+                rs <- lazyRead (Right rest')+                return (r:rs)+    +    continueParse cont = cont <$> B.hGetSome h maxRead+    +    readAndParse rest  = P.parse reply <$>+        if B.null rest+            then do+                hFlush h -- send any pending requests+                s <- B.hGetSome h maxRead `catchIOError` const errConnClosed+                when (B.null s) errConnClosed+                return s+            else return rest++    maxRead       = 4*1024+    errConnClosed = throwIO ConnectionLost
src/Database/Redis/Core.hs view
@@ -14,15 +14,14 @@ import qualified Data.ByteString as B import Data.IORef import Data.Pool-import System.IO (Handle, hFlush)+import System.IO (Handle)  import Database.Redis.Reply import Database.Redis.Request import Database.Redis.Types --- |Connection to a Redis server. Use the 'connect' function to create one.------  A 'Connection' is actually a pool of network connections.+-- |A threadsafe pool of network connections to a Redis server. Use the+--  'connect' function to create one. newtype Connection = Conn (Pool (MVar (Handle, IORef [Reply])))  -- |All Redis commands run in the 'Redis' monad.@@ -40,7 +39,8 @@ -- |Interact with a Redis datastore specified by the given 'Connection'. -- --  Each call of 'runRedis' takes a network connection from the 'Connection'---  pool and runs the given 'Redis' action. Calls to 'runRedis' may thus block, --  while all connections from the pool are in use.+--  pool and runs the given 'Redis' action. Calls to 'runRedis' may thus block+--  while all connections from the pool are in use. runRedis :: Connection -> Redis a -> IO a runRedis (Conn pool) redis =     withResource pool $ \conn ->@@ -54,9 +54,8 @@ send :: [B.ByteString] -> Redis () send req = Redis $ do     h <- askHandle-    liftIO $ do-        {-# SCC "send.hPut" #-} B.hPut h $ renderRequest req-        {-# SCC "send.hFlush" #-} hFlush h+    -- hFlushing the handle is done while reading replies.+    liftIO $ {-# SCC "send.hPut" #-} B.hPut h (renderRequest req)  recv :: Redis Reply recv = Redis $ do
src/Database/Redis/PubSub.hs view
@@ -13,7 +13,7 @@ import Data.ByteString.Char8 (ByteString) import Data.Maybe import qualified Database.Redis.Core as Core-import Database.Redis.Reply+import Database.Redis.Reply (Reply(..)) import Database.Redis.Types  @@ -100,14 +100,13 @@         recv (pending + length cmds)      recv pending = do-        reply <- Core.recv        +        reply <- Core.recv         case decodeMsg reply of-            Left cnt-                | cnt == 0 && pending == 0-                            -> return ()-                | otherwise -> send mempty (pending - 1)-            Right msg       -> do act <- liftIO $ callback msg-                                  send act pending+            Left cnt  -> let pending' = pending - 1+                         in unless (cnt == 0 && pending' == 0) $+                            send mempty pending'+            Right msg -> do act <- liftIO $ callback msg+                            send act pending  ------------------------------------------------------------------------------ -- Helpers@@ -117,7 +116,7 @@ pubSubAction cmd chans = PubSub [cmd : chans]  decodeMsg :: Reply -> Either Integer Message-decodeMsg (MultiBulk (Just (r0:r1:r2:rs))) = either (error "decodeMsg") id $ do+decodeMsg r@(MultiBulk (Just (r0:r1:r2:rs))) = either (errMsg r) id $ do     kind <- decode r0     case kind :: ByteString of         "message"  -> Right <$> decodeMessage@@ -129,4 +128,7 @@     decodePMessage = PMessage <$> decode r1 <*> decode r2                                     <*> decode (head rs)         -decodeMsg r = error $ "not a message: " ++ show r+decodeMsg r = errMsg r++errMsg :: Reply -> a+errMsg r = error $ "Hedis: expected pub/sub-message but got: " ++ show r
src/Database/Redis/Reply.hs view
@@ -1,28 +1,20 @@ {-# LANGUAGE OverloadedStrings #-}-module Database.Redis.Reply (Reply(..), parseReply) where+module Database.Redis.Reply (Reply(..), reply) where  import Prelude hiding (error, take) import Control.Applicative import Data.Attoparsec.Char8-import qualified Data.Attoparsec.Lazy as P-import qualified Data.ByteString.Char8 as S-import qualified Data.ByteString.Lazy.Char8 as L+import qualified Data.Attoparsec as P+import Data.ByteString.Char8  -- |Low-level representation of replies from the Redis server.-data Reply = SingleLine S.ByteString-           | Error S.ByteString+data Reply = SingleLine ByteString+           | Error ByteString            | Integer Integer-           | Bulk (Maybe S.ByteString)+           | Bulk (Maybe ByteString)            | MultiBulk (Maybe [Reply])          deriving (Eq, Show) --- |Parse a lazy 'L.ByteString' into a (possibly infinite) list of 'Reply's.-parseReply :: L.ByteString -> [Reply]-parseReply input =-    case P.parse reply input of-        P.Fail _ _ _  -> []-        P.Done rest r -> r : parseReply rest- ------------------------------------------------------------------------------ -- Reply parsers --@@ -50,7 +42,7 @@         len <- '*' `prefixing` signed decimal         if len < 0             then return Nothing-            else Just <$> count len reply+            else Just <$> P.count len reply   ------------------------------------------------------------------------------@@ -59,8 +51,8 @@ prefixing :: Char -> Parser a -> Parser a c `prefixing` a = char c *> a <* crlf -crlf :: Parser S.ByteString+crlf :: Parser ByteString crlf = string "\r\n" -line :: Parser S.ByteString+line :: Parser ByteString line = takeTill (=='\r')
src/Database/Redis/Types.hs view
@@ -65,7 +65,7 @@         "set"    -> Set         "zset"   -> ZSet         "QUEUED" -> Queued-        _        -> error $ "unhandled status-code: " ++ show s+        _        -> error $ "Hedis: unhandled status-code: " ++ show s     decode r = Left r  instance RedisResult Bool where
+ test/Test.hs view
@@ -0,0 +1,598 @@+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}+module Main (main) where++import Control.Applicative+import Control.Concurrent+import Control.Monad+import Control.Monad.Trans+import Data.Monoid (mappend)+import Data.Time+import Data.Time.Clock.POSIX+import qualified Test.HUnit as Test+import Test.HUnit (runTestTT, (~:))++import Database.Redis+++------------------------------------------------------------------------------+-- Main and helpers+--+main :: IO ()+main = do+    c <- connect defaultConnectInfo+    runTestTT $ Test.TestList $ map ($c) tests+    return ()++type Test = Connection -> Test.Test++testCase :: String -> Redis () -> Test+testCase name r conn = name ~:+    Test.TestCase $ runRedis conn $ flushdb >>=? Ok >> r+    +(>>=?) :: (Eq a, Show a) => Redis (Either Reply a) -> a -> Redis ()+redis >>=? expected = do+    a <- redis+    liftIO $ case a of+        Left reply   -> Test.assertFailure $ "Redis error: " ++ show reply+        Right actual -> expected Test.@=? actual++assert :: Bool -> Redis ()+assert = liftIO . Test.assert++------------------------------------------------------------------------------+-- Tests+--+tests :: [Test]+tests = concat+    [ testsMisc, testsKeys, testsStrings, testsHashes, testsLists, testsZSets+    , [testPubSub], testsConnection, testsServer, [testQuit]+    ]++------------------------------------------------------------------------------+-- Miscellaneous+--+testsMisc :: [Test]+testsMisc = [testForceErrorReply, testPipelining]++testForceErrorReply :: Test+testForceErrorReply = testCase "force error reply" $ do+    set "key" "value"+    -- key is not a hash -> wrong kind of value+    reply <- hkeys "key"+    assert $ case reply of+        Left (Error _) -> True+        _              -> False++testPipelining :: Test+testPipelining = testCase "pipelining" $ do+    let n = 10+    tPipe <- time $ do+        pongs <- replicateM n ping+        assert $ pongs == replicate n (Right Pong)+    +    tNoPipe <- time $ replicateM_ n (ping >>=? Pong)+    -- pipelining should at least be twice as fast.    +    assert $ tNoPipe / tPipe > 2+    +time :: Redis () -> Redis NominalDiffTime+time redis = do+    start <- liftIO $ getCurrentTime+    redis+    liftIO $ fmap (`diffUTCTime` start) getCurrentTime++------------------------------------------------------------------------------+-- Keys+--+testsKeys :: [Test]+testsKeys =+    [ testDel, testExists, testExpire, testExpireAt, testKeys, testMove+    , testPersist, testRandomkey, testRename, testRenamenx, testSort+    , testTtl, testGetType, testObject+    ]++testDel :: Test+testDel = testCase "del" $ do+    set "key" "value" >>=? Ok+    get "key"         >>=? Just "value"+    del ["key"]       >>=? 1+    get "key"         >>=? Nothing++testExists :: Test+testExists = testCase "exists" $ do+    exists "key"      >>=? False+    set "key" "value" >>=? Ok+    exists "key"      >>=? True++testExpire :: Test+testExpire = testCase "expire" $ do+    set "key" "value"  >>=? Ok+    expire "key" 1     >>=? True+    expire "notAKey" 1 >>=? False+    ttl "key"          >>=? 1+    +testExpireAt :: Test+testExpireAt = testCase "expireat" $ do+    set "key" "value"         >>=? Ok+    seconds <- floor . utcTimeToPOSIXSeconds <$> liftIO getCurrentTime+    let expiry = seconds + 1+    expireat "key" expiry     >>=? True+    expireat "notAKey" expiry >>=? False+    ttl "key"                 >>=? 1++testKeys :: Test+testKeys = testCase "keys" $ do+    keys "key*"      >>=? []+    set "key1" "value" >>=? Ok+    set "key2" "value" >>=? Ok+    Right ks <- keys "key*"+    assert $ length ks == 2+    assert $ elem "key1" ks+    assert $ elem "key2" ks++testMove :: Test+testMove = testCase "move" $ do+    set "key" "value" >>=? Ok+    move "key" 13     >>=? True+    get "key"         >>=? Nothing+    select 13         >>=? Ok+    get "key"         >>=? Just "value"++testPersist :: Test+testPersist = testCase "persist" $ do+    set "key" "value" >>=? Ok+    expire "key" 1    >>=? True+    ttl "key"         >>=? 1+    persist "key"     >>=? True+    ttl "key"         >>=? (-1)++testRandomkey :: Test+testRandomkey = testCase "randomkey" $ do+    set "k1" "value" >>=? Ok+    set "k2" "value" >>=? Ok+    Right k <- randomkey+    assert $ k `elem` ["k1", "k2"]++testRename :: Test+testRename = testCase "rename" $ do+    set "k1" "value" >>=? Ok+    rename "k1" "k2" >>=? Ok+    get "k1"         >>=? Nothing+    get "k2"         >>=? Just ("value" )++testRenamenx :: Test+testRenamenx = testCase "renamenx" $ do+    set "k1" "value"   >>=? Ok+    set "k2" "value"   >>=? Ok+    renamenx "k1" "k2" >>=? False+    renamenx "k1" "k3" >>=? True++testSort :: Test+testSort = testCase "sort" $ do+    lpush "ids"     ["1","2","3"]                >>=? 3+    sort "ids" defaultSortOpts                   >>=? ["1","2","3"]+    sortStore "ids" "anotherKey" defaultSortOpts >>=? 3+    mset [("weight_1","1")+         ,("weight_2","2")+         ,("weight_3","3")+         ,("object_1","foo")+         ,("object_2","bar")+         ,("object_3","baz")+         ]+    let opts = defaultSortOpts { sortOrder = Desc, sortAlpha = True+                               , sortLimit = (1,2)+                               , sortBy    = Just "weight_*"+                               , sortGet   = ["object_*"] }+    sort "ids" opts >>=? ["bar","foo"]+    +    +testTtl :: Test+testTtl = testCase "ttl" $ do+    set "key" "value" >>=? Ok+    ttl "notAKey"     >>=? (-1)+    ttl "key"         >>=? (-1)+    expire "key" 42   >>=? True+    ttl "key"         >>=? 42++testGetType :: Test+testGetType = testCase "getType" $ do+    getType "key"     >>=? None+    forM_ ts $ \(setKey, typ) -> do+        setKey+        getType "key" >>=? typ+        del ["key"]   >>=? 1+  where+    ts = [ (set "key" "value"                         >>=? Ok,   String)+         , (hset "key" "field" "value"                >>=? True, Hash)+         , (lpush "key" ["value"]                     >>=? 1,    List)+         , (sadd "key" ["member"]                     >>=? 1,    Set)+         , (zadd "key" [(42,"member"),(12.3,"value")] >>=? 2,    ZSet)+         ]++testObject :: Test+testObject = testCase "object" $ do+    set "key" "value"    >>=? Ok+    objectRefcount "key" >>=? 1+    Right _ <- objectEncoding "key"+    objectIdletime "key" >>=? 0++------------------------------------------------------------------------------+-- Strings+--+testsStrings :: [Test]+testsStrings =+    [ testAppend, testDecr, testDecrby, testGetbit, testGetrange, testGetset+    , testIncr, testIncrby, testMget, testMset, testMsetnx, testSetbit+    , testSetex, testSetnx, testSetrange, testStrlen, testSetAndGet+    ]++testAppend :: Test+testAppend = testCase "append" $ do+    set "key" "hello"    >>=? Ok+    append "key" "world" >>=? 10+    get "key"            >>=? Just "helloworld"++testDecr :: Test+testDecr = testCase "decr" $ do+    set "key" "42" >>=? Ok+    decr "key"     >>=? 41++testDecrby :: Test+testDecrby = testCase "decrby" $ do+    set "key" "42"  >>=? Ok+    decrby "key" 2  >>=? 40++testGetbit :: Test+testGetbit = testCase "getbit" $ getbit "key" 42 >>=? 0++testGetrange :: Test+testGetrange = testCase "getrange" $ do+    set "key" "value"     >>=? Ok+    getrange "key" 1 (-2) >>=? "alu"++testGetset :: Test+testGetset = testCase "getset" $ do+    getset "key" "v1" >>=? Nothing+    getset "key" "v2" >>=? Just "v1"++testIncr :: Test+testIncr = testCase "incr" $ do+    set "key" "42" >>=? Ok+    incr "key"     >>=? 43++testIncrby :: Test+testIncrby = testCase "incrby" $ do+    set "key" "40" >>=? Ok+    incrby "key" 2 >>=? 42++testMget :: Test+testMget = testCase "mget" $ do+    set "k1" "v1"               >>=? Ok+    set "k2" "v2"               >>=? Ok+    mget ["k1","k2","notAKey" ] >>=? [Just "v1", Just "v2", Nothing]++testMset :: Test+testMset = testCase "mset" $ do+    mset [("k1","v1"), ("k2","v2")] >>=? Ok+    get "k1"                        >>=? Just "v1"+    get "k2"                        >>=? Just "v2"++testMsetnx :: Test+testMsetnx = testCase "msetnx" $ do+    msetnx [("k1","v1"), ("k2","v2")] >>=? True+    msetnx [("k1","v1"), ("k2","v2")] >>=? False++testSetbit :: Test+testSetbit = testCase "setbit" $ do+    setbit "key" 42 "1" >>=? 0+    setbit "key" 42 "0" >>=? 1+    +testSetex :: Test+testSetex = testCase "setex" $ do+    setex "key" 1 "value" >>=? Ok+    ttl "key"             >>=? 1++testSetnx :: Test+testSetnx = testCase "setnx" $ do+    setnx "key" "v1" >>=? True+    setnx "key" "v2" >>=? False++testSetrange :: Test+testSetrange = testCase "setrange" $ do+    set "key" "value"      >>=? Ok+    setrange "key" 1 "ers" >>=? 5+    get "key"              >>=? Just "verse"++testStrlen :: Test+testStrlen = testCase "strlen" $ do+    set "key" "value" >>=? Ok+    strlen "key"      >>=? 5++testSetAndGet :: Test+testSetAndGet = testCase "set/get" $ do+    get "key"         >>=? Nothing+    set "key" "value" >>=? Ok+    get "key"         >>=? Just "value"+++------------------------------------------------------------------------------+-- Hashes+--+testsHashes :: [Test]+testsHashes =+    [ testHdel, testHexists,testHget, testHgetall, testHincrby, testHkeys+    , testHlen, testHmget, testHmset, testHset, testHsetnx, testHvals+    ]++testHdel :: Test+testHdel = testCase "hdel" $ do+    hdel "key" ["field"]       >>=? False+    hset "key" "field" "value" >>=? True+    hdel "key" ["field"]       >>=? True++testHexists :: Test+testHexists = testCase "hexists" $ do+    hexists "key" "field"      >>=? False+    hset "key" "field" "value" >>=? True+    hexists "key" "field"      >>=? True++testHget :: Test+testHget = testCase "hget" $ do+    hget "key" "field"         >>=? Nothing+    hset "key" "field" "value" >>=? True+    hget "key" "field"         >>=? Just "value"++testHgetall :: Test+testHgetall = testCase "hgetall" $ do+    hgetall "key"                         >>=? []+    hmset "key" [("f1","v1"),("f2","v2")] >>=? Ok+    hgetall "key"                         >>=? [("f1","v1"), ("f2","v2")]+    +testHincrby :: Test+testHincrby = testCase "hincrby" $ do+    hset "key" "field" "40" >>=? True+    hincrby "key" "field" 2 >>=? 42++testHkeys :: Test+testHkeys = testCase "hkeys" $ do+    hset "key" "field" "value" >>=? True+    hkeys "key"                >>=? ["field"]++testHlen :: Test+testHlen = testCase "hlen" $ do+    hlen "key"                 >>=? 0+    hset "key" "field" "value" >>=? True+    hlen "key"                 >>=? 1++testHmget :: Test+testHmget = testCase "hmget" $ do+    hmset "key" [("f1","v1"), ("f2","v2")] >>=? Ok+    hmget "key" ["f1", "f2", "nofield" ]   >>=? [Just "v1", Just "v2", Nothing]++testHmset :: Test+testHmset = testCase "hmset" $ do+    hmset "key" [("f1","v1"), ("f2","v2")] >>=? Ok++testHset :: Test+testHset = testCase "hset" $ do+    hset "key" "field" "value" >>=? True+    hset "key" "field" "value" >>=? False++testHsetnx :: Test+testHsetnx = testCase "hsetnx" $ do+    hsetnx "key" "field" "value" >>=? True+    hsetnx "key" "field" "value" >>=? False++testHvals :: Test+testHvals = testCase "hvals" $ do+    hset "key" "field" "value" >>=? True+    hvals "key"                >>=? ["value"]+++------------------------------------------------------------------------------+-- Lists+--+testsLists :: [Test]+testsLists =+    [testBlpop, testBrpoplpush, testLpop, testLinsert, testLpushx, testLset]++testBlpop :: Test+testBlpop = testCase "blpop/brpop" $ do+    lpush "key" ["v3","v2","v1"] >>=? 3+    blpop ["notAKey","key"] 1    >>=? Just ("key","v1")+    brpop ["notAKey","key"] 1    >>=? Just ("key","v3")+    -- run into timeout+    blpop ["notAKey"] 1          >>=? Nothing++testBrpoplpush :: Test+testBrpoplpush = testCase "brpoplpush/rpoplpush" $ do+    rpush "k1" ["v1","v2"] >>=? 2+    brpoplpush "k1" "k2" 1 >>=? "v2"+    rpoplpush "k1" "k2"    >>=? "v1"+    llen "k2"              >>=? 2+    llen "k1"              >>=? 0++testLpop :: Test+testLpop = testCase "lpop/rpop" $ do+    lpush "key" ["v3","v2","v1"] >>=? 3+    lpop "key"                   >>=? "v1"+    llen "key"                   >>=? 2+    rpop "key"                   >>=? "v3"++testLinsert :: Test+testLinsert = testCase "linsert" $ do+    rpush "key" ["v2"]                 >>=? 1+    linsertBefore "key" "v2" "v1"      >>=? 2+    linsertBefore "key" "notAVal" "v3" >>=? (-1)+    linsertAfter "key" "v2" "v3"       >>=? 3    +    linsertAfter "key" "notAVal" "v3"  >>=? (-1)+    lindex "key" 0                     >>=? "v1"+    lindex "key" 2                     >>=? "v3"++testLpushx :: Test+testLpushx = testCase "lpushx/rpushx" $ do+    lpushx "notAKey" "v1" >>=? 0+    lpush "key" ["v2"]    >>=? 1+    lpushx "key" "v1"     >>=? 2+    rpushx "key" "v3"     >>=? 3++testLset :: Test+testLset = testCase "lset/lrem/ltrim" $ do+    lpush "key" ["v3","v2","v2","v1","v1"] >>=? 5+    lset "key" 1 "v2"                      >>=? Ok+    lrem "key" 2 "v2"                      >>=? 2+    llen "key"                             >>=? 3+    ltrim "key" 0 1                        >>=? Ok+    lrange "key" 0 1                       >>=? ["v1", "v2"]++------------------------------------------------------------------------------+-- Sets+--++------------------------------------------------------------------------------+-- Sorted Sets+--+testsZSets :: [Test]+testsZSets = [testZAdd, testZRank, testZRemRange, testZRange, testZStore]++testZAdd :: Test+testZAdd = testCase "zadd/zrem/zcard/zscore/zincrby" $ do+    zadd "key" [(1,"v1"),(2,"v2"),(40,"v3")] >>=? 3+    zscore "key" "v3"                        >>=? 40+    zincrby "key" 2 "v3"                     >>=? 42+    zrem "key" ["v3","notAKey"]              >>=? 1+    zcard "key"                              >>=? 2++testZRank :: Test+testZRank = testCase "zrank/zrevrank/zcount" $ do+    zadd "key" [(1,"v1"),(2,"v2"),(40,"v3")] >>=? 3+    zrank "key" "v1"                         >>=? 0+    zrevrank "key" "v1"                      >>=? 2+    zcount "key" 10 100                      >>=? 1++testZRemRange :: Test+testZRemRange = testCase "zremrangebyscore/zremrangebyrank" $ do+    zadd "key" [(1,"v1"),(2,"v2"),(40,"v3")] >>=? 3+    zremrangebyrank "key" 0 1                >>=? 2+    zadd "key" [(1,"v1"),(2,"v2"),(40,"v3")] >>=? 2+    zremrangebyscore "key" 10 100            >>=? 1++testZRange :: Test+testZRange = testCase "zrange/zrevrange/zrangebyscore/zrevrangebyscore" $ do+    zadd "key" [(1,"v1"),(2,"v2"),(3,"v3")]           >>=? 3+    zrange "key" 0 1                                  >>=? ["v1","v2"]+    zrevrange "key" 0 1                               >>=? ["v3","v2"]+    zrangeWithscores "key" 0 1                        >>=? [("v1",1),("v2",2)]+    zrevrangeWithscores "key" 0 1                     >>=? [("v3",3),("v2",2)]+    zrangebyscore "key" 0.5 1.5                       >>=? ["v1"]+    zrangebyscoreWithscores "key" 0.5 1.5             >>=? [("v1",1)]+    zrangebyscoreLimit "key" 0.5 2.5 0 1              >>=? ["v1"]+    zrangebyscoreWithscoresLimit "key" 0.5 2.5 0 1    >>=? [("v1",1)]+    zrevrangebyscore "key" 1.5 0.5                    >>=? ["v1"]+    zrevrangebyscoreWithscores "key" 1.5 0.5          >>=? [("v1",1)]+    zrevrangebyscoreLimit "key" 2.5 0.5 0 1           >>=? ["v2"]+    zrevrangebyscoreWithscoresLimit "key" 2.5 0.5 0 1 >>=? [("v2",2)]++testZStore :: Test+testZStore = testCase "zunionstore/zinterstore" $ do+    zadd "k1" [(1, "v1"), (2, "v2")]+    zadd "k2" [(2, "v2"), (3, "v3")]+    zinterstore "newkey" ["k1","k2"] Sum                >>=? 1+    zinterstoreWeights "newkey" [("k1",1),("k2",2)] Max >>=? 1+    zunionstore "newkey" ["k1","k2"] Sum                >>=? 3+    zunionstoreWeights "newkey" [("k1",1),("k2",2)] Min >>=? 3+++------------------------------------------------------------------------------+-- Pub/Sub+--+testPubSub :: Test+testPubSub conn = testCase "pubSub" go conn+  where+    go = do+        -- producer+        liftIO $ forkIO $ do+            runRedis conn $ do+                publish "chan1" "hello" >>=? 1+                publish "chan2" "world" >>=? 1+            return ()++        -- consumer+        pubSub (subscribe ["chan1"]) $ \msg -> do+            -- ready for a message+            case msg of+                Message{..} -> return+                    (unsubscribe [msgChannel] `mappend` psubscribe ["chan*"])+                PMessage{..} -> return (punsubscribe [msgPattern])+++------------------------------------------------------------------------------+-- Transaction+--++------------------------------------------------------------------------------+-- Connection+--+testsConnection :: [Test]+testsConnection = [ testEcho, testPing, testSelect ]++testEcho :: Test+testEcho = testCase "echo" $+    echo ("value" ) >>=? "value"++testPing :: Test+testPing = testCase "ping" $ ping >>=? Pong++testQuit :: Test+testQuit = testCase "quit" $ quit >>=? Ok++testSelect :: Test+testSelect = testCase "select" $ do+    select 13 >>=? Ok+    select 0 >>=? Ok+++------------------------------------------------------------------------------+-- Server+--+testsServer :: [Test]+testsServer =+    [testBgrewriteaof, testFlushall, testInfo, testConfig, testSlowlog+    ,testDebugObject]++testBgrewriteaof :: Test+testBgrewriteaof = testCase "bgrewriteaof/bgsave/save" $ do+    save >>=? Ok+    -- TODO return types not as documented+    -- bgsave       >>=? BgSaveStarted+    -- bgrewriteaof >>=? BgAOFRewriteStarted++testConfig :: Test+testConfig = testCase "config/auth" $ do+    configSet "requirepass" "pass" >>=? Ok+    auth "pass"                    >>=? Ok+    configSet "requirepass" ""     >>=? Ok+    +testFlushall :: Test+testFlushall = testCase "flushall/flushdb" $ do+    flushall >>=? Ok+    flushdb  >>=? Ok++testInfo :: Test+testInfo = testCase "info/lastsave/dbsize" $ do+    Right _ <- info+    Right _ <- lastsave+    dbsize          >>=? 0+    configResetstat >>=? Ok++testSlowlog :: Test+testSlowlog = testCase "slowlog" $ do+    slowlogGet 5 >>=? MultiBulk (Just [])+    slowlogLen   >>=? 0+    slowlogReset >>=? Ok++testDebugObject :: Test+testDebugObject = testCase "debugObject/debugSegfault" $ do+    set "key" "value" >>=? Ok+    Right _ <- debugObject "key"+    -- Right Ok <- debugSegfault+    return ()