module Test.Transaction
( transactionTests
)
where
import qualified Control.Monad as Monad
import qualified Data.ByteString as BS
import qualified Data.IORef as IORef
import qualified Data.Text as T
import Hedgehog ((===))
import qualified Hedgehog as HH
import qualified Hedgehog.Gen as Gen
import qualified Orville.PostgreSQL as Orville
import qualified Orville.PostgreSQL.Expr as Expr
import qualified Orville.PostgreSQL.OrvilleState as OrvilleState
import qualified Orville.PostgreSQL.Raw.Connection as Conn
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
import qualified Test.Property as Property
import qualified Test.TestTable as TestTable
import qualified Test.Transaction.Util as TransactionUtil
transactionTests :: Orville.ConnectionPool -> Property.Group
transactionTests pool =
Property.group "Transaction" $
[ prop_transactionsWithoutExceptionsCommit pool
, prop_exceptionsLeadToTransactionRollback pool
, prop_savepointsRollbackInnerTransactions pool
, prop_callbacksMadeForTransactionCommit pool
, prop_callbacksMadeForTransactionRollback pool
, prop_usesCustomBeginTransactionSql pool
]
prop_transactionsWithoutExceptionsCommit :: Property.NamedDBProperty
prop_transactionsWithoutExceptionsCommit =
Property.namedDBProperty "Transactions without exceptions perform a commit" $ \pool -> do
nestingLevel <- HH.forAll TransactionUtil.genNestingLevel
tracers <-
HH.evalIO $ do
Conn.withPoolConnection pool $ \connection ->
TestTable.dropAndRecreateTableDef connection tracerTable
Orville.runOrville pool $ do
TransactionUtil.runNestedTransactions nestingLevel $ \_ ->
Monad.void $ Orville.insertEntity tracerTable Tracer
Orville.findEntitiesBy tracerTable mempty
length tracers === nestingLevel
prop_exceptionsLeadToTransactionRollback :: Property.NamedDBProperty
prop_exceptionsLeadToTransactionRollback =
Property.namedDBProperty "Exceptions within transaction blocks execute rollbock" $ \pool -> do
nestingLevel <- HH.forAll TransactionUtil.genNestingLevel
tracers <-
HH.evalIO $ do
Conn.withPoolConnection pool $ \connection ->
TestTable.dropAndRecreateTableDef connection tracerTable
Orville.runOrville pool $ do
TransactionUtil.silentlyHandleTestError $
TransactionUtil.runNestedTransactions nestingLevel $ \level -> do
_ <- Orville.insertEntity tracerTable Tracer
Monad.when (level >= nestingLevel) TransactionUtil.throwTestError
Orville.findEntitiesBy tracerTable mempty
length tracers === 0
prop_savepointsRollbackInnerTransactions :: Property.NamedDBProperty
prop_savepointsRollbackInnerTransactions =
Property.namedDBProperty "Savepoints allow inner transactions to rollback while outer transactions commit" $ \pool -> do
outerNestingLevel <- HH.forAll TransactionUtil.genNestingLevel
innerNestingLevel <- HH.forAll TransactionUtil.genNestingLevel
let
innerActions =
TransactionUtil.runNestedTransactions innerNestingLevel $ \level -> do
_ <- Orville.insertEntity tracerTable Tracer
Monad.when (level >= innerNestingLevel) TransactionUtil.throwTestError
outerActions =
TransactionUtil.runNestedTransactions outerNestingLevel $ \level -> do
_ <- Orville.insertEntity tracerTable Tracer
Monad.when (level >= outerNestingLevel) $
TransactionUtil.silentlyHandleTestError innerActions
tracers <-
HH.evalIO $ do
Conn.withPoolConnection pool $ \connection ->
TestTable.dropAndRecreateTableDef connection tracerTable
Orville.runOrville pool $ do
outerActions
Orville.findEntitiesBy tracerTable mempty
length tracers === outerNestingLevel
prop_callbacksMadeForTransactionCommit :: Property.NamedDBProperty
prop_callbacksMadeForTransactionCommit =
Property.namedDBProperty "Callbacks are delivered for a transaction that is commited" $ \pool -> do
nestingLevel <- HH.forAll TransactionUtil.genNestingLevel
allEvents <-
captureTransactionCallbackEvents pool $
TransactionUtil.runNestedTransactions nestingLevel (\_ -> pure ())
let
expectedEvents =
mkExpectedEventsForNestedActions nestingLevel $ \maybeSavepoint ->
case maybeSavepoint of
Nothing -> (Orville.BeginTransaction, Orville.CommitTransaction)
Just savepoint -> (Orville.NewSavepoint savepoint, Orville.ReleaseSavepoint savepoint)
allEvents === expectedEvents
prop_callbacksMadeForTransactionRollback :: Property.NamedDBProperty
prop_callbacksMadeForTransactionRollback =
Property.namedDBProperty "Callbacks are delivered for a transaction this is rolled back" $ \pool -> do
nestingLevel <- HH.forAll TransactionUtil.genNestingLevel
allEvents <- captureTransactionCallbackEvents pool $
TransactionUtil.runNestedTransactions nestingLevel $ \level ->
Monad.when (level >= nestingLevel) (TransactionUtil.throwTestError)
let
expectedEvents =
mkExpectedEventsForNestedActions nestingLevel $ \maybeSavepoint ->
case maybeSavepoint of
Nothing -> (Orville.BeginTransaction, Orville.RollbackTransaction)
Just savepoint -> (Orville.NewSavepoint savepoint, Orville.RollbackToSavepoint savepoint)
allEvents === expectedEvents
prop_usesCustomBeginTransactionSql :: Property.NamedDBProperty
prop_usesCustomBeginTransactionSql =
Property.namedDBProperty "Uses custom begin transaction sql" $ \pool -> do
customExpr <-
HH.forAllWith (show . RawSql.toExampleBytes) $
Gen.element
[ Expr.beginTransaction Nothing
, Expr.beginTransaction (Just Expr.readOnly)
, Expr.beginTransaction (Just Expr.readWrite)
, Expr.beginTransaction (Just Expr.deferrable)
, Expr.beginTransaction (Just Expr.notDeferrable)
, Expr.beginTransaction (Just (Expr.isolationLevel Expr.serializable))
, Expr.beginTransaction (Just (Expr.isolationLevel Expr.repeatableRead))
, Expr.beginTransaction (Just (Expr.isolationLevel Expr.readCommitted))
, Expr.beginTransaction (Just (Expr.isolationLevel Expr.readUncommitted))
]
sqlTrace <-
captureSqlTrace pool $ do
Orville.localOrvilleState
(Orville.setBeginTransactionExpr customExpr)
(Orville.withTransaction $ pure ())
sqlTrace
=== [ (Orville.OtherQuery, RawSql.toExampleBytes Expr.commit)
, (Orville.OtherQuery, RawSql.toExampleBytes customExpr)
]
captureTransactionCallbackEvents ::
Orville.ConnectionPool ->
Orville.Orville () ->
HH.PropertyT IO [Orville.TransactionEvent]
captureTransactionCallbackEvents pool actions = do
callbackEventsRef <- HH.evalIO $ IORef.newIORef []
let
captureEvent event =
IORef.modifyIORef callbackEventsRef (event :)
addEventCaptureCallback =
Orville.addTransactionCallback captureEvent
HH.evalIO $ do
Orville.runOrville pool $
TransactionUtil.silentlyHandleTestError $
Orville.localOrvilleState addEventCaptureCallback actions
reverse <$> IORef.readIORef callbackEventsRef
mkExpectedEventsForNestedActions ::
Int ->
(Maybe Orville.Savepoint -> (Orville.TransactionEvent, Orville.TransactionEvent)) ->
[Orville.TransactionEvent]
mkExpectedEventsForNestedActions nestingLevel mkEventsForLevel =
let
appendEvents mbSavepoint (befores, afters) =
let
(before, after) = mkEventsForLevel mbSavepoint
in
(before : befores, after : afters)
savepoints =
iterate OrvilleState.nextSavepoint OrvilleState.initialSavepoint
(allBefores, allAfters) =
foldr appendEvents ([], []) $
take nestingLevel (Nothing : map Just savepoints)
in
allBefores ++ reverse allAfters
data Tracer
= Tracer
tracerTable :: Orville.TableDefinition Orville.NoKey Tracer Tracer
tracerTable =
Orville.mkTableDefinitionWithoutKey "tracer" tracerMarshaller
tracerMarshaller :: Orville.SqlMarshaller Tracer Tracer
tracerMarshaller =
const Tracer
<$> Orville.marshallField (const $ T.pack "tracer") (Orville.unboundedTextField "tracer")
captureSqlTrace ::
Orville.ConnectionPool ->
Orville.Orville () ->
HH.PropertyT IO [(Orville.QueryType, BS.ByteString)]
captureSqlTrace pool actions = do
queryTraceRef <- HH.evalIO $ IORef.newIORef []
let
captureQuery :: Orville.QueryType -> RawSql.RawSql -> IO a -> IO a
captureQuery queryType sql action = do
IORef.modifyIORef queryTraceRef ((queryType, RawSql.toExampleBytes sql) :)
action
HH.evalIO $ do
Orville.runOrville pool $
Orville.localOrvilleState
(Orville.addSqlExecutionCallback captureQuery)
actions
IORef.readIORef queryTraceRef