packages feed

yeshql-1.0.0.1: tests/Database/HDBC/Mock.hs

-- | Mock HDBC database connection for unit testing purposes.
module Database.HDBC.Mock
( 
-- * Mock Connections
  MockConnection (..)
, defMockConnection
-- * Logging interaction
, loggingConnection
-- * Chat Scripts
, newChatConnection
, ChatStep (..)
-- * Constraints
-- The Contraint API is a minilanguage for declaring and combining constraints
-- on arbitrary values.
, Constraint
-- | Construct a constraint from a raw predicate.
, constraint
-- | Check a constraint against a value.
, check
-- | Check a list of constraints against a list of values.
, checkAll
-- | A constraint that matches a value exactly (as per 'Eq' / '==')
, exactly
-- | A constraint that matches a value by a comparison function
, sameBy
-- | A constraint that matches a value exactly after applying some pre-processing
, sameThrough
-- | A constraint that matches any value
, anything
-- | The inclusive-OR operator on 'Constraint's.
, (<||>)
)
where

import Database.HDBC
            ( IConnection (..)
            , SqlValue (..)
            , SqlColDesc (..)
            )
import Database.HDBC.Statement
            ( Statement (..)
            )
import Control.Monad (forM_, when)
import Control.Concurrent.STM (atomically, STM)
import Control.Concurrent.STM.TChan
    ( TChan
    , newTChan
    , writeTChan
    , readTChan
    , tryReadTChan
    , newTChanIO
    )

data MockConnection =
    MockConnection
        { mockDisconnect :: IO ()
        , mockCommit :: IO ()
        , mockRollback :: IO ()
        , mockRun :: String -> [SqlValue] -> IO Integer
        , mockPrepare :: String -> IO Statement
        , mockClone :: IO MockConnection
        , mockTransactionSupport :: Bool
        , mockGetTables :: IO [String]
        , mockDescribeTable :: String -> IO [(String, SqlColDesc)]
        }

defMockConnection :: MockConnection
defMockConnection =
    MockConnection
        { mockDisconnect = return ()
        , mockCommit = return ()
        , mockRollback = return ()
        , mockRun = \q p -> fail "'run' method not implemented"
        , mockPrepare = \q -> fail "'prepare' method not implemented"
        , mockClone = fail "'clone' method not implemented"
        , mockTransactionSupport = False
        , mockGetTables = return []
        , mockDescribeTable = const $ return []
        }

instance IConnection MockConnection where
    disconnect = mockDisconnect
    commit = mockCommit
    rollback = mockRollback
    run = mockRun
    prepare = mockPrepare
    clone = mockClone
    hdbcDriverName = const "mock"
    hdbcClientVer = const "0.0"
    dbServerVer = const "0.0"
    dbTransactionSupport = mockTransactionSupport
    getTables = mockGetTables
    describeTable = mockDescribeTable

tee :: (a -> IO ()) -> IO a -> IO a
tee sideChannel action = do
    retval <- action
    sideChannel retval
    return retval

loggingStatement :: String -> (String -> IO ()) -> Statement -> Statement
loggingStatement query log stmt =
    stmt
        { execute = \params -> do
            log $ "execute " ++ show query ++ " with " ++ show params
            tee (log . show) $ execute stmt params
        , executeRaw = do
            log $ "executeRaw " ++ show query
            tee (log . show) $ executeRaw stmt
        , executeMany = \rows -> do
            log $ "executeMany " ++ show query ++ " with: "
            forM_ rows $ \params -> log $ "    " ++ show params
            tee (log . show) $ executeMany stmt rows
        , finish = do
            log $ "finish"
            finish stmt
        }

loggingConnection :: IConnection conn => (String -> IO ()) -> conn -> MockConnection
loggingConnection log conn =
    defMockConnection
        { mockDisconnect = do
            log "disconnect"
            disconnect conn
        , mockCommit = do
            log "commit"
            commit conn
        , mockRollback = do
            log "rollback"
            rollback conn
        , mockRun = \q p -> do
            log $ "run " ++ show q ++ " with " ++ show p
            tee (log . show) $ run conn q p
        , mockPrepare = \q -> do
            log $ "prepare " ++ show q
            loggingStatement q log <$> prepare conn q
        , mockClone = do
            log $ "clone"
            loggingConnection log <$> clone conn
        , mockTransactionSupport = dbTransactionSupport conn
        , mockGetTables = do
            log $ "getTables"
            tee (log . show) $ getTables conn
        , mockDescribeTable = \tbl -> do
            log $ "describeTable " ++ tbl
            tee (log . show) $ describeTable conn tbl
        }

tChanFromList :: [a] -> IO (TChan a)
tChanFromList xs = atomically $ do
    newTChan >>= fillTChanFromList xs

fillTChanFromList :: [a] -> (TChan a) -> STM (TChan a)
fillTChanFromList xs c = do
    forM_ xs $ writeTChan c
    return c

newtype Constraint a = Constraint { check :: a -> Bool }

instance Monoid (Constraint a) where
    mempty = anything
    mappend (Constraint a) (Constraint b) =
        Constraint (\x -> a x && b x)

(<||>) :: Constraint a -> Constraint a -> Constraint a
a <||> b = Constraint $ \x -> check a x || check b x

constraint :: (a -> Bool) -> Constraint a
constraint = Constraint

exactly :: Eq a => a -> Constraint a
exactly = sameBy (==)

sameThrough :: Eq a => (a -> a) -> a -> Constraint a
sameThrough pp lhs = constraint $ \rhs -> pp lhs == pp rhs

sameBy :: (a -> a -> Bool) -> a -> Constraint a
sameBy cmp val = constraint $ cmp val

anything :: Constraint a
anything = constraint (const True)

data ChatStep =
    ChatStep
        { chatQuery :: Constraint String
        , chatParams :: [Constraint SqlValue]
        , chatResultSet :: [[SqlValue]]
        , chatColumnNames :: [String]
        , chatRowsAffected :: Integer
        }

checkAll :: [Constraint a] -> [a] -> Bool
checkAll constraints values =
    all id $ zipWith check constraints values

execChatStep :: String -> [SqlValue] -> TChan [SqlValue] -> ChatStep -> IO Integer
execChatStep query params resultChan step = do
    when (not $ check (chatQuery step) query) $
        fail $
            "Chat script mismatch: unexpected query:\n\t" ++
            query
    when (not $ checkAll (chatParams step) params) $
        fail $
            "Parameter mismatch: got " ++ show params
    atomically $ fillTChanFromList (chatResultSet step) resultChan
    return $ chatRowsAffected step

newChatConnection :: [ChatStep] -> IO MockConnection
newChatConnection steps = do
    c <- tChanFromList steps
    mkChatConnection c
    where
        mkChatConnection c =
            return $ defMockConnection
                { mockRun = \q p -> do
                    resultVar <- newTChanIO
                    step <- atomically $ readTChan c
                    execChatStep q p resultVar step
                , mockPrepare = \q -> do
                    resultVar <- newTChanIO
                    let exec p = do
                            atomically (readTChan c) >>= execChatStep q p resultVar
                    return $
                        Statement
                            { execute = exec
                            , executeRaw = exec [] >> return ()
                            , executeMany = \ps -> mapM exec ps >> return ()
                            , finish = return ()
                            , fetchRow = atomically $ tryReadTChan resultVar
                            , getColumnNames = return []
                            , originalQuery = q
                            , describeResult = return []
                            }
                , mockClone = mkChatConnection c
                }