module Test.Hspec.WebDriver.Internal (
session
, runState
, with
, SessionExample(..)
, AbortSessionEx(..)
) where
import Control.Applicative
import Control.Concurrent.MVar
import Control.Monad.Trans.State (state, evalState, execState, execStateT, StateT)
import Data.Traversable (traverse)
import Data.Typeable (Typeable, cast)
import Data.IORef
import System.IO.Unsafe (unsafePerformIO)
import Test.Hspec
#if MIN_VERSION_hspec(1,10,0)
import Test.Hspec.Core hiding (describe, it)
#else
import Test.Hspec.Core hiding (describe, it, hspec)
#endif
import qualified Control.Exception as E
#if MIN_VERSION_hspec(1,10,0)
traverseTree :: Applicative f => (Item -> f Item) -> SpecTree -> f SpecTree
traverseTree f (SpecItem msg i) = SpecItem msg <$> f i
traverseTree f (SpecGroup msg ss) = SpecGroup msg <$> traverse (traverseTree f) ss
#else
traverseTree :: Applicative f => (Item -> f Item) -> SpecTree -> f SpecTree
traverseTree f (SpecItem i) = SpecItem <$> f i
traverseTree f (SpecGroup msg ss) = SpecGroup msg <$> traverse (traverseTree f) ss
#endif
traverseSpec :: Applicative f => (Item -> f Item) -> Spec -> f Spec
traverseSpec f s = fromSpecList <$> traverse (traverseTree f) (runSpecM s)
mapWithCounter :: (Int -> Item -> Item) -> Spec -> Spec
mapWithCounter f s = flip evalState 0 $ traverseSpec go s
where
go item = state $ \cnt -> (f cnt item, cnt+1)
countItems :: Spec -> Int
countItems s = flip execState 0 $ traverseSpec go s
where
go item = state $ \cnt -> (item, cnt+1)
data AbortSessionEx = AbortSessionEx
deriving Typeable
instance E.Exception AbortSessionEx
instance Show AbortSessionEx where
show AbortSessionEx = "Session Aborted"
data SessionTest a = SessionTest (IO () -> IO ()) (a -> IO a)
deriving Typeable
instance Show (SessionTest a) where
show _ = "Test must be contained within a session of matching state type"
instance Typeable a => E.Exception (SessionTest a)
data SessionExample s = SessionExample (s -> IO s)
instance Typeable a => Example (SessionExample a) where
#if MIN_VERSION_hspec(1,10,0)
evaluateExample (SessionExample f) _ act _ = E.throwIO $ SessionTest act f
#else
evaluateExample (SessionExample f) _ act = E.throwIO $ SessionTest act f
#endif
data Session a = Session {
sessionCount :: Int
, sessionMVars :: [MVar (Either E.SomeException a)]
, sessionCreate :: IO a
, sessionClose :: a -> IO ()
}
sessionItem :: Typeable a => Session a -> Int -> Item -> Item
sessionItem sess i item =
#if MIN_VERSION_hspec(1,10,0)
item { itemExample = \p a prog -> runTest $ itemExample item p a prog }
#else
item { itemExample = \p a -> runTest $ itemExample item p a }
#endif
where
open | i == 0 = E.try $ sessionCreate sess
| otherwise = takeMVar $ sessionMVars sess !! i
close ma | i == sessionCount sess 1 = either (const $ return ()) (sessionClose sess) ma
| otherwise = putMVar (sessionMVars sess !! (i+1)) ma
runTest ex = do
ma <- open
mres <- E.try ex
case mres of
Right res -> close ma >> return res
Left (E.SomeException err) -> do
case (ma, cast err) of
(_, Nothing) -> close ma >> E.throwIO err
(Right a, Just (SessionTest act f)) -> do
aborted <- newIORef False
act $ do
mstate <- E.try $ f a
case mstate of
Right st -> close (Right st)
Left serr@(E.SomeException actErr) -> do
case cast actErr of
Just AbortSessionEx -> close (Left serr) >> writeIORef aborted True
Nothing -> close ma >> E.throwIO serr
abrt <- readIORef aborted
return $ if abrt then (Fail "Session Aborted") else Success
(Left err', _) -> close ma >> E.throwIO err'
session :: Typeable s => IO s
-> (s -> IO ())
-> Spec
-> Spec
session create close s = unsafePerformIO $ do
let cnt = countItems s
mvars <- sequence $ take cnt $ repeat newEmptyMVar
let sess = Session cnt mvars create close
return $ mapWithCounter (sessionItem sess) s
runState :: StateT s IO () -> SessionExample s
runState = SessionExample . execStateT
with :: (s -> IO ()) -> SessionExample s
with f = SessionExample $ \a -> f a >> return a