module Test.Hspec.Core.Example (
Example (..)
, Params (..)
, defaultParams
, ActionWith
, Progress
, ProgressCallback
, Result (..)
, Location (..)
, LocationAccuracy (..)
, FailureReason (..)
, safeEvaluateExample
) where
import Data.Maybe (fromMaybe)
import Data.List (isPrefixOf)
import qualified Test.HUnit.Lang as HUnit
#if MIN_VERSION_HUnit(1,4,0)
import Data.CallStack
#endif
import qualified Control.Exception as E
import Control.DeepSeq
import Data.Typeable (Typeable)
import qualified Test.QuickCheck as QC
import Test.Hspec.Expectations (Expectation)
import qualified Test.QuickCheck.State as QC
import qualified Test.QuickCheck.Property as QCP
import Test.Hspec.Core.QuickCheckUtil
import Test.Hspec.Core.Util
import Test.Hspec.Core.Compat
class Example e where
type Arg e
type Arg e = ()
evaluateExample :: e -> Params -> (ActionWith (Arg e) -> IO ()) -> ProgressCallback -> IO Result
data Params = Params {
paramsQuickCheckArgs :: QC.Args
, paramsSmallCheckDepth :: Int
} deriving (Show)
defaultParams :: Params
defaultParams = Params {
paramsQuickCheckArgs = QC.stdArgs
, paramsSmallCheckDepth = 5
}
type Progress = (Int, Int)
type ProgressCallback = Progress -> IO ()
type ActionWith a = a -> IO ()
data Result = Success | Pending (Maybe String) | Failure (Maybe Location) FailureReason
deriving (Eq, Show, Read, Typeable)
data FailureReason = NoReason | Reason String | ExpectedButGot (Maybe String) String String
deriving (Eq, Show, Read, Typeable)
instance NFData FailureReason where
rnf reason = case reason of
NoReason -> ()
Reason r -> r `deepseq` ()
ExpectedButGot p e a -> p `deepseq` e `deepseq` a `deepseq` ()
instance E.Exception Result
data Location = Location {
locationFile :: FilePath
, locationLine :: Int
, locationColumn :: Int
, locationAccuracy :: LocationAccuracy
} deriving (Eq, Show, Read)
data LocationAccuracy =
ExactLocation |
BestEffort
deriving (Eq, Show, Read)
safeEvaluateExample :: Example e => e -> Params -> (ActionWith (Arg e) -> IO ()) -> ProgressCallback -> IO (Either E.SomeException Result)
safeEvaluateExample example params around progress = do
r <- safeTry $ forceResult <$> evaluateExample example params around progress
return $ case r of
Left e | Just result <- E.fromException e -> Right result
Left e | Just hunit <- E.fromException e -> Right (hunitFailureToResult hunit)
_ -> r
where
forceResult :: Result -> Result
forceResult r = case r of
Success -> r
Pending m -> m `deepseq` r
Failure _ m -> m `deepseq` r
instance Example Result where
type Arg Result = ()
evaluateExample e = evaluateExample (\() -> e)
instance Example (a -> Result) where
type Arg (a -> Result) = a
evaluateExample example _params action _callback = do
ref <- newIORef Success
action (writeIORef ref . example)
readIORef ref
instance Example Bool where
type Arg Bool = ()
evaluateExample e = evaluateExample (\() -> e)
instance Example (a -> Bool) where
type Arg (a -> Bool) = a
evaluateExample p _params action _callback = do
ref <- newIORef Success
action $ \a -> example a >>= writeIORef ref
readIORef ref
where
example a
| p a = return Success
| otherwise = return (Failure Nothing NoReason)
instance Example Expectation where
type Arg Expectation = ()
evaluateExample e = evaluateExample (\() -> e)
hunitFailureToResult :: HUnit.HUnitFailure -> Result
hunitFailureToResult e = case e of
#if MIN_VERSION_HUnit(1,3,0)
HUnit.HUnitFailure mLoc err ->
#if MIN_VERSION_HUnit(1,5,0)
case err of
HUnit.Reason reason -> Failure location (Reason reason)
HUnit.ExpectedButGot preface expected actual -> Failure location (ExpectedButGot preface expected actual)
#else
Failure location (Reason err)
#endif
where
location = case mLoc of
Nothing -> Nothing
#if MIN_VERSION_HUnit(1,4,0)
Just loc -> Just $ Location (srcLocFile loc) (srcLocStartLine loc) (srcLocStartCol loc) ExactLocation
#else
Just loc -> Just $ Location (HUnit.locationFile loc) (HUnit.locationLine loc) (HUnit.locationColumn loc) ExactLocation
#endif
#else
HUnit.HUnitFailure err -> Failure Nothing (Reason err)
#endif
instance Example (a -> Expectation) where
type Arg (a -> Expectation) = a
evaluateExample e _ action _ = action e >> return Success
instance Example QC.Property where
type Arg QC.Property = ()
evaluateExample e = evaluateExample (\() -> e)
instance Example (a -> QC.Property) where
type Arg (a -> QC.Property) = a
evaluateExample p c action progressCallback = do
r <- QC.quickCheckWithResult (paramsQuickCheckArgs c) {QC.chatty = False} (QCP.callback qcProgressCallback $ aroundProperty action p)
return $
case r of
QC.Success {} -> Success
QC.Failure {QC.output = m} -> fromMaybe (Failure Nothing . Reason $ sanitizeFailureMessage r) (parsePending m)
QC.GaveUp {QC.numTests = n} -> Failure Nothing (Reason $ "Gave up after " ++ pluralize n "test" )
QC.NoExpectedFailure {} -> Failure Nothing (Reason $ "No expected failure")
#if MIN_VERSION_QuickCheck(2,8,0)
QC.InsufficientCoverage {} -> Failure Nothing (Reason $ "Insufficient coverage")
#endif
where
qcProgressCallback = QCP.PostTest QCP.NotCounterexample $
\st _ -> progressCallback (QC.numSuccessTests st, QC.maxSuccessTests st)
sanitizeFailureMessage :: QC.Result -> String
sanitizeFailureMessage r = let m = QC.output r in strip $
#if MIN_VERSION_QuickCheck(2,7,0)
case QC.theException r of
Just e -> case E.fromException e :: Maybe (HUnit.HUnitFailure) of
Just _ -> (addFalsifiable . stripFailed) m
Nothing -> let numbers = formatNumbers r in
"uncaught exception: " ++ formatException e ++ " " ++ numbers ++ "\n" ++ case lines m of
#if MIN_VERSION_QuickCheck(2,11,0)
x:xs | x == (exceptionPrefix ++ show e ++ "' " ++ numbers ++ ":") -> unlines xs
#else
x:xs | x == (exceptionPrefix ++ show e ++ "' " ++ numbers ++ ": ") -> unlines xs
#endif
_ -> m
Nothing ->
#endif
(addFalsifiable . stripFailed) m
addFalsifiable :: String -> String
addFalsifiable m
| "(after " `isPrefixOf` m = "Falsifiable " ++ m
| otherwise = m
stripFailed :: String -> String
stripFailed m
| prefix `isPrefixOf` m = drop n m
| otherwise = m
where
prefix = "*** Failed! "
n = length prefix
parsePending :: String -> Maybe Result
parsePending m
| exceptionPrefix `isPrefixOf` m = (readMaybe . takeWhile (/= '\'') . drop n) m
| otherwise = Nothing
where
n = length exceptionPrefix
exceptionPrefix = "*** Failed! Exception: '"