2017-01-08 07:16:27 +03:00
{- # LANGUAGE GADTs, TypeFamilies # -}
module Test.Hspec.LeanCheck
( prop
2017-01-08 07:46:22 +03:00
, forAll
2017-01-08 07:16:27 +03:00
) where
import Control.Exception
import Data.Bifunctor ( first )
import Data.String ( String )
import GHC.Show as Show ( showsPrec )
import Prologue
import Test.Hspec
import Test.Hspec.Core.Spec
import Test.LeanCheck.Core
data Property where
Property :: IOTestable prop => prop -> Property
2017-01-11 00:03:22 +03:00
-- | Perform an enumerative test of a property using LeanCheck.
--
-- 'prop' will typically be a function of one or more 'Listable' arguments, returning either 'Bool' or 'IO ()' (in the latter case, typically via 'shouldBe' and friends). For example:
--
-- > describe "+" $ do
-- > prop "associativity" $
-- > \ a b c -> a + (b + c) `shouldBe` (a + b :: Int) + c
2017-01-08 07:16:27 +03:00
prop :: ( HasCallStack , IOTestable prop ) => String -> prop -> Spec
prop s = it s . Property
2017-01-08 07:46:22 +03:00
data ForAll a where
ForAll :: IOTestable prop => [ [ a ] ] -> ( a -> prop ) -> ForAll a
2017-01-11 00:03:22 +03:00
-- | Test a property given by an explicit list of tiers rather than a 'Listable' instance. This can be used to e.g. filter input values for which the property does not hold.
--
-- > describe "mean" $ do
-- > prop "≥ the minimum" . forAll (not . null `filterT` tiers) $
-- > \ list -> (mean list :: Int) `shouldSatisfy` (>= min list)
2017-01-08 07:46:22 +03:00
forAll :: IOTestable prop => [ [ a ] ] -> ( a -> prop ) -> ForAll a
forAll = ForAll
2017-01-08 07:16:27 +03:00
instance Example Property where
type Arg Property = ()
evaluateExample ( Property prop ) ( Params _ bound ) _ _ = do
result <- iocounterExample bound prop
case result of
Just messages -> pure $ Fail Nothing ( concat messages )
Nothing -> pure Success
class IOTestable t where
2017-01-11 00:03:22 +03:00
-- 'resultiers', lifted into 'IO'.
2017-01-12 23:51:01 +03:00
ioResultTiers :: t -> [ [ IO ( [ String ] , Bool ) ] ]
2017-01-08 07:16:27 +03:00
instance IOTestable ( IO () ) where
2017-01-12 23:51:01 +03:00
ioResultTiers action = [ [ ( action >> pure ( [] , True ) ) ` catch ` ( \ e -> pure ( [ displayException ( e :: SomeException ) ] , False ) ) ] ]
2017-01-08 07:16:27 +03:00
instance ( IOTestable b , Show a , Listable a ) => IOTestable ( a -> b ) where
2017-01-12 23:51:01 +03:00
ioResultTiers p = ioconcatMapT resultiersFor tiers
where resultiersFor x = fmap ( fmap ( first ( showsPrec 11 x " " : ) ) ) <$> ioResultTiers ( p x )
2017-01-08 07:16:27 +03:00
instance IOTestable Bool where
2017-01-12 23:51:01 +03:00
ioResultTiers p = [ [ pure ( [] , p ) ] ]
2017-01-08 07:16:27 +03:00
2017-01-08 07:46:22 +03:00
instance IOTestable ( ForAll a ) where
2017-01-12 23:51:01 +03:00
ioResultTiers ( ForAll tiers property ) = concatMapT ( ioResultTiers . property ) tiers
2017-01-08 07:46:22 +03:00
2017-01-11 00:03:22 +03:00
-- | 'concatMapT', lifted into 'IO'.
2017-01-08 07:16:27 +03:00
ioconcatMapT :: ( a -> [ [ IO b ] ] ) -> [ [ a ] ] -> [ [ IO b ] ]
ioconcatMapT f = ( >>= ( >>= f ) )
2017-01-11 00:03:22 +03:00
-- | 'counterExamples', lifted into 'IO'.
2017-01-08 07:16:27 +03:00
iocounterExamples :: IOTestable a => Int -> a -> IO [ [ String ] ]
2017-01-12 23:51:01 +03:00
iocounterExamples n = fmap ( fmap fst . filter ( not . snd ) ) . sequenceA . take n . concat . ioResultTiers
2017-01-08 07:16:27 +03:00
2017-01-11 00:03:22 +03:00
-- | 'counterExample', lifted into 'IO'.
2017-01-08 07:16:27 +03:00
iocounterExample :: IOTestable a => Int -> a -> IO ( Maybe [ String ] )
iocounterExample n = fmap listToMaybe . iocounterExamples n