packages feed

mockcat-1.3.0.0: test/Test/MockCat/WithMockSpec.hs

{-# LANGUAGE BlockArguments #-}
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -fno-cse -fno-full-laziness #-}
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module Test.MockCat.WithMockSpec (spec) where

import Prelude hiding (readFile, writeFile, any)
import Data.Text (pack)
import Test.Hspec (Spec, describe, it, shouldBe, shouldThrow, anyErrorCall)
import Test.MockCat
import Test.MockCat.SharedSpecDefs
import GHC.IO (evaluate)
import Control.Concurrent.Async (async, wait)
import Control.Concurrent (threadDelay)
import Control.Monad (void, forM, forM_)
import Control.Monad.IO.Unlift (withRunInIO)
import Control.Monad.IO.Class (liftIO)
import Control.Exception (try, ErrorCall(..))

-- Generate mocks for FileOperation
makeAutoLiftMock [t|FileOperation|]



perCall :: Int -> a -> a
perCall _ x = x

operationProgram ::
  FileOperation m =>
  FilePath ->
  FilePath ->
  m ()
operationProgram inputPath outputPath = do
  content <- readFile inputPath
  writeFile outputPath content

spec :: Spec
spec = do
  describe "user-defined type comparison" $ do
    it "should be able to compare user-defined types with Eq and Show" $ do
      withMock $ do
        let p = Post 1 "title"
        f <- mock (p ~> True)
        liftIO $ f p `shouldBe` True

    it "should work with expects and specific value" $ do
      withMock $ do
        let p = Post 1 "title"
        f <- mock (any @Post ~> True) `expects` (called once `with` p)
        liftIO $ f p `shouldBe` True

    it "should work with expects and ANY" $ do
      withMock $ do
        let p = Post 1 "title"
        f <- mock (any @Post ~> True) `expects` (called once `with` any @Post)
        liftIO $ f p `shouldBe` True

  describe "withMock basic functionality" $ do
    it "simple mock with expects" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True) `expects` (called once `with` "a")
        liftIO $ mockFn "a" `shouldBe` True

    it "simple mock with expects using param" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True) `expects` (called once `with` param "a")
        liftIO $ mockFn "a" `shouldBe` True

    it "fails when not called" $ do
      withMock (do 
        mock (any @String ~> True)
          `expects` (called once `with` "a")
        pure ()) `shouldThrow` anyErrorCall

    it "error message when not called" $ do
      result <- try $ withMock $ do 
        mock (any @String ~> True)
          `expects` (called once `with` "a")
        pure ()
      case result of
        Left (ErrorCall msg) -> do
          let expected =
                "function was not called the expected number of times with the expected arguments.\n" <>
                "  expected: 1\n" <>
                "   but got: 0"
          msg `shouldBe` expected
        _ -> fail "Expected ErrorCall"

    it "atLeast expectation" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True) `expects` (called (atLeast 1) `with` "a")

        void $ liftIO $ evaluate $ mockFn "a"
        void $ liftIO $ evaluate $ mockFn "a"
        void $ liftIO $ evaluate $ mockFn "a"

    it "anything expectation" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` called once

        void $ liftIO $ evaluate $ mockFn "a"

    it "anything expectation error message when not called" $ do
      result <- try $ withMock $ do 
        mock (any @String ~> True)
          `expects` called once
        pure ()
      case result of
        Left (ErrorCall msg) -> do
          let expected =
                "function was not called the expected number of times.\n" <>
                "  expected: 1\n" <>
                "   but got: 0"
          msg `shouldBe` expected
        _ -> fail "Expected ErrorCall"





    it "never expectation without args succeeds when not called" $ do
      withMock $ do 
        mock (any @String ~> True)
          `expects` called never
        pure ()

    it "never expectation without args fails when called" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` called never
        liftIO $ mockFn "a" `shouldBe` True
        pure ()) `shouldThrow` anyErrorCall

    it "never expectation with args succeeds when not called with that arg" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` (called never `with` "z")
        liftIO $ mockFn "a" `shouldBe` True
        pure ()

    it "never expectation with args fails when called with that arg" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` (called never `with` "z")
        liftIO $ mockFn "z" `shouldBe` True
        pure ()) `shouldThrow` anyErrorCall

    it "multiple expectations in do block" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called (times 2) `with` "a"
            called once `with` "b"
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "b" `shouldBe` True
        pure ()

    it "multiple expectations in do block fails when not all satisfied" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called (times 2) `with` "a"
            called once `with` "b"
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "a" `shouldBe` True
        -- missing: mockFn "b"
        pure ()) `shouldThrow` anyErrorCall

    it "multiple expectations in do block with never" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called once `with` "a"
            called never `with` "z"
        liftIO $ mockFn "a" `shouldBe` True
        pure ()

    it "multiple expectations in do block with never fails when violated" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called once `with` "a"
            called never `with` "z"
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "z" `shouldBe` True  -- This should fail
        pure ()) `shouldThrow` anyErrorCall

    it "multiple expectations with different counts" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called (times 3) `with` "a"
            called (atLeast 2) `with` "b"
            called once `with` "c"
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "b" `shouldBe` True
        liftIO $ mockFn "b" `shouldBe` True
        liftIO $ mockFn "c" `shouldBe` True
        pure ()

    it "multiple expectations fails when count is insufficient" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called (times 3) `with` "a"
            called once `with` "b"
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "a" `shouldBe` True
        -- missing one more "a" call
        liftIO $ mockFn "b" `shouldBe` True
        pure ()) `shouldThrow` anyErrorCall

    it "multiple expectations fails when atLeast is not satisfied" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called (atLeast 2) `with` "a"
            called once `with` "b"
        liftIO $ mockFn "a" `shouldBe` True
        -- missing one more "a" call (need at least 2)
        liftIO $ mockFn "b" `shouldBe` True
        pure ()) `shouldThrow` anyErrorCall

    it "multiple expectations with mixed never and count" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called (times 2) `with` "a"
            called never `with` "z"
            called once `with` "b"
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "b" `shouldBe` True
        pure ()

    it "multiple expectations with never fails when never is violated" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called (times 2) `with` "a"
            called never `with` "z"
            called once `with` "b"
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "z" `shouldBe` True  -- This should fail
        pure ()) `shouldThrow` anyErrorCall


  describe "withMock verification failures" $ do
    it "fails when called fewer times than expected" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called (times 3) `with` "a"

        liftIO $ evaluate $ mockFn "a"
        liftIO $ evaluate $ mockFn "a"
        pure ()) `shouldThrow` anyErrorCall

    it "fails when called with unexpected arguments" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called once `with` "a"

        liftIO $ evaluate $ mockFn "b"
        pure ()) `shouldThrow` anyErrorCall

    it "fails when called but never expected" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            called never `with` "z"

        liftIO $ evaluate $ mockFn "z"
        pure ()) `shouldThrow` anyErrorCall

  describe "withMock with runMockT" $ do
    it "can use runMockT inside withMock" $ do
      withMock $ do
        result <- runMockT do
          _readFile $ "input.txt" ~> pack "content"
          _writeFile $ "output.txt" ~> pack "content" ~> ()
          operationProgram "input.txt" "output.txt"

        liftIO $ result `shouldBe` ()

  describe "order verification" $ do
    it "calledInOrder succeeds when called in correct order" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            calledInOrder ["a", "b", "c"]

        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "b" `shouldBe` True
        liftIO $ mockFn "c" `shouldBe` True
        pure ()

    it "calledInOrder fails when called in wrong order" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            calledInOrder ["a", "b", "c"]

        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "c" `shouldBe` True  -- Wrong order: should be "b"
        liftIO $ mockFn "b" `shouldBe` True
        pure ()) `shouldThrow` anyErrorCall

    it "calledInOrder fails when not all calls are made" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            calledInOrder ["a", "b", "c"]

        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "b" `shouldBe` True
        -- missing: mockFn "c"
        pure ()) `shouldThrow` anyErrorCall

    it "calledInSequence succeeds when sequence is followed" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            calledInSequence ["a", "c"]

        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "b" `shouldBe` True  -- This is ignored
        liftIO $ mockFn "c" `shouldBe` True
        pure ()

    it "calledInSequence fails when sequence is violated" $ do
      withMock (do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            calledInSequence ["a", "c"]

        liftIO $ mockFn "c" `shouldBe` True  -- Wrong: "a" should come first
        liftIO $ mockFn "a" `shouldBe` True
        pure ()) `shouldThrow` anyErrorCall

    it "calledInSequence succeeds with extra calls in between" $ do
      withMock $ do 
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            calledInSequence ["a", "c"]

        liftIO $ mockFn "a" `shouldBe` True
        liftIO $ mockFn "x" `shouldBe` True
        liftIO $ mockFn "y" `shouldBe` True
        liftIO $ mockFn "c" `shouldBe` True
        pure ()

  describe "multiple mocks" $ do
    it "can define multiple mocks in withMock" $ do
      withMock $ do 
        fn1 <- mock (any @String ~> True)
        pure fn1 `expects` do
            called once `with` "a" 

        fn2 <- mock (any @String ~> any @String ~> False)
        pure fn2 `expects` do
            called once `with` ("x" ~> "y")

        liftIO $ fn1 "a" `shouldBe` True
        liftIO $ fn2 "x" "y" `shouldBe` False
        pure ()

  describe "withMock scope isolation" $ do
    it "mocks from different withMock blocks do not interfere" $ do
      -- First withMock block: expect one call
      withMock $ do 
        fn1 <- mock (any @String ~> True)
        pure fn1 `expects` do
            called once `with` "a"
        liftIO $ evaluate $ fn1 "a"

      -- Second withMock block: expect zero (if leaked, would see 1 and fail)
      withMock $ do 
        mock (any @String ~> True)
          `expects` do
            called never `with` "a"
        pure ()

    it "multiple sequential withMock blocks are independent" $ do
      -- Block 1: call with "x"
      withMock $ do
        fn1 <- mock (any @String ~> True)
        pure fn1 `expects` do
            called once `with` "x"
        liftIO $ evaluate $ fn1 "x"

      -- Block 2: call with "y"
      withMock $ do
        fn2 <- mock (any @String ~> True)
        pure fn2 `expects` do
            called once `with` "y"
        liftIO $ evaluate $ fn2 "y"

      -- Block 3: no calls
      withMock $ do
        mock (any ~> True)
          `expects` do
            called never `with` "z"
        pure ()

  describe "withMock concurrency" $ do
    it "counts calls across parallel threads" $ do
      withMock $ do
        mockFn <- mock (any ~> True)
        pure mockFn `expects` do
            called (times 10)

        withRunInIO $ \runInIO -> do
          as <- forM [1 .. 10] $ \i ->
            async $ runInIO $ do
              liftIO $ evaluate $ perCall i (mockFn "a")
              pure ()
          mapM_ wait as

    it "handles concurrent calls with different arguments" $ do
      withMock $ do
        mockFn <- mock (any ~> True)
        pure mockFn `expects` do
            called (times 5) `with` "a"
            called (times 5) `with` "b"

        withRunInIO $ \runInIO -> do
          as1 <- forM [1 .. 5] $ \i ->
            async $ runInIO $ liftIO $ evaluate $ perCall i (mockFn "a")
          as2 <- forM [1 .. 5] $ \i ->
            async $ runInIO $ liftIO $ evaluate $ perCall (100 + i) (mockFn "b")
          mapM_ wait (as1 ++ as2)

    it "stress test: many threads with many calls" $ do
      let threads = 20 :: Int
          callsPerThread = 10 :: Int
          total = threads * callsPerThread :: Int

      withMock $ do
        mockFn <- mock (any ~> True)
        pure mockFn `expects` do
            called (times total)

        withRunInIO $ \runInIO -> do
          as <- forM [1 .. threads] $ \threadIx ->
            async $ runInIO $ do
              forM_ [1 .. callsPerThread] $ \callIx -> do
                let tag = threadIx * 1000 + callIx
                liftIO $ evaluate $ perCall tag (mockFn "stress")
                liftIO $ threadDelay 1
          mapM_ wait as

    it "concurrent calls preserve order expectations" $ do
      withMock $ do
        mockFn <- mock (any @String ~> True)
        pure mockFn `expects` do
            calledInOrder ["first", "second", "third"]

        withRunInIO $ \runInIO -> do
          -- Sequential calls to preserve order
          runInIO $ void $ liftIO $ evaluate $ mockFn "first"
          runInIO $ void $ liftIO $ evaluate $ mockFn "second"
          runInIO $ void $ liftIO $ evaluate $ mockFn "third"