mockcat-1.0.0.0: test/Property/AdditionalProps.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -O0 #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# OPTIONS_GHC -Wno-type-defaults #-}
module Property.AdditionalProps
( prop_predicate_param_match_counts
, prop_multicase_progression
, prop_runMockT_isolation
, prop_partial_order_duplicates
) where
import Test.QuickCheck
import Test.QuickCheck.Monadic (monadicIO, run, assert)
import Control.Exception (try, SomeException, evaluate)
import Control.Monad (forM, forM_)
import Data.List (nub)
import Data.Proxy (Proxy(..))
import Test.MockCat
import Control.Monad.IO.Class (liftIO)
perCall :: Int -> a -> a
perCall i x = case i of
_ -> x
--------------------------------------------------------------------------------
-- 21. PredicateParam property
--------------------------------------------------------------------------------
-- | Property: A predicate Param (expect even) accepts only matching values and
-- the total recorded calls 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
f <- run $ mock (expect even "even" ~> True)
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 $ f `shouldBeCalled` times 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
f <- run $ mock $ cases [ param arg ~> r | r <- rs ]
let 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 (call 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 (perCall i (f arg))
let (prefix, suffix) = splitAt (length rs) vals
assert (prefix == rs)
assert (all (== last rs) suffix)
run $ f `shouldBeCalled` times 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 call counts.
-- The first run enforces a single invocation; the second expects zero for a fresh mock.
prop_runMockT_isolation :: Property
prop_runMockT_isolation = monadicIO $ do
-- Run 1: expect one call
r1 <- run $ try $ runMockT $ do
f <- liftIO $ mock (param (1 :: Int) ~> True)
addDefinition Definition { symbol = Proxy @"iso", mockFunction = f, verification = Verification (\m' -> m' `shouldBeCalled` times 1) }
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
f <- liftIO $ mock (param (1 :: Int) ~> True)
addDefinition Definition { symbol = Proxy @"iso", mockFunction = f, verification = Verification (\m' -> m' `shouldBeCalled` times 0) }
pure ()
case r2 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
f <- run $ mock $ cases [ param x ~> True | x <- xs ]
run $ forM_ xs $ \x -> f x `seq` pure ()
let uniques = nub xs
-- success case
run $ f `shouldBeCalled` inPartialOrderWith (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 (f `shouldBeCalled` inPartialOrderWith (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