-- vim:fdm=marker:foldtext=foldtext() -------------------------------------------------------------------- -- | -- Module : Test.SmallCheck.Property -- Copyright : (c) Colin Runciman et al. -- License : BSD3 -- Maintainer: Roman Cheplyaka -- -- Properties and tools to construct them. -------------------------------------------------------------------- {-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, TypeFamilies, ScopedTypeVariables, DeriveDataTypeable #-} -- CPP is for Typeable1 vs Typeable {-# LANGUAGE CPP #-} -- Are we using new, polykinded and derivable Typeable yet? #define NEWTYPEABLE MIN_VERSION_base(4,7,0) #if NEWTYPEABLE {-# LANGUAGE Safe #-} #else -- Trustworthy is needed because of the hand-written Typeable instance {-# LANGUAGE Trustworthy #-} #endif module Test.SmallCheck.Property ( -- * Constructors forAll, exists, existsUnique, over, (==>), monadic, changeDepth, changeDepth1, -- * Property's entrails 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.Monad import Control.Monad.Logic import Control.Monad.Reader import Control.Applicative import Data.Typeable ------------------------------ -- Property-related types ------------------------------ --{{{ -- | The type of properties over the monad @m@ 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 -- Typeable here is not polykinded yet, and also GHC doesn't know how to -- derive this. instance Typeable1 m => Typeable (Property m) where typeOf _ = mkTyConApp (mkTyCon3 "smallcheck" "Test.SmallCheck.Property" "Property") [typeOf (undefined :: m ())] #endif -- }}} ------------------------------------ -- Property runners and constructors ------------------------------------ --{{{ 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' s $ \\x -> p x@ makes @x@ range over the 'Series' @s@ (by -- default, all variables range over the 'series' for their types). -- -- Note that, unlike the quantification operators, this affects only the -- variable following the operator and not subsequent variables. -- -- 'over' does not affect the quantification context. over :: (Show a, Testable m b) => Series m a -> (a -> b) -> Property m over = testFunction -- | Execute a monadic test 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) -- }}} ------------------------------- -- Testable class and instances ------------------------------- -- {{{ -- | Class of tests that can be run in a monad. For pure tests, it is -- recommended to keep their types polymorphic in @m@ rather than -- specialising it to 'Identity'. 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 -- | Works like the 'Bool' instance, but includes an explanation of the result. -- -- 'Left' and 'Right' correspond to test failure and success -- respectively. 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 -- }}} ------------------------------ -- Test constructors ------------------------------ -- {{{ 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 -- | Set the universal quantification context forAll :: Testable m a => a -> Property m forAll = quantify Forall . test -- | Set the existential quantification context exists :: Testable m a => a -> Property m exists = quantify Exists . test -- | Set the uniqueness quantification context. -- -- Bear in mind that ∃! (x, y): p x y is not the same as ∃! x: ∃! y: p x y. -- -- For example, ∃! x: ∃! y: |x| = |y| is true (it holds only when x=0), but ∃! (x,y): |x| = |y| is false (there are many such pairs). -- -- As is customary in mathematics, -- @'existsUnique' $ \\x y -> p x y@ is equivalent to -- @'existsUnique' $ \\(x,y) -> p x y@ and not to -- @'existsUnique' $ \\x -> 'existsUnique' $ \\y -> p x y@ -- (the latter, of course, may be explicitly written when desired). -- -- That is, all the variables affected by the same uniqueness context are -- quantified simultaneously as a tuple. existsUnique :: Testable m a => a -> Property m existsUnique = quantify ExistsUnique . test -- | The '==>' operator can be used to express a restricting condition -- under which a property should hold. It corresponds to implication in the -- classical logic. -- -- Note that '==>' resets the quantification context for its operands to -- the default (universal). 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 -- NB: we do not invoke the test hook in the antecedent where env' = env { testHook = const $ return () } consequent = unProp env $ freshContext prop badTestHook = lift $ testHook env BadTest success = ifte counterExample -- then (\ex -> do badTestHook return $ Vacuously ex ) -- else (searchExamples consequent) failure = ifte counterExample -- then (const $ do lift $ testHook env BadTest mzero ) -- else (searchCounterExamples consequent) return $ atomicProperty success failure -- | Run property with a modified depth. Affects all quantified variables -- in the property. 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) ((\(prop, args) -> (changeDepth modifyDepth prop, args)) <$> localDepth modifyDepth sc) -- | Quantify the function's argument over its 'series', but adjust the -- depth. This doesn't affect any subsequent variables. changeDepth1 :: (Show a, Serial m a, Testable m b) => (Depth -> Depth) -> (a -> b) -> Property m changeDepth1 modifyDepth = over $ localDepth modifyDepth series -- }}}