{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
#define NEWTYPEABLE MIN_VERSION_base(4,7,0)
#if NEWTYPEABLE
{-# LANGUAGE Safe #-}
#else
{-# LANGUAGE Trustworthy #-}
#endif
module Test.SmallCheck.Property (
forAll, exists, existsUnique, over, (==>), monadic, changeDepth, changeDepth1,
Property,
PropertySuccess(..), PropertyFailure(..), runProperty, TestQuality(..), Argument, Reason, Depth, Testable(..),
) where
import Test.SmallCheck.Series
import Test.SmallCheck.SeriesMonad
import Test.SmallCheck.Property.Result
import Control.Arrow (first)
import Control.Monad (liftM, mzero)
import Control.Monad.Logic (MonadLogic, runLogicT, ifte, once, msplit, lnot)
import Control.Monad.Reader (Reader, runReader, lift, ask, local, reader)
import Control.Applicative (pure, (<$>), (<$))
import Data.Typeable (Typeable(..))
#if !NEWTYPEABLE
import Data.Typeable (Typeable1, mkTyConApp, mkTyCon3, typeOf)
#endif
newtype Property m = Property { unProperty :: Reader (Env m) (PropertySeries m) }
#if NEWTYPEABLE
deriving Typeable
#endif
data PropertySeries m =
PropertySeries
{ searchExamples :: Series m PropertySuccess
, searchCounterExamples :: Series m PropertyFailure
, searchClosest :: Series m (Property m, [Argument])
}
data Env m =
Env
{ quantification :: Quantification
, testHook :: TestQuality -> m ()
}
data Quantification
= Forall
| Exists
| ExistsUnique
data TestQuality
= GoodTest
| BadTest
deriving (Eq, Ord, Enum, Show)
#if !NEWTYPEABLE
instance Typeable1 m => Typeable (Property m)
where
typeOf _ =
mkTyConApp
(mkTyCon3 "smallcheck" "Test.SmallCheck.Property" "Property")
[typeOf (undefined :: m ())]
#endif
unProp :: Env t -> Property t -> PropertySeries t
unProp q (Property p) = runReader p q
runProperty
:: Monad m
=> Depth
-> (TestQuality -> m ())
-> Property m
-> m (Maybe PropertyFailure)
runProperty depth hook prop =
(\l -> runLogicT l (\x _ -> return $ Just x) (return Nothing)) $
runSeries depth $
searchCounterExamples $
flip runReader (Env Forall hook) $
unProperty prop
atomicProperty :: Series m PropertySuccess -> Series m PropertyFailure -> PropertySeries m
atomicProperty s f =
let prop = PropertySeries s f (pure (Property $ pure prop, []))
in prop
makeAtomic :: Property m -> Property m
makeAtomic (Property prop) =
Property $ flip fmap prop $ \ps ->
atomicProperty (searchExamples ps) (searchCounterExamples ps)
over
:: (Show a, Testable m b)
=> Series m a -> (a -> b) -> Property m
over = testFunction
monadic :: Testable m a => m a -> Property m
monadic a =
Property $ reader $ \env ->
let pair = unProp env . freshContext <$> lift a in
atomicProperty
(searchExamples =<< pair)
(searchCounterExamples =<< pair)
class Monad m => Testable m a where
test :: a -> Property m
instance Monad m => Testable m Bool where
test b = Property $ reader $ \env ->
let
success = do
lift $ testHook env GoodTest
if b then return $ PropertyTrue Nothing else mzero
failure = PropertyFalse Nothing <$ lnot success
in atomicProperty success failure
instance Monad m => Testable m (Either Reason Reason) where
test r = Property $ reader $ \env ->
let
success = do
lift $ testHook env GoodTest
either (const mzero) (pure . PropertyTrue . Just) r
failure = do
lift $ testHook env GoodTest
either (pure . PropertyFalse . Just) (const mzero) r
in atomicProperty success failure
instance (Serial m a, Show a, Testable m b) => Testable m (a->b) where
test = testFunction series
instance (Monad m, m ~ n) => Testable n (Property m) where
test = id
testFunction
:: (Show a, Testable m b)
=> Series m a -> (a -> b) -> Property m
testFunction s f = Property $ reader $ \env ->
let
closest = do
x <- s
(p, args) <- searchClosest $ unProp env $ test $ f x
return (p, show x : args)
in
case quantification env of
Forall -> PropertySeries success failure closest
where
failure = do
x <- s
failure <- searchCounterExamples $ unProp env $ test $ f x
let arg = show x
return $
case failure of
CounterExample args etc -> CounterExample (arg:args) etc
_ -> CounterExample [arg] failure
success = PropertyTrue Nothing <$ lnot failure
Exists -> PropertySeries success failure closest
where
success = do
x <- s
s <- searchExamples $ unProp env $ test $ f x
let arg = show x
return $
case s of
Exist args etc -> Exist (arg:args) etc
_ -> Exist [arg] s
failure = NotExist <$ lnot success
ExistsUnique -> PropertySeries success failure closest
where
search = atMost 2 $ do
(prop, args) <- closest
ex <- once $ searchExamples $ unProp env $ test prop
return (args, ex)
success =
search >>=
\examples ->
case examples of
[(x,s)] -> return $ ExistUnique x s
_ -> mzero
failure =
search >>=
\examples ->
case examples of
[] -> return NotExist
(x1,s1):(x2,s2):_ -> return $ AtLeastTwo x1 s1 x2 s2
_ -> mzero
atMost :: MonadLogic m => Int -> m a -> m [a]
atMost n m
| n <= 0 = return []
| otherwise = do
m' <- msplit m
case m' of
Nothing -> return []
Just (x,rest) ->
(x:) `liftM` atMost (n-1) rest
quantify :: Quantification -> Property m -> Property m
quantify q (Property a) =
makeAtomic $ Property $ local (\env -> env { quantification = q }) a
freshContext :: Testable m a => a -> Property m
freshContext = forAll
forAll :: Testable m a => a -> Property m
forAll = quantify Forall . test
exists :: Testable m a => a -> Property m
exists = quantify Exists . test
existsUnique :: Testable m a => a -> Property m
existsUnique = quantify ExistsUnique . test
infixr 0 ==>
(==>) :: (Testable m c, Testable m a) => c -> a -> Property m
cond ==> prop = Property $ do
env <- ask
let
counterExample = once $ searchCounterExamples $ unProp env' $ freshContext cond
where env' = env { testHook = const $ return () }
consequent = unProp env $ freshContext prop
badTestHook = lift $ testHook env BadTest
success =
ifte counterExample
(\ex -> do
badTestHook
return $ Vacuously ex
)
(searchExamples consequent)
failure =
ifte counterExample
(const $ do
lift $ testHook env BadTest
mzero
)
(searchCounterExamples consequent)
return $ atomicProperty success failure
changeDepth :: Testable m a => (Depth -> Depth) -> a -> Property m
changeDepth modifyDepth a = Property (changeDepthPS <$> unProperty (test a))
where
changeDepthPS (PropertySeries ss sf sc) =
PropertySeries
(localDepth modifyDepth ss)
(localDepth modifyDepth sf)
(first (changeDepth modifyDepth) <$>
localDepth modifyDepth sc)
changeDepth1 :: (Show a, Serial m a, Testable m b) => (Depth -> Depth) -> (a -> b) -> Property m
changeDepth1 modifyDepth = over $ localDepth modifyDepth series