packages feed

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

{-# LANGUAGE BlockArguments #-}
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Test.MockCat.HPCNestSpec (spec) where

import Test.Hspec
import Test.MockCat
import Control.Exception (try, SomeException, evaluate)
import Control.Monad.IO.Class (liftIO)
import Data.Either (isLeft)

spec :: Spec
spec = describe "HPC Nesting and Isolation" $ do

  it "RunMockT nested in IO: outer mock with 'shouldBeCalled' becomes unverifiable inside runMockT" $ do
    outer <- mock (param "outer" ~> True)
    -- execution in IO context
    evaluate (outer "outer")

    runMockT @IO $ do
      -- runMockT resets history, so 'outer' should not be verifiable here.
      result <- liftIO $ try @SomeException (outer `shouldBeCalled` "outer")
      liftIO $ result `shouldSatisfy` isLeft

  it "RunMockT nested in IO: outer mock is verifiable AFTER runMockT returns" $ do
    outer <- mock (param "outer" ~> True)
    evaluate (outer "outer")

    runMockT @IO $ do
      pure ()

    -- Post-runMockT, history is reset. Verification should fail.
    result <- try @SomeException (outer `shouldBeCalled` "outer")
    result `shouldSatisfy` isLeft

  it "withMock (using expects) is immune to history reset" $ do
    withMock $ do
      f <- mock (param "a" ~> True) `expects` do
             called once `with` "a"
      
      liftIO $ do
        evaluate (f "a")
        -- Nested runMockT clears history, but expectations capture the recorder directly.
        runMockT @IO $ pure ()
        
      pure ()

  it "Sibling runMockT blocks are isolated" $ do
    runMockT @IO $ do
      f1 <- mock (param "a" ~> True)
      liftIO $ evaluate (f1 "a")
      liftIO $ f1 `shouldBeCalled` "a"
    
    runMockT @IO $ do
      f2 <- mock (param "a" ~> True)
      liftIO $ evaluate (f2 "a")
      liftIO $ f2 `shouldBeCalled` "a"

  it "Nested withMock blocks work correctly even with same-type mocks" $ do
    withMock $ do
      -- Outer mock
      outer <- mock (param "a" ~> True) `expects` do
             called once
      
      liftIO $ do
        evaluate (outer "a")
        
        -- Inner withMock
        withMock $ do
          -- Inner mock with SAME signature
          inner <- mock (param "a" ~> True) `expects` do
                 called once
          
          liftIO $ evaluate (inner "a")
          pure ()
          
      pure ()

  it "Nested runMockT: inner block clears outer block's history" $ do
    runMockT @IO $ do
      outer <- mock (param "a" ~> True)
      liftIO $ evaluate (outer "a")
      
      -- At this point, outer is verifiable
      liftIO $ outer `shouldBeCalled` "a"
      
      liftIO $ runMockT @IO $ do
        -- Inner block resets history. 
        -- 'outer' (defined in parent runMockT) relies on history lookup (HPC).
        -- So verifies should fail here.
        result <- liftIO $ try @SomeException (outer `shouldBeCalled` "a")
        liftIO $ result `shouldSatisfy` isLeft
        
        -- Inner mock should work checking
        inner <- mock (param "b" ~> True)
        liftIO $ evaluate (inner "b")
        liftIO $ inner `shouldBeCalled` "b"

      -- After inner block returns, history is NOT restored (resetMockHistory is destructive).
      -- So outer mock remains broken.
      result <- liftIO $ try @SomeException (outer `shouldBeCalled` "a")
      liftIO $ result `shouldSatisfy` isLeft

  it "withMock inside runMockT works correctly" $ do
    runMockT @IO $ do
      f <- mock (param "a" ~> True)
      liftIO $ evaluate (f "a")
      
      liftIO $ withMock $ do
        g <- mock (param "b" ~> True)
        pure g `expects` do called once
        liftIO $ evaluate (g "b")
        pure ()
      
      -- withMock does not reset history, so 'f' should still be verifiable
      liftIO $ f `shouldBeCalled` "a"