packages feed

HMock 0.2.0.0 → 0.3.0.0

raw patch · 15 files changed

+375/−133 lines, 15 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Test.HMock.Internal.Rule: JustMatcher :: Matcher cls name m r -> WholeMethodMatcher cls name m r
+ Test.HMock.Internal.Rule: SuchThat :: Matcher cls name m r -> (Action cls name m r -> Bool) -> WholeMethodMatcher cls name m r
+ Test.HMock.Internal.Rule: data WholeMethodMatcher cls name m r
+ Test.HMock.Internal.Rule: showWholeMatcher :: MockableBase cls => Maybe (Action cls name m a) -> WholeMethodMatcher cls name m b -> String
+ Test.HMock.Internal.State: [mockAllowUnexpected] :: MockState m -> TVar [Step m]
+ Test.HMock.Internal.State: [mockSideEffects] :: MockState m -> TVar [Step m]
+ Test.HMock.MockT: whenever :: forall cls name m r ctx. (MonadIO m, MockableMethod cls name m r, MockContext ctx) => Rule cls name m r -> ctx m ()
+ Test.HMock.Rule: SuchThat :: Matcher cls name m r -> (Action cls name m r -> Bool) -> WholeMethodMatcher cls name m r
+ Test.HMock.Rule: data WholeMethodMatcher cls name m r
+ Test.HMock.Rule: instance Test.HMock.Rule.Expectable cls name m r (Test.HMock.Internal.Rule.WholeMethodMatcher cls name m r)
- Test.HMock.Internal.Rule: [:=>] :: Matcher cls name m r -> [Action cls name m r -> MockT m r] -> Rule cls name m r
+ Test.HMock.Internal.Rule: [:=>] :: WholeMethodMatcher cls name m r -> [Action cls name m r -> MockT m r] -> Rule cls name m r
- Test.HMock.Internal.State: MockState :: TVar (ExpectSet (Step m)) -> TVar [(Bool, Step m)] -> TVar Bool -> TVar (Set TypeRep) -> Maybe (MockState m) -> MockState m
+ Test.HMock.Internal.State: MockState :: TVar (ExpectSet (Step m)) -> TVar [Step m] -> TVar [Step m] -> TVar [Step m] -> TVar Bool -> TVar (Set TypeRep) -> Maybe (MockState m) -> MockState m
- Test.HMock.Internal.State: [mockDefaults] :: MockState m -> TVar [(Bool, Step m)]
+ Test.HMock.Internal.State: [mockDefaults] :: MockState m -> TVar [Step m]
- Test.HMock.Internal.Step: [:->] :: Matcher cls name m r -> Maybe (Action cls name m r -> MockT m r) -> SingleRule cls name m r
+ Test.HMock.Internal.Step: [:->] :: WholeMethodMatcher cls name m r -> Maybe (Action cls name m r -> MockT m r) -> SingleRule cls name m r
- Test.HMock.Internal.TH: splitType :: Type -> ([Name], Cxt, [Type])
+ Test.HMock.Internal.TH: splitType :: Type -> ([Name], Cxt, [Type], Type)

Files

