Source

couchdb-enumerator / tests.hs

Full commit
{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving, FlexibleContexts #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Main where

import           Control.Applicative
import qualified Control.Exception.Lifted as E
import           Control.Monad
import           Control.Monad.IO.Class (MonadIO, liftIO)
import           Control.Monad.Trans.Class (lift)
import           Control.Monad.Trans.Control (MonadBaseControl)
import           Control.Monad.Trans.Reader
import           Data.Aeson ((.=))
import qualified Data.Aeson as A
import qualified Data.ByteString.UTF8 as BU8
import           Data.Enumerator hiding (map, length, head, run, drop)
import qualified Data.Enumerator.List as EL
import           Data.List (find, deleteBy, nubBy)
import qualified Data.HashMap.Lazy as M
import           Data.Maybe (fromJust)
import qualified Data.Text as T
import qualified Data.Vector as V

import Database.CouchDB.Enumerator

import Test.Framework (defaultMain, Test)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.Framework.Providers.HUnit (testCase)

import Test.QuickCheck
import Test.QuickCheck.Monadic
import Test.HUnit hiding (Test, path)

main :: IO ()
main = defaultMain tests

tests :: [Test]
tests = [ testCouchCase     "basic connection"      connectTest
        , testCouchCase     "basic error"           missingObjectTest
        , testCouchProperty "single insert" (1,7)   insertTest
        , testCouchCase     "basic delete"          deleteTest
        , testCouchProperty "view"  (12,13)         views
        ]
   
----------------------------------------------------------------------------------
--- Test Helpers
----------------------------------------------------------------------------------

type CouchT m a = ReaderT CouchConnection m a

testCouch :: CouchT IO a -> IO ()
testCouch c = withCouchConnection "172.16.5.2" 5984 "testcouchenum" (runReaderT c) >> return ()

testCouchCase :: String -> CouchT IO a -> Test
testCouchCase s c = testCase s $ testCouch c

testCouchProperty :: (Show a, Arbitrary a) => String -> (Int,Int) -> ([a] -> CouchT IO b) -> Test
testCouchProperty s i f = testProperty s $ monadicIO $ do
    len <- pick $ choose i
    lst <- pick $ vector len
    run $ testCouch $ f lst

-- | Assert that the value is a string, and check that it matches the given string
assertStr :: T.Text -> A.Value -> Assertion
assertStr t (A.String t') = unless (t == t') $ assertFailure $ "strings are not equal. expecting " 
                                                   ++ T.unpack t ++ "  received  " ++ T.unpack t'
assertStr _ _ = assertFailure "expecting a JSON string"

member :: T.Text -> A.Object -> Bool
member k o = M.lookup k o /= Nothing

isSubmapOf :: A.Object -> A.Object -> Bool
isSubmapOf x y = 0 == M.size (M.difference x y)

-- | Assert that the given key exists, and the value matches the given assertion
assertObjMember :: T.Text -> (A.Value -> Assertion) -> A.Object -> Assertion
assertObjMember t f x = do
    assertBool (T.unpack t ++ " is missing") $ member t x
    f $ fromJust $ M.lookup t x

-- | Check an action for a couch error
checkError :: MonadBaseControl IO m => Maybe Int -> m () -> m ()
checkError code m = E.catch m handler
  where handler e@(CouchError c _) = unless (c == code) $ E.throwIO e

-- | Expect a couch error with the given code
assertRecvError :: (MonadIO m, MonadBaseControl IO m) => Maybe Int -> m a -> m ()
assertRecvError code v = checkError code $ v >> liftIO (assertFailure "was expecting a couch error")

-- | Check that an object in the database matches the given value.
checkLoad :: String -> A.Object -> CouchT IO ()
checkLoad n obj = do
    obj' <- couchGet n []
    lift $ assertBool "returned object does not match" $ isSubmapOf obj obj'

-- | Delete the given object, useful for the start of a test
clearObject :: String -> CouchT IO ()
clearObject n = checkError (Just 404) go
  where go = do obj <- couchGet n []
                unless (member "_rev" obj) $ fail "_rev is missing"
                let (A.String rev) = fromJust $ M.lookup "_rev" obj
                couchDelete n rev

newtype ArbitraryObject = ArbitraryObject { unArbObject :: A.Object }
    deriving (Show,Eq,A.FromJSON,A.ToJSON)

instance Arbitrary T.Text where
    arbitrary = liftM T.pack $ listOf $ elements $ ['a'..'z'] ++ ['A'..'Z'] ++ " 1234567890!@#$%^&*()+|"
    shrink "" = []
    shrink x  = [T.tail x]

arbBaseValue :: Gen A.Value
arbBaseValue = oneof [ A.String <$> arbitrary
                     , A.toJSON <$> (arbitrary :: Gen Integer)
                     , A.Bool <$> arbitrary
                     , return A.Null
                     ]

arbObject :: Bool -> Gen A.Object
arbObject onlyBase = do nkeys <- choose (3,15)
                        keys <- vectorOf nkeys arbitrary
                        vals <- vectorOf nkeys $ if onlyBase
                                                    then arbBaseValue
                                                    else frequency [ (8, arbBaseValue)
                                                                   , (1, A.Object <$> arbObject False)
                                                                   , (1, A.Array <$> arbArrayOfObj)
                                                                   ]

                        return $ M.fromList $ zip keys vals 

arbArrayOfObj :: Gen A.Array
arbArrayOfObj = do len <- choose (1,20)
                   vals <- vectorOf len (A.Object <$> arbObject False)
                   return $ V.fromList vals

instance Arbitrary ArbitraryObject where
    arbitrary = ArbitraryObject <$> arbObject True

----------------------------------------------------------------------------------
--- Base Tests
----------------------------------------------------------------------------------

connectTest :: CouchT IO ()
connectTest = do
    v <- couchGet "" []
    lift $ assertObjMember "db_name" (assertStr "testcouchenum") v

missingObjectTest :: CouchT IO ()
missingObjectTest = assertRecvError (Just 404) $ couchGet "jaosihaweoghaweiouhawef" []

insertTest :: [(Int,ArbitraryObject,ArbitraryObject)] -> CouchT IO ()
insertTest objs = do
    let objs' = nubBy (\(a,_,_) (b,_,_) -> a == b) objs
    let keys  = map (("otest"++) . show . (\(a,_,_) -> a)) objs'
    let vals1 = map (\(_,ArbitraryObject a,_) -> a) objs'
    let vals2 = map (\(_,_,ArbitraryObject a) -> a) objs'

    mapM_ clearObject keys

    rev <- forM (zip keys vals1) $ \(k,o) -> couchPut k [] o
    forM_ (zip3 rev keys vals1) $ \(r,k,o) ->
        checkLoad k $ M.insert "_rev" (A.toJSON r) o

    rev2 <- forM (zip3 rev keys vals2) $ \(r,k,o) ->
        couchPut k [] $ M.insert "_rev" (A.toJSON r) o

    forM_ (zip3 rev2 keys vals2) $ \(r,k,o) ->
        checkLoad k $ M.insert "_rev" (A.toJSON r) o

deleteTest :: CouchT IO ()
deleteTest = do
    (ArbitraryObject obj) <- liftM (head . drop 5) $ lift $ sample' arbitrary

    clearObject "deltest"

    rev <- couchPut "deltest" [] obj
    checkLoad "deltest" obj
    couchDelete "deltest" rev
    assertRecvError (Just 404) $ couchGet "deltest" []

----------------------------------------------------------------------------------
--- View Tests
----------------------------------------------------------------------------------

checkEqual :: (Monad m) => [A.Object] -> [A.Object] -> Iteratee a m ()
checkEqual []     []    = return ()
checkEqual []     (x:_) = error $ "Extra object in list 2  " ++ show (A.encode x)
checkEqual (x:xs) lst2  = case find (isSubmapOf x) lst2 of
                            Nothing -> error $ "Unable to find " ++ show (A.encode $ A.Object x)
                            Just _  -> checkEqual xs $ deleteBy isSubmapOf x lst2

assertViewRet :: (MonadIO m) => [A.Object] -> Enumerator A.Object m () -> m ()
assertViewRet lst e = run_ (e $$ EL.consume >>= checkEqual lst)

addKeys :: Int -> Int -> Int -> ArbitraryObject -> A.Object
addKeys u g t (ArbitraryObject o) = o `M.union` M.fromList [ ("user", A.toJSON u)
                                                           , ("group", A.toJSON g)
                                                           , ("otype", A.toJSON t)
                                                           ]

addView :: CouchT IO ()
addView = couchPut_ "_design/dataviews" [] viewObj where
   viewObj = A.object 
        [ "language"    .= ("javascript" :: T.Text)
        , "views"       .= A.object
            [ "bytype"  .= A.object
                [ "map" .= ("function(doc) {\
                              \   emit([doc.user,doc.group,doc.otype], doc); \
                              \}" :: T.Text)
                ]
            ]
        ]

queryByType :: Integer -> Integer -> Integer -> Enumerator A.Object (ReaderT CouchConnection IO) b
queryByType u g t = couchView path query $= extractViewValue
    where path  = "dataviews/_view/bytype"
          key   = "[" ++ show u ++ "," ++ show g ++ "," ++ show t ++ "]"
          query = [(BU8.fromString "key", Just $ BU8.fromString key)]

queryByGroup :: Integer -> Integer -> Enumerator A.Object (ReaderT CouchConnection IO) b
queryByGroup u g = couchView path query $= extractViewValue
    where path  = "dataviews/_view/bytype"
          skey  = "[" ++ show u ++ "," ++ show g ++ "]"
          ekey  = "[" ++ show u ++ "," ++ show g ++ ",{}]"
          query = [ (BU8.fromString "startkey", Just $ BU8.fromString skey)
                  , (BU8.fromString "endkey"  , Just $ BU8.fromString ekey)
                  ]

views :: [ArbitraryObject] -> CouchT IO ()
views lst = do
    let (group1,x1) = splitAt 3 lst
        (group2,x2) = splitAt 3 x1
        (group3,x3) = splitAt 3 x2
        (group4,_ ) = splitAt 3 x3

        g1key = map (("view"++) . show) ([0..2] :: [Int])
        g1obj = map (addKeys 0 0 0) group1
        g2key = map (("view"++) . show) ([5..7] :: [Int])
        g2obj = map (addKeys 0 0 1) group2
        g3key = map (("view"++) . show) ([10..12] :: [Int])
        g3obj = map (addKeys 0 1 0) group3
        g4key = map (("view"++) . show) ([15..17] :: [Int])
        g4obj = map (addKeys 1 0 0) group4

    mapM_ clearObject $ g1key ++ g2key ++ g3key ++ g4key

    checkError (Just 409) addView

    forM_ (zip g1key g1obj ++ zip g2key g2obj ++ zip g3key g3obj ++ zip g4key g4obj) $ \(k,o) ->
        couchPut_ k [] o

    assertViewRet []    $ queryByType 0 0 2
    assertViewRet []    $ queryByType 0 2 0
    assertViewRet []    $ queryByType 2 0 0

    assertViewRet g1obj $ queryByType 0 0 0
    assertViewRet g2obj $ queryByType 0 0 1
    assertViewRet g3obj $ queryByType 0 1 0
    assertViewRet g4obj $ queryByType 1 0 0

    assertViewRet (g1obj ++ g2obj) $ queryByGroup 0 0
    assertViewRet g3obj $ queryByGroup 0 1
    assertViewRet g4obj $ queryByGroup 1 0

    assertViewRet [] $ queryByGroup 0 2
    assertViewRet [] $ queryByGroup 2 0

-- vim: set expandtab:set tabstop=4: