1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
|
{-# LANGUAGE FlexibleInstances #-}
module Test.QuickCheck.Property where
--------------------------------------------------------------------------
-- imports
import Test.QuickCheck.Gen
import Test.QuickCheck.Arbitrary
import Test.QuickCheck.Text( showErr )
import Test.QuickCheck.Exception
import Test.QuickCheck.State
import Control.Concurrent
( forkIO
, threadDelay
, killThread
, newEmptyMVar
, takeMVar
, putMVar
)
import Data.IORef
import System.IO
( hFlush
, stdout
)
--------------------------------------------------------------------------
-- fixeties
infixr 0 ==>
infixr 1 .&.
-- infixr 1 .&&.
--------------------------------------------------------------------------
-- * Property and Testable types
type Property = Gen Prop
-- | The class of things which can be tested, i.e. turned into a property.
class Testable prop where
property :: prop -> Property
instance Testable () where
property _ = property rejected
instance Testable Bool where
property = property . liftBool
instance Testable Result where
property = return . MkProp . return . return
instance Testable Prop where
property = return
instance Testable prop => Testable (Gen prop) where
property mp = do p <- mp; property p
instance (Arbitrary a, Show a, Testable prop) => Testable (a -> prop) where
property f = forAllShrink arbitrary shrink f
--------------------------------------------------------------------------
-- ** Type Prop
-- is this the right level to be abstract at?
newtype Prop = MkProp{ unProp :: Rose (IO Result) }
-- ** type Rose
data Rose a = MkRose a [Rose a]
join :: Rose (Rose a) -> Rose a
join (MkRose ~(MkRose x ts) tts) =
-- first shrinks outer quantification; makes most sense
MkRose x (map join tts ++ ts)
-- first shrinks inner quantification
--MkRose x (ts ++ map join tts)
instance Functor Rose where
fmap f ~(MkRose x rs) = MkRose (f x) [ fmap f r | r <- rs ]
instance Monad Rose where
return x = MkRose x []
m >>= k = join (fmap k m)
protectRose :: Rose (IO Result) -> IO (Rose (IO Result))
protectRose rose = either (return . return . exception result) id `fmap` tryEvaluate (unpack rose)
where unpack (MkRose mres ts) = MkRose (protectResult mres) ts
-- ** Result type
-- | Different kinds of callbacks
data Callback
= PostTest (State -> Result -> IO ()) -- ^ Called just after a test
| PostFinalFailure (State -> Result -> IO ()) -- ^ Called with the final failing test-case
-- | The result of a single test.
data Result
= MkResult
{ ok :: Maybe Bool -- ^ result of the test case; Nothing = discard
, expect :: Bool -- ^ indicates what the expected result of the property is
, reason :: String -- ^ a message indicating what went wrong
, interrupted :: Bool -- ^ indicates if the test case was cancelled by pressing ^C
, stamp :: [(String,Int)] -- ^ the collected values for this test case
, callbacks :: [Callback] -- ^ the callbacks for this test case
}
result :: Result
result =
MkResult
{ ok = undefined
, expect = True
, reason = ""
, interrupted = False
, stamp = []
, callbacks = []
}
failed :: Result -> Result
failed res = res{ ok = Just False }
exception res err = failed res{ reason = "Exception: '" ++ showErr err ++ "'",
interrupted = isInterrupt err }
protectResult :: IO Result -> IO Result
protectResult m = either (exception result) id `fmap` tryEvaluateIO (fmap force m)
where force res = ok res == Just False `seq` res
succeeded :: Result
succeeded = result{ ok = Just True }
rejected :: Result
rejected = result{ ok = Nothing }
--------------------------------------------------------------------------
-- ** Lifting and mapping functions
liftBool :: Bool -> Property
liftBool b = liftResult $
result
{ ok = Just b
, reason = if b then "" else "Falsifiable"
}
liftResult :: Result -> Property
liftResult r = liftIOResult (return r)
liftIOResult :: IO Result -> Property
liftIOResult m = liftRoseIOResult (return m)
liftRoseIOResult :: Rose (IO Result) -> Property
liftRoseIOResult t = return (MkProp t)
mapResult :: Testable prop => (Result -> Result) -> prop -> Property
mapResult f = mapIOResult (fmap f)
mapIOResult :: Testable prop => (IO Result -> IO Result) -> prop -> Property
mapIOResult f = mapRoseIOResult (fmap (f . protectResult))
mapRoseIOResult :: Testable prop => (Rose (IO Result) -> Rose (IO Result)) -> prop -> Property
mapRoseIOResult f = mapProp (\(MkProp t) -> MkProp (f t))
mapProp :: Testable prop => (Prop -> Prop) -> prop -> Property
mapProp f = fmap f . property
--------------------------------------------------------------------------
-- ** Property combinators
-- | Changes the maximum test case size for a property.
mapSize :: Testable prop => (Int -> Int) -> prop -> Property
mapSize f p = sized ((`resize` property p) . f)
-- | Shrinks the argument to property if it fails. Shrinking is done
-- automatically for most types. This is only needed weh you want to
-- override the default behavior.
shrinking :: Testable prop =>
(a -> [a]) -- ^ 'shrink'-like function.
-> a -- ^ The original argument
-> (a -> prop) -> Property
shrinking shrink x pf = fmap (MkProp . join . fmap unProp) (promote (props x))
where
props x =
MkRose (property (pf x)) [ props x' | x' <- shrink x ]
-- | Disables shrinking for a property altogether.
noShrinking :: Testable prop => prop -> Property
noShrinking = mapRoseIOResult f
where f (MkRose mres ts) = MkRose mres []
-- | Adds a callback
callback :: Testable prop => Callback -> prop -> Property
callback cb = mapResult (\res -> res{ callbacks = cb : callbacks res })
-- | Performs an 'IO' action after the last failure of a property.
whenFail :: Testable prop => IO () -> prop -> Property
whenFail m =
callback $ PostFinalFailure $ \st res ->
m
-- | Performs an 'IO' action every time a property fails. Thus,
-- if shrinking is done, this can be used to keep track of the
-- failures along the way.
whenFail' :: Testable prop => IO () -> prop -> Property
whenFail' m =
callback $ PostTest $ \st res ->
if ok res == Just False
then m
else return ()
-- | Modifies a property so that it is expected to fail for some test cases.
expectFailure :: Testable prop => prop -> Property
expectFailure = mapResult (\res -> res{ expect = False })
-- | Attaches a label to a property. This is used for reporting
-- test case distribution.
label :: Testable prop => String -> prop -> Property
label s = classify True s
-- | Labels a property with a value:
--
-- > collect x = label (show x)
collect :: (Show a, Testable prop) => a -> prop -> Property
collect x = label (show x)
-- | Conditionally labels test case.
classify :: Testable prop =>
Bool -- ^ @True@ if the test case should be labelled.
-> String -- ^ Label.
-> prop -> Property
classify b s = cover b 0 s
-- | Checks that at least the given proportion of the test cases belong
-- to the given class.
cover :: Testable prop =>
Bool -- ^ @True@ if the test case belongs to the class.
-> Int -- ^ The required percentage (0-100) of test cases.
-> String -- ^ Label for the test case class.
-> prop -> Property
cover b n s = mapResult $ \res ->
case b of
True -> res{ stamp = (s,n) : stamp res }
False -> res
-- | Implication for properties: The resulting property holds if
-- the first argument is 'False', or if the given property holds.
(==>) :: Testable prop => Bool -> prop -> Property
False ==> _ = property ()
True ==> p = property p
-- | Considers a property failed if it does not complete within
-- the given number of microseconds.
within :: Testable prop => Int -> prop -> Property
within n = mapIOResult race
where
race ior =
do put "Race starts ..."
resV <- newEmptyMVar
let waitAndFail =
do put "Waiting ..."
threadDelay n
put "Done waiting!"
putMVar resV (failed result{reason = "Time out"})
evalProp =
do put "Evaluating Result ..."
res <- protectResult ior
put "Evaluating OK ..."
putMVar resV res
pid1 <- forkIO evalProp
pid2 <- forkIO waitAndFail
put "Blocking ..."
res <- takeMVar resV
put "Killing threads ..."
killThread pid1
killThread pid2
put ("Got Result: " ++ show (ok res))
return res
put s | True = do return ()
| otherwise = do putStrLn s
hFlush stdout
-- | Explicit universal quantification: uses an explicitly given
-- test case generator.
forAll :: (Show a, Testable prop)
=> Gen a -> (a -> prop) -> Property
forAll gen pf =
gen >>= \x ->
whenFail (putStrLn (show x)) $
property (pf x)
-- | Like 'forAll', but tries to shrink the argument for failing test cases.
forAllShrink :: (Show a, Testable prop)
=> Gen a -> (a -> [a]) -> (a -> prop) -> Property
forAllShrink gen shrink pf =
gen >>= \x ->
shrinking shrink x $ \x' ->
whenFail (putStrLn (show x')) $
property (pf x')
(.&.) :: (Testable prop1, Testable prop2) => prop1 -> prop2 -> Property
p1 .&. p2 =
arbitrary >>= \b ->
whenFail (putStrLn (if b then "LHS" else "RHS")) $
if b then property p1 else property p2
{-
-- TODO
(.&&.) :: (Testable prop1, Testable prop2) => prop1 -> prop2 -> Property
p1 .&&. p2 = error "not implemented yet"
-}
--------------------------------------------------------------------------
-- the end.
|