{-# LANGUAGE DeriveDataTypeable, BangPatterns #-}
module Test.Tasty.HUnit.Steps (testCaseSteps) where

import Control.Applicative
import Control.Exception
import Data.IORef
import Data.List (foldl')
import Data.Typeable (Typeable)
import Prelude  -- Silence AMP import warnings
import Test.Tasty.HUnit.Orig
import Test.Tasty.Providers
import Test.Tasty.Runners (getTime)
import Text.Printf (printf)

newtype TestCaseSteps = TestCaseSteps ((String -> IO ()) -> Assertion)
  deriving Typeable

instance IsTest TestCaseSteps where
  run :: OptionSet -> TestCaseSteps -> (Progress -> IO ()) -> IO Result
run OptionSet
_ (TestCaseSteps (String -> IO ()) -> IO ()
assertionFn) Progress -> IO ()
_ = do
    IORef [(Time, String)]
ref <- forall a. a -> IO (IORef a)
newIORef []

    let
      stepFn :: String -> IO ()
      stepFn :: String -> IO ()
stepFn String
msg = do
        Time
tme <- IO Time
getTime
        forall a b. IORef a -> (a -> (a, b)) -> IO b
atomicModifyIORef IORef [(Time, String)]
ref (\[(Time, String)]
l -> ((Time
tme,String
msg)forall a. a -> [a] -> [a]
:[(Time, String)]
l, ()))

    Either String ()
hunitResult <- (forall a b. b -> Either a b
Right forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (String -> IO ()) -> IO ()
assertionFn String -> IO ()
stepFn) forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch`
        \(SomeException e
ex) -> forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ forall a b. a -> Either a b
Left (forall e. Exception e => e -> String
displayException e
ex)

    Time
endTime <- IO Time
getTime

    Int
maxMsgLength <- forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' forall a. Ord a => a -> a -> a
max Int
0 forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a -> b) -> [a] -> [b]
map (forall (t :: * -> *) a. Foldable t => t a -> Int
length forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a, b) -> b
snd) forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall a. IORef a -> IO a
readIORef IORef [(Time, String)]
ref

    let msgFormat :: String
msgFormat = String
"%-" forall a. [a] -> [a] -> [a]
++ forall a. Show a => a -> String
show (forall a. Ord a => a -> a -> a
min Int
maxMsgLength Int
62) forall a. [a] -> [a] -> [a]
++ String
"s (%.02fs)"

    [String]
msgs <- forall a b. (a, b) -> b
snd forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl'
      (\(Time
lastTime, [String]
acc) (Time
curTime, String
msg) ->
           let !duration :: Time
duration = Time
lastTime forall a. Num a => a -> a -> a
- Time
curTime
               !msg' :: String
msg' = if Time
duration forall a. Ord a => a -> a -> Bool
>= Time
0.01 then forall r. PrintfType r => String -> r
printf String
msgFormat String
msg Time
duration else String
msg
            in (Time
curTime, String
msg'forall a. a -> [a] -> [a]
:[String]
acc))
      (Time
endTime, [])
        forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall a. IORef a -> IO a
readIORef IORef [(Time, String)]
ref

    forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$
      case Either String ()
hunitResult of

        Right {} -> String -> Result
testPassed ([String] -> String
unlines [String]
msgs)

        Left String
errMsg -> String -> Result
testFailed forall a b. (a -> b) -> a -> b
$
          if forall (t :: * -> *) a. Foldable t => t a -> Bool
null [String]
msgs
            then
              String
errMsg
            else
              -- Indent the error msg w.r.t. step messages
              [String] -> String
unlines forall a b. (a -> b) -> a -> b
$
                [String]
msgs forall a. [a] -> [a] -> [a]
++ forall a b. (a -> b) -> [a] -> [b]
map (String
"  " forall a. [a] -> [a] -> [a]
++) (String -> [String]
lines String
errMsg)

  testOptions :: Tagged TestCaseSteps [OptionDescription]
testOptions = forall (m :: * -> *) a. Monad m => a -> m a
return []

-- | Create a multi-step unit test.
--
-- Example:
--
-- >main = defaultMain $ testCaseSteps "Multi-step test" $ \step -> do
-- >  step "Preparing..."
-- >  -- do something
-- >
-- >  step "Running part 1"
-- >  -- do something
-- >
-- >  step "Running part 2"
-- >  -- do something
-- >  assertFailure "BAM!"
-- >
-- >  step "Running part 3"
-- >  -- do something
--
-- The @step@ calls are mere annotations. They let you see which steps were
-- performed successfully, and which step failed.
--
-- You can think of @step@
-- as 'putStrLn', except 'putStrLn' would mess up the output with the
-- console reporter and get lost with the others.
--
-- For the example above, the output will be
--
-- >Multi-step test: FAIL
-- >  Preparing...
-- >  Running part 1
-- >  Running part 2
-- >    BAM!
-- >
-- >1 out of 1 tests failed (0.00s)
--
-- Note that:
--
-- * Tasty still treats this as a single test, even though it consists of
-- multiple steps.
--
-- * The execution stops after the first failure. When we are looking at
-- a failed test, we know that all /displayed/ steps but the last one were
-- successful, and the last one failed. The steps /after/ the failed one
-- are /not displayed/, since they didn't run.
testCaseSteps :: TestName -> ((String -> IO ()) -> Assertion) -> TestTree
testCaseSteps :: String -> ((String -> IO ()) -> IO ()) -> TestTree
testCaseSteps String
name = forall t. IsTest t => String -> t -> TestTree
singleTest String
name forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((String -> IO ()) -> IO ()) -> TestCaseSteps
TestCaseSteps