{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Test.Tasty.Bdd
( (@?=)
, (@?/=)
, (^?=)
, (^?/=)
, acquire
, acquirePure
, Phase (..)
, Language (..)
, testBehavior
, testBehaviorIO
, BDDTesting
, BDDPreparing
, TestableMonad (..)
, failFastIngredients
, failFastTester
, prettyDifferences
, beforeEach
, afterEach
, before
, after
, onEach
, captureStdout
, testBehaviorF
)
where
import Control.Monad.Catch
( Exception (..)
, MonadCatch (..)
, MonadThrow (..)
)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Tagged (Tagged (..))
import Data.TreeDiff
import Data.Typeable (Proxy (..), Typeable)
import System.CaptureStdout
import System.IO.Unsafe (unsafePerformIO)
import Test.BDD.Language
import Test.BDD.LanguageFree
import Test.Tasty
( withResource
)
import Test.Tasty.Ingredients.FailFast (FailFast (..), failFast)
import Test.Tasty.Options (OptionDescription (..), lookupOption)
import Test.Tasty.Providers
( IsTest (..)
, singleTest
, testFailed
, testPassed
)
import Test.Tasty.Runners
import Text.Printf (printf)
data FreeBDDCase m = FreeBDDCase (m Result -> IO Result) (m (BDDResult m))
testBehaviorF
:: (Typeable m, MonadCatch m)
=> (m Result -> IO Result)
-> String
-> FreeBDD m x
-> TestTree
testBehaviorF f s = singleTest s . FreeBDDCase f . testFreeBDD
instance (MonadCatch m, Typeable m) => IsTest (FreeBDDCase m) where
run _ (FreeBDDCase rc test) _ = rc $ test >>= g
where
g (Failed e td) = do
td
maybe
(throwM e)
(return . testFailed . testFailMessage)
$ fromException e
g (Succeded td) = td >> return (testPassed "")
testOptions = Tagged [Option (Proxy :: Proxy FailFast)]
class (MonadCatch m, MonadIO m, Monad m, Typeable m) => TestableMonad m where
runCase :: m Result -> IO Result
instance TestableMonad IO where
runCase = id
instance
(Typeable t, TestableMonad m)
=> IsTest (BDDTest m t ())
where
run os (BDDTest ts rup w) f = runCase $ do
teardowns <-
sequence_ . reverse <$> mapM (\(TestContext g a) -> a <$> g) rup
resultOfWhen <- w
let loop [] = return Nothing
loop (then' : xs) = do
liftIO $
f
(Progress
""
(fromIntegral (length xs) / fromIntegral (length ts)))
(then' resultOfWhen >> loop xs)
`catch` (\(EqualityDoesntHold e) -> return (Just e))
resultOfThen <- loop ts
case resultOfThen of
Just reason -> do
case lookupOption os of
FailFast False -> teardowns
_ -> return ()
return $ testFailed reason
Nothing -> teardowns >> return (testPassed "")
testOptions = Tagged [Option (Proxy :: Proxy FailFast)]
prettyDifferences :: (ToExpr a) => a -> a -> String
prettyDifferences a1 a2 =
show $ ansiWlEditExpr $ exprDiff (toExpr a1) (toExpr a2)
newtype EqualityDoesntHold = EqualityDoesntHold {testFailMessage :: String}
deriving (Show, Typeable)
instance Exception EqualityDoesntHold
infixl 4 @?=
(@?=) :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => a -> a -> m ()
a1 @?= a2 =
if a1 == a2
then return ()
else
throwM $
EqualityDoesntHold $
printf "Expected equality:\n%s" $
prettyDifferences a1 a2
(@?/=) :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => a -> a -> m ()
a1 @?/= a2 =
if a1 /= a2
then return ()
else
throwM $
EqualityDoesntHold $
printf "Expected inequality:\n%s" $
prettyDifferences a1 a2
(^?=) :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => m a -> a -> b -> m ()
f ^?= t = const $ f >>= (@?= t)
(^?/=) :: (ToExpr a, Eq a, Typeable a, MonadThrow m) => m a -> a -> b -> m ()
f ^?/= t = const $ f >>= (@?/= t)
testBehavior
:: (MonadIO m, TestableMonad m, Typeable t)
=> String
-> BDDPreparing m t ()
-> TestTree
testBehavior s = singleTest s . interpret
before :: IO () -> TestTree -> TestTree
before f = withResource f return . const
after :: IO () -> TestTree -> TestTree
after f = withResource (return ()) (const f) . const
beforeEach :: IO () -> TestTree -> TestTree
beforeEach = onEach . before
onEach :: (TestTree -> TestTree) -> TestTree -> TestTree
onEach op t@(SingleTest _ _) = op t
onEach op (TestGroup n ts) = TestGroup n $ (map $ onEach op) ts
onEach op (WithResource spec rf) = WithResource spec $ onEach op . rf
onEach op (AskOptions rf) = AskOptions $ onEach op . rf
onEach op (PlusTestOptions g t) = PlusTestOptions g $ onEach op t
afterEach :: IO () -> TestTree -> TestTree
afterEach = onEach . after
acquire :: MonadIO m => IO a -> (m a -> TestTree) -> TestTree
acquire f g = withResource f (const $ return ()) (g . liftIO)
acquirePure :: IO a -> (a -> TestTree) -> TestTree
acquirePure f g = acquire f $ g . unsafePerformIO
testBehaviorIO
:: (Typeable t, MonadIO m, TestableMonad m)
=> String
-> IO (BDDPreparing m t ())
-> TestTree
testBehaviorIO s f = acquirePure f (testBehavior s)
failFastTester :: TestTree -> IO ()
failFastTester = defaultMainWithIngredients failFastIngredients
failFastIngredients :: [Ingredient]
failFastIngredients = [listingTests, failFast consoleTestReporter]