mockcat-0.6.0.0: test/Property/AdditionalProps.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -O0 #-}
module Property.AdditionalProps
( prop_predicate_param_match_counts
, prop_multicase_progression
, prop_runMockT_isolation
, prop_neverApply_unused
, prop_partial_order_duplicates
) where
import Test.QuickCheck
import Test.QuickCheck.Monadic (monadicIO, run, assert)
import Control.Exception (try, SomeException, evaluate)
import Control.Monad (replicateM, replicateM_, forM, forM_)
import Data.List (nub)
import Data.Proxy (Proxy(..))
import Test.MockCat
import Control.Monad.IO.Class (liftIO)
import Test.MockCat.MockT (MockT(..), Definition(..), runMockT, MonadMockDefs(..))
--------------------------------------------------------------------------------
-- 21. PredicateParam property
--------------------------------------------------------------------------------
-- | Property: A predicate Param (expect even) accepts only matching values and
-- the total recorded applications equals count of even inputs attempted.
prop_predicate_param_match_counts :: Property
prop_predicate_param_match_counts = forAll genVals $ \xs -> monadicIO $ do
-- build predicate mock: (Int -> Bool) returning True for all evens
m <- run $ createMock (expect even "even" |> True)
let f = stubFn m
results <- run $ mapM (\x -> try (evaluate (f x)) :: IO (Either SomeException Bool)) xs
let successes = length [ () | Right _ <- results ]
expected = length (filter even xs)
-- sanity: every success came from an even value
assert (all even [ x | (x, Right _) <- zip xs results ])
-- counts should match
run $ m `shouldApplyTimesToAnything` expected
assert (successes == expected)
where
genVals = resize 40 $ listOf (arbitrary :: Gen Int)
--------------------------------------------------------------------------------
-- 22. Multi-case progression property
--------------------------------------------------------------------------------
-- | For identical argument, successive cases yield successive results then
-- saturate at the last value.
prop_multicase_progression :: Property
prop_multicase_progression = forAll genSeq $ \(arg, rs, extra) -> monadicIO $ do
m <- run $ createMock $ cases [ param arg |> r | r <- rs ]
let f = stubFn m
totalCalls = length rs + extra
-- NOTE [GHC9.4 duplicate-call counting]
-- On GHC 9.4 we observed that using @replicateM totalCalls (evaluate (f arg))@
-- can result in only a single side‑effect (application recording) when all
-- of (argument,result) pairs are identical. The optimizer (or full laziness
-- even under -O0) may float the pure expression @f arg@ and share it.
-- We intentionally inject the loop index via a seq so each iteration has a
-- syntactically distinct thunk, ensuring the unsafePerformIO body runs per call.
vals <- run $ forM [1 .. totalCalls] $ \i -> evaluate (case i of { _ -> f arg })
let (prefix, suffix) = splitAt (length rs) vals
assert (prefix == rs)
assert (all (== last rs) suffix)
run $ m `shouldApplyTimesToAnything` totalCalls
where
genSeq = do
arg <- arbitrary :: Gen Int
Positive len <- arbitrary :: Gen (Positive Int)
let capped = 1 + (len `mod` 5) -- keep small (1..5)
rs <- vectorOf capped (arbitrary :: Gen Int)
extra <- chooseInt (0,3)
pure (arg, rs, extra)
--------------------------------------------------------------------------------
-- 23. runMockT isolation property
--------------------------------------------------------------------------------
-- | Two independent runMockT invocations must not leak application counts.
-- First run uses expectApplyTimes 1; second run expects 0 for a freshly built mock.
prop_runMockT_isolation :: Property
prop_runMockT_isolation = monadicIO $ do
-- Run 1: expect one application
r1 <- run $ try $ runMockT $ do
m <- liftIO $ createMock (param (1 :: Int) |> True)
addDefinition Definition { symbol = Proxy @"iso", mock = m, verify = \m' -> m' `shouldApplyTimesToAnything` 1 }
let f = stubFn m
liftIO $ f 1 `seq` pure ()
case r1 of
Left (_ :: SomeException) -> assert False
Right () -> assert True
-- Run 2: expect zero (if leaked, would see 1 and fail)
r2 <- run $ try $ runMockT $ do
m <- liftIO $ createMock (param (1 :: Int) |> True)
addDefinition Definition { symbol = Proxy @"iso", mock = m, verify = \m' -> m' `shouldApplyTimesToAnything` 0 }
pure ()
case r2 of
Left (_ :: SomeException) -> assert False
Right () -> assert True
--------------------------------------------------------------------------------
-- 24. neverApply unused property
--------------------------------------------------------------------------------
-- | A definition not invoked verifies zero applications while another is used.
prop_neverApply_unused :: Property
prop_neverApply_unused = forAll (chooseInt (0,5)) $ \n -> monadicIO $ do
r <- run $ try $ runMockT $ do
-- used mock
mUsed <- liftIO $ createMock (param (0 :: Int) |> True)
addDefinition Definition { symbol = Proxy @"used", mock = mUsed, verify = \m' -> m' `shouldApplyTimesToAnything` n }
-- unused mock
mUnused <- liftIO $ createMock (param (42 :: Int) |> True)
addDefinition Definition { symbol = Proxy @"unused", mock = mUnused, verify = \m' -> m' `shouldApplyTimesToAnything` 0 }
let f = stubFn mUsed
-- See NOTE [GHC9.4 duplicate-call counting] above: make each application
-- depend on the loop index to avoid sharing; case forces dependence.
liftIO $ forM_ [1 .. n] $ \i -> evaluate (case i of { _ -> f 0 })
case r of
Left (_ :: SomeException) -> assert False
Right () -> assert True
--------------------------------------------------------------------------------
-- 25. Partial order duplicates property
--------------------------------------------------------------------------------
-- | For a sequence with duplicates, the partial order check succeeds with the
-- list of first occurrences; reversing that list (when length >=2) fails.
prop_partial_order_duplicates :: Property
prop_partial_order_duplicates = forAll genDupScript $ \xs -> length xs >= 2 ==> monadicIO $ do
-- build mock over sequence
m <- run $ createMock $ cases [ param x |> True | x <- xs ]
let f = stubFn m
run $ sequence_ [ f x `seq` pure () | x <- xs ]
let uniques = nub xs
-- success case
run $ m `shouldApplyInPartialOrder` (param <$> uniques)
assert True
-- failure case (if we have at least two unique values)
case uniques of
(_:_:_) | isClusterOrdered uniques xs -> do
let reversed = reverse uniques
e <- run $ (try (m `shouldApplyInPartialOrder` (param <$> reversed)) :: IO (Either SomeException ()))
case e of
Left _ -> assert True
Right _ -> assert False
_ -> assert True
where
genDupScript = do
Positive len <- arbitrary :: Gen (Positive Int)
let size = 2 + (len `mod` 6) -- 2..7
base <- vectorOf size (arbitrary :: Gen Int)
-- ensure at least one duplicate by forcing first element copy if all distinct
pure $ ensureDup base
ensureDup ys = if length (nub ys) == length ys && not (null ys) then head ys : ys else ys
-- Check that for each earlier unique value all its indices precede all indices of later unique values.
isClusterOrdered us seqVals = all pairOrdered pairs
where
idxs v = [ i | (i,a) <- zip [0..] seqVals, a == v ]
pairs = [ (u1,u2) | (i,u1) <- zip [0..] us, u2 <- drop (i+1) us ]
pairOrdered (u1,u2) = case (idxs u1, idxs u2) of
([],_) -> True
(_,[]) -> True
(is1,is2) -> maximum is1 < minimum is2