tasty-bdd-0.2.0.0: src/Test/BDD/LanguageFree.hs
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{- | Free monads for composing Given/When/Then scenarios.
Module : Test.BDD.LanguageFree
Copyright : (c) Paolo Veronelli 2017
License : BSD-3-Clause
Maintainer: paolo.veronelli@gmail.com
Stability : experimental
Portability: non-portable
-}
module Test.BDD.LanguageFree
( given
, givenAndAfter_
, givenAndAfter
, then_
, then__
, when_
, GivenFree
, ThenFree
, FreeBDD
, testFreeBDD
, BDDResult (..)
)
where
import Control.Monad.Catch
import Control.Monad.Free
import Control.Monad.Reader
-- | Separating the 2 phases by type
data Phase t = Preparing | Testing t
-- | Bare hoare language
data Language m a where
-- | action to prepare the test
Given :: m a -> (a -> Language m 'Preparing) -> Language m 'Preparing
-- | action to prepare the test, and related teardown action
GivenAndAfter
:: m (a, r)
-> (r -> m ())
-> (a -> Language m 'Preparing)
-> Language m 'Preparing
-- | core logic of the test (last preparing action)
When :: m t -> Language m ('Testing t) -> Language m 'Preparing
-- | action producing a test
Then
:: (t -> m ()) -> Language m ('Testing t) -> Language m ('Testing t)
-- | final placeholder
End :: Language m x
And
:: Language m 'Preparing
-> Language m 'Preparing
-> Language m 'Preparing
-- | A scenario result carrying the recorded teardown action.
data BDDResult m = Failed SomeException (m ()) | Succeded (m ())
type CJR m = ReaderT (m ()) m (BDDResult m)
catchCJR :: (MonadCatch m) => CJR m -> CJR m
catchCJR f = catch f $ asks . Failed
stepIn :: (MonadCatch m) => m a -> (a -> CJR m) -> CJR m
stepIn g q = catchCJR (lift g >>= q)
{- | Run a teardown, then the remaining ones even when it throws; the
first exception is rethrown once all have run.
-}
releaseThen :: (MonadCatch m) => m () -> m () -> m ()
releaseThen release rest =
try release >>= \case
Right () -> rest
Left (e :: SomeException) -> do
(_ :: Either SomeException ()) <- try rest
throwM e
interpret
:: forall m. (MonadCatch m) => Language m 'Preparing -> m (BDDResult m)
interpret y = runReaderT (interpret' y) (return ())
where
interpret' :: Language m 'Preparing -> CJR m
interpret' (Given g p) = stepIn g $ interpret' . p
interpret' (GivenAndAfter g z p) =
stepIn g $ \(x, r) -> local (releaseThen $ z r) $ interpret' $ p x
interpret' (When fa p) =
stepIn fa $ \x -> interpretT' x p
interpret' (And f g) = do
r <- interpret' f
case r of
Succeded _ -> interpret' g
w -> pure w
interpret' End = asks Succeded
interpretT' :: t -> Language m ('Testing t) -> CJR m
interpretT' _ End = asks Succeded
interpretT' x (Then f p) =
stepIn (f x) $ \() -> interpretT' x p
-- | Preparation instructions for the free-monad interface.
data GivenFree m a where
GivenFree :: m b -> (b -> a) -> GivenFree m a
GivenAndAfterFree
:: m (b, r) -> (r -> m ()) -> (b -> a) -> GivenFree m a
WhenFree :: m t -> Free (ThenFree m t) c -> a -> GivenFree m a
-- | Assertions that consume the result of a scenario action.
data ThenFree m t a
= ThenFree (t -> m ()) a
deriving (Functor)
instance Functor (GivenFree m) where
fmap f (GivenFree m x) = GivenFree m $ f <$> x
fmap f (GivenAndAfterFree mr rm x) = GivenAndAfterFree mr rm $ f <$> x
fmap f (WhenFree mt ft x) = WhenFree mt ft $ f x
-- | A scenario built from preparation instructions.
type FreeBDD m x = Free (GivenFree m) x
-- | Run a preparation action and return its value to subsequent steps.
given :: m a -> Free (GivenFree m) a
given m = liftF $ GivenFree m id
-- | Acquire a value and resource, recording the resource teardown.
givenAndAfter :: m (b, r) -> (r -> m ()) -> Free (GivenFree m) b
givenAndAfter g td = liftF $ GivenAndAfterFree g td id
-- | Acquire a resource and record its teardown without returning a value.
givenAndAfter_
:: (Functor m) => m r -> (r -> m ()) -> Free (GivenFree m) ()
givenAndAfter_ g td = liftF $ GivenAndAfterFree (((),) <$> g) td id
-- | Run the scenario action and feed its result to the assertions.
when_ :: m t -> Free (ThenFree m t) b -> Free (GivenFree m) ()
when_ mt ts = liftF $ WhenFree mt ts ()
thens :: Free (ThenFree m t) a -> Language m ('Testing t)
thens (Free (ThenFree m f)) = Then m $ thens f
thens (Pure _) = End
bddFree :: Free (GivenFree m) x -> Language m 'Preparing
bddFree (Free (GivenFree m f)) = Given m $ bddFree <$> f
bddFree (Free (GivenAndAfterFree mr rm f)) =
GivenAndAfter mr rm $ bddFree <$> f
bddFree (Free (WhenFree mt ts f)) = And (When mt $ thens ts) (bddFree f)
bddFree (Pure _) = End
-- | Add an assertion using the scenario action result.
then_ :: (t -> m ()) -> Free (ThenFree m t) ()
then_ m = liftF $ ThenFree m ()
-- | Add an assertion independent of the scenario action result.
then__ :: m () -> Free (ThenFree m t) ()
then__ = then_ . const
-- | Execute a scenario and return its result with recorded teardown.
testFreeBDD
:: (MonadCatch m)
=> Free (GivenFree m) x
-> m (BDDResult m)
testFreeBDD = interpret . bddFree