CHANGELOG.md view
@@ -1,5 +1,13 @@ # Revision history for hmock +## 0.3.0.0 -- 2021-06-30++* Methods with polymorphic return types can now be mocked if the return type has+  a `Typeable` constraint.+* Added `whenever` to associate a side effect to a method.+* Added `WholeMethodMatcher` to match entire method args at once.+* `allowUnexpected` no longer changes the default for expected calls.+ ## 0.2.0.0 -- 2021-06-24  * Added ambiguity checking.
HMock.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               HMock-version:            0.2.0.0+version:            0.3.0.0 synopsis:           A flexible mock framework for testing effectful code. description:        HMock is a flexible mock framework for testing effectful                     code in Haskell.  Tests can set up expectations about
src/Test/HMock/Internal/Rule.hs view
@@ -11,6 +11,23 @@ import {-# SOURCE #-} Test.HMock.Internal.State (MockT) import Test.HMock.Mockable (MockableBase (..)) +-- | A way to match an entire action, using conditions that might depend on the+-- relationship between arguments.+data WholeMethodMatcher cls name m r+  = JustMatcher (Matcher cls name m r)+  | Matcher cls name m r `SuchThat` (Action cls name m r -> Bool)++-- | Displays a WholeMethodMatcher.  The predicate isn't showable, but we can at+-- least indicate whether there is one present.+showWholeMatcher ::+  MockableBase cls =>+  Maybe (Action cls name m a) ->+  WholeMethodMatcher cls name m b ->+  String+showWholeMatcher a (JustMatcher m) = showMatcher a m+showWholeMatcher a (m `SuchThat` _) =+  showMatcher a m ++ " (with whole method matcher)"+ -- | A rule for matching a method and responding to it when it matches. -- -- The method may be matched by providing either an 'Action' to match exactly,@@ -40,6 +57,6 @@     (r :: Type)   where   (:=>) ::-    Matcher cls name m r ->+    WholeMethodMatcher cls name m r ->     [Action cls name m r -> MockT m r] ->     Rule cls name m r
src/Test/HMock/Internal/State.hs view
@@ -49,7 +49,9 @@ -- | Full state of a mock. data MockState m = MockState   { mockExpectSet :: TVar (ExpectSet (Step m)),-    mockDefaults :: TVar [(Bool, Step m)],+    mockDefaults :: TVar [Step m],+    mockAllowUnexpected :: TVar [Step m],+    mockSideEffects :: TVar [Step m],     mockCheckAmbiguity :: TVar Bool,     mockClasses :: TVar (Set TypeRep),     mockParent :: Maybe (MockState m)@@ -61,6 +63,8 @@ initMockState parent =   MockState     <$> newTVarIO ExpectNothing+    <*> newTVarIO []+    <*> newTVarIO []     <*> newTVarIO []     <*> maybeM       (newTVarIO False)
src/Test/HMock/Internal/Step.hs view
@@ -12,7 +12,11 @@ import GHC.TypeLits (Symbol) import Test.HMock.ExpectContext (ExpectContext (..), MockableMethod) import Test.HMock.Internal.ExpectSet (ExpectSet (..))-import Test.HMock.Internal.Rule (Rule (..))+import Test.HMock.Internal.Rule+  ( Rule (..),+    WholeMethodMatcher (..),+    showWholeMatcher,+  ) import {-# SOURCE #-} Test.HMock.Internal.State (MockT) import Test.HMock.Internal.Util (Located (..), locate, withLoc) import Test.HMock.Mockable (MockableBase (..))@@ -34,7 +38,7 @@     (r :: Type)   where   (:->) ::-    Matcher cls name m r ->+    WholeMethodMatcher cls name m r ->     Maybe (Action cls name m r -> MockT m r) ->     SingleRule cls name m r @@ -47,7 +51,7 @@  instance Show (Step m) where   show (Step l@(Loc _ (m :-> _))) =-    withLoc (showMatcher Nothing m <$ l)+    withLoc (showWholeMatcher Nothing m <$ l)  -- | Expands a Rule into an expectation.  The expected multiplicity will be one -- if there are no responses; otherwise one call is expected per response.
src/Test/HMock/Internal/TH.hs view
@@ -72,16 +72,15 @@     subst (VarT x) | Just t <- lookup x classVars = t     subst t = t --- | Splits a function type into a list of bound type vars, context, and--- parameters and return value.  The return value is the last element of the--- list.-splitType :: Type -> ([Name], Cxt, [Type])+-- | Splits a function type into a list of bound type vars, context, parameter+-- types, and return value type.+splitType :: Type -> ([Name], Cxt, [Type], Type) splitType (ForallT tv cx b) =-  let (tvs, cxs, parts) = splitType b-   in (map tvName tv ++ tvs, cx ++ cxs, parts)+  let (tvs, cxs, params, retval) = splitType b+   in (map tvName tv ++ tvs, cx ++ cxs, params, retval) splitType (AppT (AppT ArrowT a) b) =-  let (tvs, cx, parts) = splitType b in (tvs, cx, a : parts)-splitType r = ([], [], [r])+  let (tvs, cx, params, retval) = splitType b in (tvs, cx, a : params, retval)+splitType r = ([], [], [], r)  -- | Gets all free type variable 'Name's in the given 'Type'. freeTypeVars :: Type -> [Name]
src/Test/HMock/MockMethod.hs view
@@ -11,7 +11,7 @@ where  import Control.Concurrent.STM (TVar, readTVar, writeTVar)-import Control.Monad (forM, join)+import Control.Monad (forM, forM_, join, void) import Control.Monad.Extra (concatMapM) import Control.Monad.IO.Class (MonadIO) import Control.Monad.Reader (ask)@@ -21,12 +21,13 @@ import Data.Function (on) import Data.Functor (($>)) import Data.List (intercalate, sortBy)-import Data.Maybe (catMaybes, fromMaybe, isNothing)+import Data.Maybe (catMaybes) import Data.Proxy (Proxy (Proxy)) import Data.Typeable (cast) import GHC.Stack (HasCallStack, withFrozenCallStack) import Test.HMock.ExpectContext (MockableMethod) import Test.HMock.Internal.ExpectSet (ExpectSet, liveSteps)+import Test.HMock.Internal.Rule (WholeMethodMatcher (..), showWholeMatcher) import Test.HMock.Internal.State   ( MockContext (..),     MockSetup (..),@@ -40,8 +41,19 @@ import Test.HMock.Internal.Util (Located (Loc), withLoc) import Test.HMock.MockT (describeExpectations) import Test.HMock.Mockable (MatchResult (..), Mockable (..), MockableBase (..))-import Control.Applicative ((<|>)) +matchWholeAction ::+  MockableBase cls =>+  WholeMethodMatcher cls name m a ->+  Action cls name m a ->+  MatchResult+matchWholeAction (JustMatcher m) a = matchAction m a+matchWholeAction (m `SuchThat` p) a = case matchAction m a of+  NoMatch n -> NoMatch n+  Match+    | p a -> Match+    | otherwise -> NoMatch 0+ -- | Implements mock delegation for actions. mockMethodImpl ::   forall cls name m r.@@ -61,20 +73,30 @@             (tryMatch (mockExpectSet state) <$> liveSteps expectSet)     let orderedPartial = snd <$> sortBy (compare `on` fst) (catMaybes partial)     defaults <- concatMapM (mockSetupSTM . readTVar . mockDefaults) states+    unexpected <-+      concatMapM+        (mockSetupSTM . readTVar . mockAllowUnexpected)+        states+    sideEffect <-+      getSideEffect+        <$> concatMapM (mockSetupSTM . readTVar . mockSideEffects) states     checkAmbig <- mockSetupSTM $ readTVar . mockCheckAmbiguity . head $ states-    case (full, orderedPartial, findDefault defaults) of-      (opts@(_ : _ : _), _, _)+    case ( full,+           orderedPartial,+           allowedUnexpected unexpected,+           findDefault defaults+         ) of+      (opts@(_ : _ : _), _, _, _)         | checkAmbig ->-          return $-            ambiguityError-              action-              ((\(s, _, _) -> s) <$> opts)-      ((_, choose, Just response) : _, _, _) -> choose >> return response-      ((_, choose, Nothing) : _, _, (_, d)) -> choose >> return d-      ([], _, (True, d)) -> return d-      ([], [], _) -> return (noMatchError action)-      ([], _, _) ->-        return (partialMatchError action orderedPartial)+          return $ ambiguityError action ((\(s, _, _) -> s) <$> opts)+      ((_, choose, Just response) : _, _, _, _) ->+        choose >> return (sideEffect >> response)+      ((_, choose, Nothing) : _, _, _, d) ->+        choose >> return (sideEffect >> d)+      ([], _, Just (Just resp), _) -> return (sideEffect >> resp)+      ([], _, Just Nothing, d) -> return (sideEffect >> d)+      ([], [], _, _) -> return (noMatchError action)+      ([], _, _, _) -> return (partialMatchError action orderedPartial)   where     tryMatch ::       TVar (ExpectSet (Step m)) ->@@ -84,27 +106,39 @@         (String, MockSetup m (), Maybe (MockT m r))     tryMatch tvar (Step expected, e)       | Just lrule@(Loc _ (m :-> impl)) <- cast expected =-        case matchAction m action of+        case matchWholeAction m action of           NoMatch n ->-            Left (Just (n, withLoc (showMatcher (Just action) m <$ lrule)))+            Left (Just (n, withLoc (showWholeMatcher (Just action) m <$ lrule)))           Match ->             Right-              ( withLoc (lrule $> showMatcher (Just action) m),+              ( withLoc (lrule $> showWholeMatcher (Just action) m),                 mockSetupSTM $ writeTVar tvar e,                 ($ action) <$> impl               )       | otherwise = Left Nothing -    findDefault :: [(Bool, Step m)] -> (Bool, MockT m r)-    findDefault defaults = go False Nothing defaults-      where go True (Just r) _ = (True, r)-            go allowed r ((thisAllowed, Step expected) : steps)-              | thisAllowed || isNothing r,-                Just (Loc _ (m :-> r')) <- cast expected,-                Match <- matchAction m action =-                  go (allowed || thisAllowed) (r <|> (($ action) <$> r')) steps-              | otherwise = go allowed r steps-            go allowed r [] = (allowed, fromMaybe (return surrogate) r)+    allowedUnexpected :: [Step m] -> Maybe (Maybe (MockT m r))+    allowedUnexpected [] = Nothing+    allowedUnexpected (Step unexpected : steps)+      | Just (Loc _ (m :-> impl)) <- cast unexpected,+        Match <- matchWholeAction m action =+        Just (($ action) <$> impl)+      | otherwise = allowedUnexpected steps++    findDefault :: [Step m] -> MockT m r+    findDefault [] = return surrogate+    findDefault (Step expected : steps)+      | Just (Loc _ (m :-> impl)) <- cast expected,+        Match <- matchWholeAction m action =+        maybe (findDefault steps) ($ action) impl+      | otherwise = findDefault steps++    getSideEffect :: [Step m] -> MockT m ()+    getSideEffect effects =+      forM_ effects $ \(Step expected) -> case cast expected of+        Just (Loc _ (m :-> Just impl))+          | Match <- matchWholeAction m action -> void (impl action)+        _ -> return ()  -- | Implements a method in a 'Mockable' monad by delegating to the mock -- framework.  If the method is called unexpectedly, an exception will be
src/Test/HMock/MockT.hs view
@@ -24,6 +24,7 @@     MockContext,     allowUnexpected,     byDefault,+    whenever,   ) where @@ -33,6 +34,7 @@   ) import Control.Monad.Trans (lift) import Data.List (intercalate)+import Data.Maybe (listToMaybe) import Data.Proxy (Proxy (Proxy)) import GHC.Stack (callStack) import Test.HMock.ExpectContext (MockableMethod)@@ -43,7 +45,6 @@ import Test.HMock.Internal.Util (locate) import Test.HMock.Rule (Expectable (toRule)) import UnliftIO-import Data.Maybe (listToMaybe)  -- | Runs a test in the 'MockT' monad, handling all of the mocks. runMockT :: forall m a. MonadIO m => MockT m a -> m a@@ -182,8 +183,8 @@     state <- MockSetup ask     mockSetupSTM $       modifyTVar'-        (mockDefaults state)-        ((True, Step (locate callStack (m :-> listToMaybe r))) :)+        (mockAllowUnexpected state)+        (Step (locate callStack (m :-> listToMaybe r)) :)  -- | Sets a default action for *expected* matching calls.  The new default only -- applies to calls for which an expectation exists, but it lacks an explicit@@ -202,5 +203,29 @@   mockSetupSTM $     modifyTVar'       (mockDefaults state)-      ((False, Step (locate callStack (m :-> Just r))) :)+      (Step (locate callStack (m :-> Just r)) :) byDefault _ = error "Defaults must have exactly one response."++-- | Adds a side-effect, which happens whenever a matching call occurs, in+-- addition to the usual response.  The return value is entirely ignored.+--+-- Be warned: using side effects makes it easy to break abstraction boundaries.+-- Be aware that there may be other uses of a method besides the one which you+-- intend to intercept here.  If possible, add the desired behavior to the+-- response for the matching expectation instead.+whenever ::+  forall cls name m r ctx.+  ( MonadIO m,+    MockableMethod cls name m r,+    MockContext ctx+  ) =>+  Rule cls name m r ->+  ctx m ()+whenever (m :=> [r]) = fromMockSetup $ do+  initClassIfNeeded (Proxy :: Proxy cls)+  state <- MockSetup ask+  mockSetupSTM $+    modifyTVar'+      (mockSideEffects state)+      (Step (locate callStack (m :-> Just r)) :)+whenever _ = error "Side effects must have exactly one response."
src/Test/HMock/Multiplicity.hs view
@@ -21,22 +21,23 @@ data Multiplicity = Multiplicity Int (Maybe Int) deriving (Eq)  instance Show Multiplicity where-  show mult = go (normalize mult) where-    go m | not (feasible m) = "infeasible"-    go (Multiplicity 0 (Just 0)) = "never"-    go (Multiplicity 1 (Just 1)) = "once"-    go (Multiplicity 2 (Just 2)) = "twice"-    go (Multiplicity 0 Nothing) = "any number of times"-    go (Multiplicity 1 Nothing) = "at least once"-    go (Multiplicity 2 Nothing) = "at least twice"-    go (Multiplicity n Nothing) = "at least " ++ show n ++ " times"-    go (Multiplicity 0 (Just 1)) = "at most once"-    go (Multiplicity 0 (Just 2)) = "at most twice"-    go (Multiplicity 0 (Just n)) = "at most " ++ show n ++ " times"-    go (Multiplicity m (Just n))-      | m == n = show n ++ " times"-      | m == n - 1 = show m ++ " or " ++ show n ++ " times"-      | otherwise = show m ++ " to " ++ show n ++ " times"+  show mult = go (normalize mult)+    where+      go m | not (feasible m) = "infeasible"+      go (Multiplicity 0 (Just 0)) = "never"+      go (Multiplicity 1 (Just 1)) = "once"+      go (Multiplicity 2 (Just 2)) = "twice"+      go (Multiplicity 0 Nothing) = "any number of times"+      go (Multiplicity 1 Nothing) = "at least once"+      go (Multiplicity 2 Nothing) = "at least twice"+      go (Multiplicity n Nothing) = "at least " ++ show n ++ " times"+      go (Multiplicity 0 (Just 1)) = "at most once"+      go (Multiplicity 0 (Just 2)) = "at most twice"+      go (Multiplicity 0 (Just n)) = "at most " ++ show n ++ " times"+      go (Multiplicity m (Just n))+        | m == n = show n ++ " times"+        | m == n - 1 = show m ++ " or " ++ show n ++ " times"+        | otherwise = show m ++ " to " ++ show n ++ " times"  -- | A 'Multiplicity' value representing inconsistent expectations. infeasible :: Multiplicity@@ -70,12 +71,12 @@   (*) = error "Multiplicities are not closed under multiplication"    abs = id--  signum (Multiplicity 0 (Just 0)) = 0-  signum _ = 1+  signum = id  normalize :: Multiplicity -> Multiplicity-normalize (Multiplicity a b) = Multiplicity (max a 0) b+normalize m@(Multiplicity a b)+  | not (feasible m) = infeasible+  | otherwise = Multiplicity (max a 0) b  -- | Checks whether a certain number satisfies the 'Multiplicity'. meetsMultiplicity :: Multiplicity -> Int -> Bool@@ -114,7 +115,7 @@ -- >>> meetsMultiplicity (atLeast 2) 3 -- True atLeast :: Multiplicity -> Multiplicity-atLeast (Multiplicity n _) = Multiplicity n Nothing+atLeast (Multiplicity n _) = normalize $ Multiplicity n Nothing  -- | A 'Multiplicity' that means at most this many times. --@@ -125,7 +126,7 @@ -- >>> meetsMultiplicity (atMost 2) 3 -- False atMost :: Multiplicity -> Multiplicity-atMost (Multiplicity _ n) = Multiplicity 0 n+atMost (Multiplicity _ n) = normalize $ Multiplicity 0 n  -- | A 'Multiplicity' that means any number in this interval, endpoints -- included.  For example, @'between' 2 3@ means 2 or 3 times, while@@ -140,7 +141,7 @@ -- >>> meetsMultiplicity (between 2 3) 4 -- False between :: Multiplicity -> Multiplicity -> Multiplicity-between (Multiplicity m _) (Multiplicity _ n) = Multiplicity m n+between (Multiplicity m _) (Multiplicity _ n) = normalize $ Multiplicity m n  -- | Checks whether a 'Multiplicity' is capable of matching any number at all. --@@ -151,4 +152,4 @@ -- >>> feasible (once - 2) -- False feasible :: Multiplicity -> Bool-feasible (Multiplicity a b) = maybe True (>= a) b+feasible (Multiplicity a b) = maybe True (>= max 0 a) b
src/Test/HMock/Rule.hs view
@@ -6,9 +6,16 @@ -- matching methods.  The 'Expectable' type class generalizes 'Rule', so that -- you can specify a bare 'Matcher' or 'Action' in most situations where a -- 'Rule' is needed but you don't want to specify the response.-module Test.HMock.Rule (Rule, Expectable (..), (|->), (|=>)) where+module Test.HMock.Rule+  ( Rule,+    Expectable (..),+    (|->),+    (|=>),+    WholeMethodMatcher (SuchThat),+  )+where -import Test.HMock.Internal.Rule (Rule (..))+import Test.HMock.Internal.Rule (Rule (..), WholeMethodMatcher (..)) import {-# SOURCE #-} Test.HMock.Internal.State (MockT) import Test.HMock.Mockable (MockableBase (Action, Matcher)) @@ -46,4 +53,7 @@   toRule = id  instance Expectable cls name m r (Matcher cls name m r) where+  toRule m = JustMatcher m :=> []++instance Expectable cls name m r (WholeMethodMatcher cls name m r) where   toRule m = m :=> []
src/Test/HMock/TH.hs view
@@ -1,8 +1,11 @@ {-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-} {-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeOperators #-}  -- | This module provides Template Haskell splices that can be used to derive -- boilerplate instances for HMock.@@ -53,12 +56,12 @@ import Data.List (foldl', (\\)) import Data.Typeable (Typeable) import GHC.Stack (HasCallStack)-import GHC.TypeLits (Symbol)+import GHC.TypeLits (ErrorMessage (Text, (:$$:), (:<>:)), Symbol, TypeError) import Language.Haskell.TH hiding (Match, match) import Language.Haskell.TH.Syntax (Lift (lift))+import Test.HMock.Internal.State (MockT) import Test.HMock.Internal.TH-import Test.HMock.MockT (MockT)-import Test.HMock.MockMethod (mockMethod, mockDefaultlessMethod)+import Test.HMock.MockMethod (mockDefaultlessMethod, mockMethod) import Test.HMock.Mockable (MatchResult (..), Mockable, MockableBase (..)) import Test.HMock.Predicates (Predicate (..), eq) import Test.HMock.Rule (Expectable (..))@@ -320,49 +323,62 @@         instExtraMembers = extraMembers       }   where-    memberOrMethod :: Dec -> Either String Method -> Q (Either Dec Method)-    memberOrMethod dec (Left warning) = do-      when (mockVerbose options) $ reportWarning warning+    memberOrMethod :: Dec -> Either [String] Method -> Q (Either Dec Method)+    memberOrMethod dec (Left warnings) = do+      when (mockVerbose options) $ mapM_ reportWarning warnings       return (Left dec)     memberOrMethod _ (Right method) = return (Right method) -getMethod :: Type -> Name -> [(Name, Type)] -> Dec -> Q (Either String Method)+getMethod :: Type -> Name -> [(Name, Type)] -> Dec -> Q (Either [String] Method) getMethod instTy m tbl (SigD name ty) = do   simpleTy <- localizeMember instTy m (substTypeVars tbl ty)+  let (tvs, cx, args, mretval) = splitType simpleTy   return $ do-    let (tvs, cx, argsAndReturn) = splitType simpleTy-    (m', result) <- case last argsAndReturn of-      AppT (VarT m') result -> return (m', result)+    retval <- case mretval of+      AppT (VarT m') retval | m' == m -> return retval       _ ->-        Left $-          nameBase name-            ++ " can't be mocked: non-monadic result."-    when (m' /= m) $-      Left $-        nameBase name-          ++ " can't be mocked: return value in wrong monad."-    when (relevantContext result (tvs, cx) /= ([], [])) $-      Left $-        nameBase name-          ++ " can't be mocked: polymorphic return value."-    let argTypes =-          map-            (substTypeVar m (AppT (ConT ''MockT) (VarT m)))-            (init argsAndReturn)+        Left+          [ nameBase name+              ++ " can't be mocked: return value not in the expected monad."+          ]+    unless (all (isVarTypeable cx) (filter (`elem` tvs) (freeTypeVars retval))) $+      Left+        [ nameBase name+            ++ " can't be mocked: return value not Typeable."+        ]+    let argTypes = map (substTypeVar m (AppT (ConT ''MockT) (VarT m))) args     when (any hasNestedPolyType argTypes) $-      Left $-        nameBase name-          ++ " can't be mocked: rank-n types nested in arguments."+      Left+        [ nameBase name+            ++ " can't be mocked: rank-n types nested in arguments."+        ]+     return $       Method         { methodName = name,           methodTyVars = tvs,           methodCxt = cx,           methodArgs = argTypes,-          methodResult = result+          methodResult = retval         }-getMethod _ _ _ _ = return (Left "A non-value member cannot be mocked.")+  where+    isVarTypeable :: Cxt -> Name -> Bool+    isVarTypeable cx v = AppT (ConT ''Typeable) (VarT v) `elem` cx +getMethod _ _ _ (DataD _ name _ _ _ _) =+  return (Left [nameBase name ++ " must be defined in manual MockT instance."])+getMethod _ _ _ (NewtypeD _ name _ _ _ _) =+  return (Left [nameBase name ++ " must be defined in manual MockT instance."])+getMethod _ _ _ (TySynD name _ _) =+  return (Left [nameBase name ++ " must be defined in manual MockT instance."])+getMethod _ _ _ (DataFamilyD name _ _) =+  return (Left [nameBase name ++ " must be defined in manual MockT instance."])+getMethod _ _ _ (OpenTypeFamilyD (TypeFamilyHead name _ _ _)) =+  return (Left [nameBase name ++ " must be defined in manual MockT instance."])+getMethod _ _ _ (ClosedTypeFamilyD (TypeFamilyHead name _ _ _) _) =+  return (Left [nameBase name ++ " must be defined in manual MockT instance."])+getMethod _ _ _ _ = return (Left [])+ isKnownType :: Method -> Type -> Bool isKnownType method ty = null tyVars && null cx   where@@ -598,33 +614,56 @@  defineExpectableActions :: MockableOptions -> Instance -> Q [Dec] defineExpectableActions options inst =-  concatMapM (defineExpectableAction options inst) (instMethods inst)+  mapM (defineExpectableAction options inst) (instMethods inst) -defineExpectableAction :: MockableOptions -> Instance -> Method -> Q [Dec]+type ComplexExpectableMessage name =+  ( 'Text "Method " ':<>: 'Text name+      ':<>: 'Text " is too complex to expect with an Action."+  )+    ':$$: 'Text "Suggested fix: Use a Matcher instead of an Action."++defineExpectableAction :: MockableOptions -> Instance -> Method -> Q Dec defineExpectableAction options inst method = do   maybeCxt <- wholeCxt (methodArgs method)   argVars <- replicateM (length (methodArgs method)) (newName "a")   case maybeCxt of     Just cx -> do-      (: [])-        <$> instanceD-          (pure (methodCxt method ++ cx))-          ( appT-              (withMethodParams inst method [t|Expectable|])-              (withMethodParams inst method [t|Action|])-          )-          [ funD-              'toRule-              [ clause-                  [conP (getActionName options method) (map varP argVars)]-                  ( normalB $-                      let matcherCon = conE (getMatcherName options method)-                       in appE (varE 'toRule) (makeBody argVars matcherCon)-                  )-                  []-              ]-          ]-    _ -> pure []+      instanceD+        (pure (methodCxt method ++ cx))+        ( appT+            (withMethodParams inst method [t|Expectable|])+            (withMethodParams inst method [t|Action|])+        )+        [ funD+            'toRule+            [ clause+                [conP (getActionName options method) (map varP argVars)]+                ( normalB $+                    let matcherCon = conE (getMatcherName options method)+                     in appE (varE 'toRule) (makeBody argVars matcherCon)+                )+                []+            ]+        ]+    _ -> do+      checkExt UndecidableInstances+      instanceD+        ( (: [])+            <$> [t|+              TypeError+                ( ComplexExpectableMessage+                    $(litT $ strTyLit $ nameBase $ methodName method)+                )+              |]+        )+        ( appT+            (withMethodParams inst method [t|Expectable|])+            (withMethodParams inst method [t|Action|])+        )+        [ funD+            'toRule+            [clause [] (normalB [|undefined|]) []]+        ]   where     makeBody [] e = e     makeBody (v : vs) e = makeBody vs [|$e (eq $(varE v))|]
test/Classes.hs view
@@ -190,8 +190,6 @@   it "returns the customized default value" $ do     example $       runMockT $ do-        expectAny WithSetup-         result <- withSetup         liftIO (result `shouldBe` "custom default") @@ -489,11 +487,35 @@       success       failure `shouldThrow` anyException +class MonadPolyResult m where+  polyResult :: Typeable a => a -> m a+  nonTypeablePolyResult :: a -> m a++deriveMockable ''MonadPolyResult++instance (MonadIO m, Typeable m) => MonadPolyResult (MockT m) where+  polyResult x = mockDefaultlessMethod (PolyResult x)+  nonTypeablePolyResult x = return x++polyResultTests :: SpecWith ()+polyResultTests = describe "MonadPolyResult" $ do+  it "generates mock impl" $+    example $ do+      decs <- runMockT $ do+        $(onReify [|expectAny|] ''MonadPolyResult)+        runQ (deriveMockable ''MonadPolyResult)+      evaluate (rnf decs)++  it "is mockable" $+    example . runMockT $ do+      expect $ PolyResult_ anything |=> \(PolyResult a) -> return (a :: Int)+      x <- polyResult (3 :: Int)+      liftIO $ x `shouldBe` 3+ class MonadExtraneousMembers m where   data SomeDataType m   favoriteNumber :: SomeDataType m -> Int   wrongMonad :: Monad n => m Int -> n Int-  polyResult :: a -> m a   nestedRankN :: ((forall a. a -> Bool) -> Bool) -> m ()    mockableMethod :: Int -> m ()@@ -504,7 +526,6 @@   data SomeDataType (MockT m) = SomeCon   favoriteNumber SomeCon = 42   wrongMonad _ = return 42-  polyResult = return   nestedRankN _ = return ()    mockableMethod a = mockMethod (MockableMethod a)@@ -522,15 +543,22 @@     example $ do       decs <- runMockT $ do         $(onReify [|expectAny|] ''MonadExtraneousMembers)-        expect $ QReport False "A non-value member cannot be mocked."         expect $-          QReport False "favoriteNumber can't be mocked: non-monadic result."+          QReport+            False+            "SomeDataType must be defined in manual MockT instance."         expect $           QReport             False-            "wrongMonad can't be mocked: return value in wrong monad."+            ( "favoriteNumber can't be mocked: "+                ++ "return value not in the expected monad."+            )         expect $-          QReport False "polyResult can't be mocked: polymorphic return value."+          QReport+            False+            ( "wrongMonad can't be mocked: "+                ++ "return value not in the expected monad."+            )         expect $           QReport             False
test/Core.hs view
@@ -14,7 +14,7 @@ import Control.Monad.Reader (MonadReader (local), ask, runReaderT) import Control.Monad.State (execStateT, modify) import Control.Monad.Trans (liftIO)-import Data.IORef (newIORef, readIORef, writeIORef)+import Data.IORef (modifyIORef, newIORef, readIORef, writeIORef) import Data.List (isInfixOf, isPrefixOf) import Test.HMock import Test.Hspec@@ -213,6 +213,24 @@         expect $ WriteFile_ (hasSubstr "bar") anything         writeFile "bar.txt" "unknown contents" +    it "matches with WholeMethodMatcher" $+      example $ do+        let setExpectations =+              expect $+                WriteFile_ anything anything+                  `SuchThat` \(WriteFile f txt) -> txt `isInfixOf` f++            success = runMockT $ do+              setExpectations+              writeFile "foo.txt" "foo"++            failure = runMockT $ do+              setExpectations+              writeFile "foo.txt" "bar"++        success+        failure `shouldThrow` anyException+     it "stores source location in suchThat predicate" $       example $ do         let test = runMockT $ do@@ -637,6 +655,20 @@               <*> readFile "baz.txt"           liftIO (result `shouldBe` ("foo", "bar", "")) +    it "performs side effects" $+      example $+        runMockT $ do+          ref <- liftIO $ newIORef False++          allowUnexpected $ WriteFile_ anything anything+          whenever $+            WriteFile_ anything anything+              |=> const (liftIO (writeIORef ref True))++          writeFile "foo.txt" "foo"++          liftIO (readIORef ref `shouldReturn` True)+     it "doesn't adopt lax behavior for byDefault" $       example $ do         let test = runMockT $ do@@ -698,6 +730,30 @@             liftIO (result `shouldBe` ("foo #1", "bar #1", "foo #2", "bar #2"))        it "inherits defaults correctly" $+        example $+          runMockT $ do+            allowUnexpected $ WriteFile_ anything anything++            superCount <- liftIO $ newIORef (0 :: Int)+            subCount <- liftIO $ newIORef (0 :: Int)++            whenever $+              WriteFile_ anything anything+                |=> \_ -> liftIO $ modifyIORef superCount (+ 1)+            writeFile "foo.txt" "foo"++            nestMockT $ do+              whenever $+                WriteFile_ anything anything+                  |=> \_ -> liftIO $ modifyIORef subCount (+ 1)+              writeFile "foo.txt" "foo"++            writeFile "foo.txt" "foo"++            liftIO (readIORef superCount `shouldReturn` 3)+            liftIO (readIORef subCount `shouldReturn` 1)++      it "inherits side effects correctly" $         example $           runMockT $ do             allowUnexpected $ ReadFile "foo.txt" |-> "foo"
test/Extras.hs view
@@ -15,6 +15,11 @@       <$> (fromInteger <$> arbitrary)       <*> (fromInteger . getNonNegative <$> arbitrary) +guessMultiplicity :: [Int] -> Multiplicity+guessMultiplicity vals+  | null vals = -1+  | otherwise = between (fromIntegral (minimum vals)) (fromIntegral (maximum vals))+ multiplicityTests :: SpecWith () multiplicityTests = do   describe "Multiplicity" $ do@@ -38,9 +43,20 @@         \m1 m2 (NonNegative n) ->           let natSums k = [(i, k - i) | i <- [0 .. k]]            in meetsMultiplicity (m1 + m2) n-                == any+                === any                   (\(j, k) -> meetsMultiplicity m1 j && meetsMultiplicity m2 k)                   (natSums n)++    it "correctly implements difference" $+      property $ \m1 m2 ->+        m1 - m2+          === guessMultiplicity+            [ i - j+              | i <- [0 .. 100],+                meetsMultiplicity m1 i,+                j <- [0 .. 100],+                meetsMultiplicity m2 j+            ]  predicateTests :: SpecWith () predicateTests = do
test/QuasiMockBase.hs view
@@ -9,6 +9,7 @@ {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}  module QuasiMockBase where