{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} module Classes where import Control.DeepSeq (NFData (rnf)) import Control.Exception (evaluate) import Control.Monad.Trans (MonadIO, liftIO) import Data.Default (Default (def)) import Data.Dynamic (Typeable) import Data.Kind (Type) import Language.Haskell.TH.Syntax hiding (Type) import QuasiMock import Test.HMock import Test.Hspec import Test.Predicates (anything, eq, hasSubstr, qMatch, with) import Util.DeriveRecursive (deriveRecursive) #if MIN_VERSION_template_haskell(2, 16, 0) -- Pre-define low-level instance to prevent deriveRecursive from trying. instance NFData Bytes where rnf = undefined #endif deriveRecursive (Just AnyclassStrategy) ''NFData ''Dec class MonadSimple m where simple :: String -> m () makeMockable [t|MonadSimple|] simpleTests :: SpecWith () simpleTests = describe "MonadSimple" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadSimple) runQ (makeMockable [t|MonadSimple|]) evaluate (rnf decs) it "doesn't require unnecessary extensions for simple cases" $ example . runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadSimple) expectAny $ QIsExtEnabled RankNTypes |-> False _ <- runQ (makeMockable [t|MonadSimple|]) return () it "fails when GADTs is disabled" $ example $ do let missingGADTs = runMockT $ do expectAny $ QIsExtEnabled GADTs |-> False expect $ QReport_ anything (hasSubstr "Please enable GADTs") _ <- runQ (makeMockable [t|MonadSimple|]) return () missingGADTs `shouldThrow` anyException it "fails when TypeFamilies is disabled" $ example $ do let missingTypeFamilies = runMockT $ do expectAny $ QIsExtEnabled TypeFamilies |-> False expect $ QReport_ anything (hasSubstr "Please enable TypeFamilies") _ <- runQ (makeMockable [t|MonadSimple|]) return () missingTypeFamilies `shouldThrow` anyException it "fails when DataKinds is disabled" $ example $ do let missingDataKinds = runMockT $ do expectAny $ QIsExtEnabled DataKinds |-> False expect $ QReport_ anything (hasSubstr "Please enable DataKinds") _ <- runQ (makeMockable [t|MonadSimple|]) return () missingDataKinds `shouldThrow` anyException it "fails when FlexibleInstances is disabled" $ example $ do let missingDataKinds = runMockT $ do expectAny $ QIsExtEnabled FlexibleInstances |-> False expect $ QReport_ anything (hasSubstr "Please enable FlexibleInstances") _ <- runQ (makeMockable [t|MonadSimple|]) return () missingDataKinds `shouldThrow` anyException it "fails when MultiParamTypeClasses is disabled" $ example $ do let missingDataKinds = runMockT $ do expectAny $ QIsExtEnabled MultiParamTypeClasses |-> False expect $ QReport_ anything (hasSubstr "Please enable MultiParamTypeClasses") _ <- runQ (makeMockable [t|MonadSimple|]) return () missingDataKinds `shouldThrow` anyException it "fails when ScopedTypeVariables is disabled" $ example $ do let missingDataKinds = runMockT $ do expectAny $ QIsExtEnabled ScopedTypeVariables |-> False expect $ QReport_ anything (hasSubstr "Please enable ScopedTypeVariables") _ <- runQ (makeMockable [t|MonadSimple|]) return () missingDataKinds `shouldThrow` anyException it "fails when too many params are given" $ example $ do let tooManyParams = runMockT $ do $(onReify [|expectAny|] ''MonadSimple) expect $ QReport_ anything (hasSubstr "is applied to too many arguments") _ <- runQ (makeMockable [t|MonadSimple IO|]) return () tooManyParams `shouldThrow` anyException it "is mockable" $ example $ do let success = runMockT $ do expect $ Simple "foo" simple "foo" failure = runMockT $ do expect $ Simple "foo" simple "bar" success failure `shouldThrow` anyException class MonadSuffix m where suffix :: String -> m () makeMockableWithOptions [t|MonadSuffix|] def {mockSuffix = "Blah"} suffixTests :: SpecWith () suffixTests = describe "MonadSuffix" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadSuffix) runQ $ makeMockableWithOptions [t|MonadSuffix|] def {mockSuffix = "Blah"} evaluate (rnf decs) it "is mockable" $ example $ do let success = runMockT $ do expect $ SuffixBlah "foo" suffix "foo" failure = runMockT $ do expect $ SuffixBlah "foo" suffix "bar" success failure `shouldThrow` anyException class MonadWithSetup m where withSetup :: m String makeMockableWithOptions [t|MonadWithSetup|] def {mockEmptySetup = False} instance Mockable MonadWithSetup where setupMockable _ = do allowUnexpected $ WithSetup |-> "custom default" setupTests :: SpecWith () setupTests = describe "MonadWithSetup" $ do it "generates mock impl" $ do example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadWithSetup) runQ $ makeMockableWithOptions [t|MonadWithSetup|] def {mockEmptySetup = False} evaluate (rnf decs) it "returns the customized default value" $ do example $ runMockT $ do result <- withSetup liftIO (result `shouldBe` "custom default") it "uses default in a nested context" $ do example $ runMockT $ do nestMockT $ do result <- withSetup liftIO (result `shouldBe` "custom default") it "retains default when initialized in nested context" $ do example $ runMockT $ do nestMockT $ do _ <- withSetup return () result <- withSetup liftIO (result `shouldBe` "custom default") class SuperClass (m :: Type -> Type) instance SuperClass m class (SuperClass m, Monad m, Typeable m) => MonadSuper m where withSuper :: m () makeMockable [t|MonadSuper|] superTests :: SpecWith () superTests = describe "MonadSuper" $ do it "generated mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadSuper) expectAny $ QReifyInstances_ (eq ''SuperClass) $(qMatch [p|[AppT (ConT (Name (OccName "MockT") _)) (VarT _)]|]) |-> $( reifyInstancesStatic ''SuperClass [AppT (ConT ''MockT) (VarT (mkName "v"))] ) runQ (makeMockable [t|MonadSuper|]) evaluate (rnf decs) it "is mockable" $ example $ do let success = runMockT $ do expect WithSuper withSuper failure = runMockT withSuper success failure `shouldThrow` anyException class MonadMPTC a m where mptc :: a -> m () mptcList :: [a] -> m () makeMockable [t|MonadMPTC|] mptcTests :: SpecWith () mptcTests = describe "MonadMPTC" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadMPTC) runQ (makeMockable [t|MonadMPTC|]) evaluate (rnf decs) it "is mockable" $ example $ do let success = runMockT $ do expect $ Mptc "foo" mptc "foo" failure = runMockT $ do expect $ Mptc "foo" mptc "bar" success failure `shouldThrow` anyException class MonadFDSpecialized a m | m -> a where fdSpecialized :: a -> m a makeMockable [t|MonadFDSpecialized String|] fdSpecializedTests :: SpecWith () fdSpecializedTests = describe "MonadFDSpecialized" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadFDSpecialized) runQ (makeMockable [t|MonadFDSpecialized String|]) evaluate (rnf decs) it "is mockable" $ example $ do let success = runMockT $ do expect $ FdSpecialized "foo" |-> "bar" r <- fdSpecialized "foo" liftIO $ r `shouldBe` "bar" failure = runMockT $ do expect $ FdSpecialized "foo" |-> "bar" fdSpecialized "bar" success failure `shouldThrow` anyException class MonadFDGeneral a m | m -> a where fdGeneral :: a -> m a makeMockableWithOptions [t|MonadFDGeneral|] def {mockDeriveForMockT = False} newtype MyBase m a = MyBase {runMyBase :: m a} deriving newtype (Functor, Applicative, Monad, MonadIO) instance (MonadIO m, Typeable m) => MonadFDGeneral String (MockT (MyBase m)) where fdGeneral x = mockMethod (FdGeneral x) fdGeneralTests :: SpecWith () fdGeneralTests = describe "MonadFDGeneral" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadFDGeneral) runQ $ makeMockableWithOptions [t|MonadFDGeneral|] def {mockDeriveForMockT = False} evaluate (rnf decs) it "is mockable" $ example $ do let success = runMyBase . runMockT $ do expect $ FdGeneral "foo" |-> "bar" r <- fdGeneral "foo" liftIO $ r `shouldBe` "bar" failure = runMyBase . runMockT $ do expect $ FdGeneral "foo" |-> "bar" fdGeneral "bar" success failure `shouldThrow` anyException class MonadFDMixed a b c d m | m -> a b c d where fdMixed :: a -> b -> c -> m d makeMockableWithOptions [t|MonadFDMixed String Int|] def {mockDeriveForMockT = False} makeMockable [t|MonadFDMixed String Int String String|] fdMixedTests :: SpecWith () fdMixedTests = describe "MonadFDMixed" $ do it "generates mock impl" $ example . runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadFDMixed) decs1 <- runQ $ makeMockableWithOptions [t|MonadFDMixed String Int|] def {mockDeriveForMockT = False} decs2 <- runQ (makeMockable [t|MonadFDMixed String Int String Int|]) _ <- liftIO $ evaluate (rnf (decs1 ++ decs2)) return () it "is mockable" $ example $ do let success = runMockT $ do expect $ FdMixed "foo" (1 :: Int) "bar" |-> "qux" r <- fdMixed "foo" 1 "bar" liftIO $ r `shouldBe` "qux" failure = runMockT $ do expect $ FdMixed "foo" (1 :: Int) "bar" |-> "qux" _ <- fdMixed "bar" 1 "foo" return () success failure `shouldThrow` anyException class MonadPolyArg m where polyArg :: Enum a => String -> a -> b -> m () makeMockable [t|MonadPolyArg|] polyArgTests :: SpecWith () polyArgTests = describe "MonadPolyArg" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadPolyArg) runQ (makeMockable [t|MonadPolyArg|]) evaluate (rnf decs) it "fails without RankNTypes" $ example $ do let missingRankNTypes = runMockT $ do $(onReify [|expectAny|] ''MonadPolyArg) expectAny $ QIsExtEnabled RankNTypes |-> False expect $ QReport_ anything (hasSubstr "Please enable RankNTypes") _ <- runQ (makeMockable [t|MonadPolyArg|]) return () missingRankNTypes `shouldThrow` anyException it "is mockable" $ example $ do let success = runMyBase . runMockT $ do expect $ PolyArg_ (eq "foo") (with fromEnum (eq 1)) anything polyArg "foo" (toEnum 1 :: Bool) "hello" failure = runMyBase . runMockT $ do expect $ PolyArg_ (eq "foo") (with fromEnum (eq 1)) anything polyArg "foo" (toEnum 2 :: Bool) "hello" success failure `shouldThrow` anyException class MonadUnshowableArg m where unshowableArg :: (Int -> Int) -> m () makeMockable [t|MonadUnshowableArg|] unshowableArgTests :: SpecWith () unshowableArgTests = describe "MonadUnshowableArg" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadUnshowableArg) runQ (makeMockable [t|MonadUnshowableArg|]) evaluate (rnf decs) it "is mockable" $ example $ do let success = runMyBase . runMockT $ do expect $ UnshowableArg_ anything unshowableArg (+ 1) failure = runMyBase . runMockT $ do expect $ UnshowableArg_ anything unshowableArg (+ 1) unshowableArg (+ 1) success failure `shouldThrow` anyException class MonadInArg m where monadInArg :: (Int -> m ()) -> m () makeMockable [t|MonadInArg|] monadInArgTests :: SpecWith () monadInArgTests = describe "MonadInArg" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadInArg) runQ (makeMockable [t|MonadInArg|]) evaluate (rnf decs) it "is mockable" $ example $ do let success = runMyBase . runMockT $ do expect $ MonadInArg_ anything monadInArg (const (return ())) failure = runMyBase . runMockT $ do expect $ UnshowableArg_ anything monadInArg (const (return ())) monadInArg (const (return ())) success failure `shouldThrow` anyException class MonadPolyResult m where polyResult :: Typeable a => a -> m a nonTypeablePolyResult :: a -> m a makeMockableWithOptions [t|MonadPolyResult|] def {mockDeriveForMockT = False} 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 allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadPolyResult) runQ $ makeMockableWithOptions [t|MonadPolyResult|] def {mockDeriveForMockT = False} 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 nestedRankN :: ((forall a. a -> Bool) -> Bool) -> m () mockableMethod :: Int -> m () makeMockableWithOptions [t|MonadExtraneousMembers|] def {mockDeriveForMockT = False} instance (Typeable m, MonadIO m) => MonadExtraneousMembers (MockT m) where data SomeDataType (MockT m) = SomeCon favoriteNumber SomeCon = 42 wrongMonad _ = return 42 nestedRankN _ = return () mockableMethod a = mockMethod (MockableMethod a) extraneousMembersTests :: SpecWith () extraneousMembersTests = describe "MonadExtraneousMembers" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadExtraneousMembers) runQ $ makeMockableWithOptions [t|MonadExtraneousMembers|] def {mockDeriveForMockT = False} evaluate (rnf decs) it "warns about non-methods" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadExtraneousMembers) expect $ QReport False "SomeDataType must be defined manually in MockT instance." expect $ QReport False ( "favoriteNumber can't be mocked: " ++ "return value not in the expected monad." ) expect $ QReport False ( "wrongMonad can't be mocked: " ++ "return value not in the expected monad." ) expect $ QReport False "nestedRankN can't be mocked: rank-n types nested in arguments." runQ ( makeMockableWithOptions [t|MonadExtraneousMembers|] def {mockDeriveForMockT = False, mockVerbose = True} ) evaluate (rnf decs) it "fails to derive MockT when class has extra methods" $ example $ do let unmockableMethods = runMockT $ do $(onReify [|expectAny|] ''MonadExtraneousMembers) expect $ QReport_ anything (hasSubstr "has unmockable methods") _ <- runQ (makeMockable [t|MonadExtraneousMembers|]) return () unmockableMethods `shouldThrow` anyException it "is mockable" $ example $ do let success = runMyBase . runMockT $ do expect $ MockableMethod 42 mockableMethod (favoriteNumber (SomeCon @(MockT IO))) failure = runMyBase . runMockT $ do expect $ MockableMethod 42 mockableMethod 12 success failure `shouldThrow` anyException class MonadRankN m where rankN :: (forall a. a -> Bool) -> Bool -> m () makeMockable [t|MonadRankN|] rankNTests :: SpecWith () rankNTests = describe "MonadRankN" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadRankN) runQ (makeMockable [t|MonadRankN|]) evaluate (rnf decs) it "is mockable" $ example $ do let success = runMockT $ do expect $ RankN_ anything (eq True) rankN (const True) True failure = runMockT $ do expect $ RankN_ anything (eq True) rankN (const True) False success failure `shouldThrow` anyException -- | Type with no Default instance. data NoDefault = NoDefault class MonadManyReturns m where returnsUnit :: m () returnsInt :: m Int returnsString :: m String returnsMaybe :: m (Maybe Bool) returnsNoDefault :: m NoDefault makeMockable [t|MonadManyReturns|] defaultReturnTests :: SpecWith () defaultReturnTests = do describe "MonadManyReturns" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadManyReturns) $(onReifyInstances [|expectAny|] ''Default [ConT ''NoDefault]) runQ (makeMockable [t|MonadManyReturns|]) evaluate (rnf decs) it "fails when there's an unexpected method" $ example $ runMockT returnsUnit `shouldThrow` anyException it "succeeds when there's an expected method with default response" $ example $ do result <- runMockT $ do expect ReturnsUnit expect ReturnsInt expect ReturnsString expect ReturnsMaybe (,,,) <$> returnsUnit <*> returnsInt <*> returnsString <*> returnsMaybe result `shouldBe` ((), 0, "", Nothing) it "overrides default when response is specified" $ example $ do result <- runMockT $ do expect ReturnsInt expect $ ReturnsString |-> "non-default" (,) <$> returnsInt <*> returnsString result `shouldBe` (0, "non-default") it "returns undefined when response isn't given for defaultless method" $ example $ do let test = runMockT $ do expect ReturnsNoDefault returnsNoDefault _ <- test (test >>= evaluate) `shouldThrow` anyException class MonadNestedNoDef m where nestedNoDef :: m (NoDefault, String) makeMockable [t|MonadNestedNoDef|] nestedNoDefTests :: SpecWith () nestedNoDefTests = describe "MonadNestedNoDef" $ do it "generates mock impl" $ example $ do decs <- runMockT $ do allowUnexpected $ QReifyInstances_ anything anything |-> [] $(onReify [|expectAny|] ''MonadNestedNoDef) $( [t|(NoDefault, String)|] >>= onReifyInstances [|expectAny|] ''Default . (: []) ) $(onReifyInstances [|expectAny|] ''Default [ConT ''NoDefault]) runQ (makeMockable [t|MonadNestedNoDef|]) evaluate (rnf decs) class ClassWithNoParams $(return []) -- Hack to get types into the TH environment. errorTests :: SpecWith () errorTests = describe "errors" $ do it "fails when given a type instead of a class" $ example $ do let wrongKind = runMockT $ do expect $ QReport_ anything (hasSubstr "Expected GHC.Types.Int to be a class") _ <- runQ (makeMockable [t|Int|]) return () wrongKind `shouldThrow` anyException it "fails when given an unexpected type constructor" $ example $ do let notClass = runMockT $ do expect $ QReport_ anything (hasSubstr "Expected a class") _ <- runQ (makeMockable [t|(Int, String)|]) return () notClass `shouldThrow` anyException it "fails when class has no params" $ example $ do let tooManyParams = runMockT $ do $(onReify [|expectAny|] ''ClassWithNoParams) expect $ QReport_ anything (hasSubstr "ClassWithNoParams has no type parameters") _ <- runQ (makeMockable [t|ClassWithNoParams|]) return () tooManyParams `shouldThrow` anyException it "fails when class has no mockable methods" $ example $ do let noMockableMethods = runMockT $ do $(onReify [|expectAny|] ''Show) expect $ QReport_ anything (hasSubstr "has no mockable methods") _ <- runQ (makeMockable [t|Show|]) return () noMockableMethods `shouldThrow` anyException classTests :: SpecWith () classTests = describe "makeMockable" $ do simpleTests suffixTests setupTests superTests mptcTests fdSpecializedTests fdGeneralTests fdMixedTests polyArgTests unshowableArgTests monadInArgTests extraneousMembersTests rankNTests defaultReturnTests nestedNoDefTests errorTests