22{-# LANGUAGE LambdaCase #-}
33{-# LANGUAGE ScopedTypeVariables #-}
44{-# LANGUAGE TupleSections #-}
5+ {-# LANGUAGE TypeApplications #-}
56
67-- |
78-- Copyright: © 2018-2020 IOHK
1314module Test.Hspec.Extra
1415 ( aroundAll
1516 , it
17+ , itWithCustomTimeout
1618 ) where
1719
1820import Prelude
1921
22+ import Control.Concurrent
23+ ( threadDelay )
2024import Control.Concurrent.Async
21- ( async , race , wait )
25+ ( AsyncCancelled , async , race , race_ , wait )
2226import Control.Concurrent.MVar
2327 ( MVar , newEmptyMVar , putMVar , takeMVar )
28+ import Control.Exception
29+ ( fromException )
2430import Control.Exception
2531 ( SomeException , throwIO , try )
32+ import Data.Maybe
33+ ( isJust )
2634import Test.Hspec
2735 ( ActionWith
2836 , HasCallStack
@@ -31,6 +39,7 @@ import Test.Hspec
3139 , afterAll
3240 , beforeAll
3341 , beforeWith
42+ , expectationFailure
3443 , specify
3544 )
3645
@@ -104,17 +113,39 @@ aroundAll acquire =
104113-- | A drop-in replacement for 'it' that'll automatically retry a scenario once
105114-- if it fails, to cope with potentially flaky tests.
106115it :: HasCallStack => String -> ActionWith ctx -> SpecWith ctx
107- it title action = specify title $ \ ctx -> do
108- res1 <- try $ action ctx
109- case res1 of
110- Right r1 -> return r1
111- Left (e1 :: SomeException ) -> do
112- res2 <- try $ action ctx
113- case res2 of
114- -- If the second try fails, return the first error. The
115- -- second error might not be helpful.
116- Left (_e2 :: SomeException ) -> throwIO e1
117- Right r2 -> return r2
116+ it = itWithCustomTimeout (10 * minute)
117+ where
118+ minute = 60 * 1000 * 1000
119+
120+ -- | Like @it@ but with a custom timeout, which makes it realistic to test.
121+ itWithCustomTimeout :: HasCallStack => Int -> String -> ActionWith ctx -> SpecWith ctx
122+ itWithCustomTimeout micro title action = specify title $ \ ctx -> do
123+ race_ timeout $ do
124+ res1 <- try $ action ctx
125+ case res1 of
126+ Right r1 -> return r1
127+ Left (e1 :: SomeException )
128+ -- If we time out during the first attempt, then we /should/ time
129+ -- out.
130+ | isAsyncCancelled e1 -> throwIO e1
131+ | otherwise -> do
132+ res2 <- try $ action ctx
133+ case res2 of
134+ -- If the second try fails, return the first error. The
135+ -- second error might not be helpful.
136+ Left (e2 :: SomeException )
137+ | isAsyncCancelled e2 -> throwIO e1
138+ | otherwise -> throwIO e1
139+ Right r2 -> return r2
140+ where
141+ isAsyncCancelled = isJust . fromException @ AsyncCancelled
142+ timeout = do
143+ threadDelay micro
144+ putStrLn " -------- timing out "
145+ expectationFailure
146+ $ " timed out in "
147+ <> show (micro `div` 1000000 )
148+ <> " seconds"
118149
119150-- | Some helper to help readability on the thread synchronization above.
120151await :: MVar () -> IO ()
0 commit comments