Source

quickcheck-properties / Test / QuickCheck / Property / Common.hs

Full commit
{-# LANGUAGE TypeFamilies      #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE FlexibleInstances #-}
-- | 
-- /Fixing type/
--
-- All properties in this library are polymorphic. For example
-- property for checking associativity of 'mappend' could have
-- following type:
--
-- > prop_mappend :: (Eq a, Monoid a) => a -> a -> a -> Bool
--
-- But if one tries to pass this expression to 'quickCheck' GHC will
-- rightfully complain that type is too generic. Indeed there is no
-- way to figure out what is type of a. Obvious way to fix type of @a@
-- is to add type signature. However it's too cumbersome to write
-- signature for 3 parameter function. 
--
-- Another approach was taken instead. All properties take dummy
-- parameter which fix type:
--
-- > prop_Mappend :: (Eq a, Monoid a) => T a -> a -> a -> a -> Bool
--
-- 'T' is phanom typed unit. It ensures that only type information
-- could be passed to function. For example test invokation could look
-- like this:
--
-- > quickCheck $ prop_Mappend (T :: T [Int])
--
-- By convention all user supplied parameters are placed before T and
-- all quickcheck supplied parameters are after T.
--
-- /Comparing for equality/
--
-- A lot of QuickCheck properties have form @expression = another
-- expression@. It's natural to compare them for equality however not
-- all types have 'Eq' instance. Functions for example and hence many
-- monads.
--
-- There are three generic ways to compare values for equality.
--
--  (1) Use '==' operator
--
--  2. Convert value to some type with Eq instance and compare
--     them. Caller must ensure that such conversion make sence
--
--  3. Most generic: use custom comparison function.
--
-- Functions 'eq', 'eqOn' and 'eqWith' provide this functionality.
--
-- Additionally properties of same type could be composed using
-- boolean expressions. 
module Test.QuickCheck.Property.Common (
    -- * Comparison for equality
    Equal(..)
  , appEqual
  , Equalable(..)
    -- ** Compose properties
  , (.==.)
  , (.&&.)
  , (.||.)
  , notE
    -- ** Convert to QuickCheck properies
  , eq
  , eqOn
  , eqWith
    -- * Utils
  , T(..)
    -- * Examples
    -- $examples
  ) where

import Data.Function (on)


-- | Values to be compared for equality
data Equal a = Equal a a
             | NotE (Equal a)
             | AndE (Equal a) (Equal a)
             | OrE  (Equal a) (Equal a)

appEqual :: (a -> a -> Bool) -> Equal a -> Bool
appEqual f (Equal a b) = f a b
appEqual f (NotE e)    = not $ appEqual f e
appEqual f (AndE e g)  = appEqual f e && appEqual f g
appEqual f (OrE  e g)  = appEqual f e && appEqual f g

-- | Recurse through function to apply comparison to 'Equal'.
class Equalable a where
  -- | Type which should be compared for equality
  type Result a :: *
  -- | Result of comparison. Could be passed to 'quickCheck'
  type Compared a :: *
  -- | Compare value using custom comparison function
  equalWith :: (Result a -> Result a -> Bool) -> a -> Compared a
  -- | Map property 
  mapEqual  :: (Equal (Result a) -> Equal (Result a)) -> a -> a
  -- | Zip properties
  zipEquals :: (Equal (Result a) -> Equal (Result a) -> Equal (Result a)) -> a -> a -> a

-- | Convenience sinonym for 'Equal'. Delay comparison for equality
(.==.) :: a -> a -> Equal a
(.==.) = Equal

-- | Both properties are true.
(.&&.) :: Equalable a => a -> a -> a
(.&&.) = zipEquals AndE

-- | One of properties is true
(.||.) :: Equalable a => a -> a -> a
(.||.) = zipEquals OrE

-- | Property is false
notE :: Equalable a => a -> a
notE = mapEqual NotE

instance Equalable (Equal a) where
  type Result   (Equal a) = a
  type Compared (Equal a) = Bool
  equalWith = appEqual
  mapEqual  = id
  zipEquals = id

instance Equalable a => Equalable (x -> a) where
  type Result   (x -> a) = Result a
  type Compared (x -> a) = x -> Compared a
  equalWith f fun = equalWith f . fun
  mapEqual  f fun = mapEqual  f . fun
  zipEquals f fun1 fun2 = \x -> zipEquals f (fun1 x) (fun2 x)

-- | Compare values using @==@ 
eq :: (Equalable a, Eq (Result a)) => a -> Compared a
eq = equalWith (==)

-- | Convert values to types which could be compare
eqOn :: (Equalable a, Eq b) => (Result a -> b) -> a -> Compared a
eqOn f = equalWith ((==) `on` f)

-- | Compare with custom function. Just a shorter sinonym for equalWith
eqWith :: (Equalable a) => (Result a -> Result a -> Bool) -> a -> Compared a
eqWith = equalWith


-- | Data type is used to fix concrete data in properties
data T a = T

-- $examples
-- 
-- Some example invocation of tests
--
-- Testing monoid laws
--
-- >>> quickCheck $ eq $ prop_Monoid (T :: T [Int])
-- +++ OK, passed 100 tests.
-- >>> quickCheck $ eq $ prop_Monoid (T :: T (Maybe [Int]))
-- +++ OK, passed 100 tests.
-- 
-- Testing functor laws
--
-- >>> quickCheck $ eq $ prop_FunctorCompose (+2) (+199) (T :: T [Int])
-- +++ OK, passed 100 tests.