packages feed

hdbi-postgresql-1.0.0: testsrc/runtests.hs

{-# LANGUAGE
  OverloadedStrings
, ScopedTypeVariables
, FlexibleInstances
, FlexibleContexts
  #-}

module Runtests where

import Control.Applicative
import Data.AEq
import Data.Decimal
import Data.Fixed
import Data.Int
import Data.List
import Data.Monoid
import Data.Time
import Data.UUID
import Data.Word
import Database.HDBI
import Database.HDBI.PostgreSQL
import System.Environment
import System.Exit
import Test.Framework
import Test.Framework.Providers.HUnit
import Test.Framework.Providers.QuickCheck2
import Test.HUnit ((@?=), Assertion)
import Test.QuickCheck
import Test.QuickCheck.Assertions
import Test.QuickCheck.Instances ()
import qualified Data.ByteString as B
import qualified Data.Set as S
import qualified Data.Text.Lazy as TL
import qualified Test.QuickCheck.Monadic as QM


instance Arbitrary (DecimalRaw Integer) where
  arbitrary = Decimal <$> arbitrary <*> arbitrary

instance Arbitrary UUID where
  arbitrary = fromWords
              <$> arbitrary
              <*> arbitrary
              <*> arbitrary
              <*> arbitrary

runInsertSelect :: (FromSql a, ToSql a) => PostgreConnection -> Query -> a -> IO a
runInsertSelect conn tname val = withTransaction conn $ do
      runRaw conn "drop table if exists table1"
      runRaw conn $ "create table table1 (val " <> tname <> ")"
      run conn "insert into table1 (val) values (?)" [toSql val]
      s <- prepare conn "select val from table1"
      executeRaw s
      (Just [res])<- fetchRow s
      finish s
      return $ fromSql res

runInsertMany :: PostgreConnection -> [String] -> [[SqlValue]] -> IO [[SqlValue]]
runInsertMany conn types values = withTransaction conn $ do
  runRaw conn "drop table if exists table1"
  runRaw conn $ pk $ "create table table1 (" ++ valnames ++ ")"
  s <- prepare conn $ pk $ "insert into table1(" ++ colnames ++ ") values (" ++ (intersperse ',' $ replicate (length names) '?') ++")"
  executeMany s values
  finish s
  s2 <- prepare conn $ pk $ "select " ++ colnames ++ " from table1"
  executeRaw s2
  fetchAllRows s2
  
  where
    pk = Query . TL.pack
    names = map (("val"++) . show) [1..length types]
    colnames = intercalate "," names
    valnames = intercalate "," $ map (\(a, b) -> a ++ " " ++ b) $ zip names types

setsEqual :: PostgreConnection -> [String] -> [[SqlValue]] -> Property
setsEqual conn types values = QM.monadicIO $ do
  ret <- QM.run $ runInsertMany conn types values
  QM.stop $ (S.fromList values) ==? (S.fromList ret)
  
preciseEqual :: (Eq a, Show a, FromSql a, ToSql a) => PostgreConnection -> Query -> a -> Property
preciseEqual conn tname val = QM.monadicIO $ do
  res <- QM.run $ runInsertSelect conn tname val
  QM.stop $ res ?== val


approxEqual :: (Show a, AEq a, FromSql a, ToSql a) => PostgreConnection -> Query -> a -> Property
approxEqual conn tname val = QM.monadicIO $ do
  res <- QM.run $ runInsertSelect conn tname val
  QM.stop $ res ?~== val

genTOD :: Gen TimeOfDay
genTOD = roundTod <$> arbitrary

genLT :: Gen LocalTime
genLT = rnd <$> arbitrary
  where
    rnd x@(LocalTime {localTimeOfDay = t}) = x {localTimeOfDay = roundTod t}

-- | Generate Text without 'NUL' symbols
genText :: Gen TL.Text
genText = TL.filter fltr <$> arbitrary
  where
    fltr '\NUL' = False         -- NULL truncates C string when pass to libpq binding.
    fltr _ = True

genUTC :: Gen UTCTime
genUTC = rnd <$> arbitrary
  where
    rnd x@(UTCTime {utctDayTime = d}) = x {utctDayTime = anyToMicro d}


-- | Strip TimeOfDay to microsecond precision
roundTod :: TimeOfDay -> TimeOfDay
roundTod x@(TimeOfDay {todSec = s}) = x {todSec = anyToMicro s}

anyToMicro :: (Fractional b, Real a) => a -> b
anyToMicro a = fromRational $ toRational ((fromRational $ toRational a) :: Micro)

testG1 :: PostgreConnection -> Test
testG1 c = testGroup "Can insert and select"
           [ testProperty "Decimal" $ \(d :: Decimal) -> preciseEqual c "decimal(400,255)" d
           , testProperty "Int32" $ \(i :: Int32) -> preciseEqual c "integer" i
           , testProperty "Int64" $ \(i :: Int64) -> preciseEqual c "bigint"  i
           , testProperty "Integer" $ \(i :: Integer) -> preciseEqual c "decimal(100,0)" i
           , testProperty "Double" $ \(d :: Double) -> approxEqual c "double precision" d
           , testProperty "Text" $ forAll genText $ \(t :: TL.Text) -> preciseEqual c "text" t
           , testProperty "ByteString" $ \(b :: B.ByteString) -> preciseEqual c "bytea" b
           , testProperty "Bool" $ \(b :: Bool) -> preciseEqual c "boolean" b
           , testProperty "UUID" $ \(u :: UUID) -> preciseEqual c "uuid" u
           , testProperty "BitField" $ \(w :: Word64) -> preciseEqual c "varbit" (BitField w)
           , testProperty "UTCTime" $ forAll genUTC $ \(u :: UTCTime) -> preciseEqual c "timestamp with time zone" u
           , testProperty "Day" $ \(d :: Day) -> preciseEqual c "date" d
           , testProperty "TimeOfDay" $ forAll genTOD $ \(tod :: TimeOfDay) -> preciseEqual c "time" tod
           , testProperty "LocalTime" $ forAll genLT $ \(lt :: LocalTime) -> preciseEqual c "timestamp without time zone" lt
           , testProperty "Null" $ preciseEqual c "integer" SqlNull
           , testProperty "Maybe Integer" $ \(val :: Maybe Integer) -> preciseEqual c "decimal(100,0)" val
           , testProperty "Maybe ByteString" $ \(val :: Maybe B.ByteString) -> preciseEqual c "bytea" val
           , testProperty "Insert many numbers" $ \(x :: [(Integer, Decimal)]) -> setsEqual c
                                                                          ["decimal(100,0)", "decimal(400,255)"]
                                                                          $ map (\(i, d) ->  [toSql i, toSql d]) x
           , testProperty "Insert many text" $ \(x :: [(Maybe Integer, UUID, Maybe B.ByteString)]) -> setsEqual c
                                                                                                      ["decimal(100,0)", "text", "bytea"]
                                                                                                      $ map (\(i, u, b) ->  [toSql i, toSql u, toSql b]) x
           ]

testAffectedRows :: PostgreConnection -> [Int32] -> Property
testAffectedRows c is = QM.monadicIO $ do
  res <- QM.run $ withTransaction c $ do
    runRaw c "drop table if exists table1"
    runRaw c "create table table1 (val bigint)"
    runMany c "insert into table1(val) values (?)" $ map ((:[]) . toSql) is

    s2 <- prepare c "update table1 set val=10"
    executeRaw s2
    res <- affectedRows s2
    finish s2
  
    return res
  QM.stop $ res ?== (genericLength is)
           
testG2 :: PostgreConnection -> Test
testG2 c = testGroup "Auxiliary functions"
           [ testProperty "affectedRows" $ testAffectedRows c ]


stmtStatus :: PostgreConnection -> Assertion
stmtStatus c = do
  runRaw c "drop table table1"
  runRaw c "create table table1 (val bigint)" -- Just for postgre 9
  s <- prepare c "select * from table1"
  statementStatus s >>= (@?= StatementNew)
  executeRaw s
  statementStatus s >>= (@?= StatementExecuted)
  _ <- fetchRow s
  statementStatus s >>= (@?= StatementFetched)
  finish s
  statementStatus s >>= (@?= StatementFinished)
  reset s
  statementStatus s >>= (@?= StatementNew)

inTransactionStatus :: PostgreConnection -> Assertion
inTransactionStatus c = do
  inTransaction c >>= (@?= False)
  withTransaction c $ do
    inTransaction c >>= (@?= True)

connStatusGood :: PostgreConnection -> Assertion
connStatusGood c = connStatus c >>= (@?= ConnOK)

connClone :: PostgreConnection -> Assertion
connClone c = do
  newc <- clone c
  connStatus newc >>= (@?= ConnOK)
  withTransaction newc $ inTransaction c >>= (@?= False)
  withTransaction c $ inTransaction newc >>= (@?= False)
  disconnect newc
  connStatus newc >>= (@?= ConnDisconnected)

checkColumnNames :: PostgreConnection -> Assertion
checkColumnNames c = do
  withTransaction c $ do
    runRaw c "drop table if exists table1"
    runRaw c "create table table1 (val1 bigint, val2 bigint, val3 bigint)"
    s <- prepare c "select val1, val2, val3 from table1"
    executeRaw s
    getColumnNames s >>= (@?= ["val1", "val2", "val3"])
    getColumnsCount s >>= (@?= 3)
    finish s
  
testG3 :: PostgreConnection -> Test
testG3 c = testGroup "Fixed tests"
           [ testCase "Statement status" $ stmtStatus c
           , testCase "inTransaction return right value" $ inTransactionStatus c
           , testCase "Connection status is good" $ connStatusGood c
           , testCase "Connection clone works" $ connClone c
           , testCase "Check driver name" $ hdbiDriverName c @?= "postgresql"
           , testCase "Check transaction support" $ dbTransactionSupport c @?= True
           , testCase "Check right column names" $ checkColumnNames c
           ]

main :: IO ()
main = do
  a <- getArgs
  case a of
    (conn:args) -> do
      c <- connectPostgreSQL $ TL.pack conn
      (flip defaultMainWithArgs) args [ testG1 c
                                      , testG2 c
                                      , testG3 c
                                      ]
      disconnect c

    _ -> do
      mapM_ putStrLn [ "Need at least one argument as connection string"
                     , "the rest will be passed as arguments to test-framework"]
      exitWith $ ExitFailure 1