packages feed

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

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}

module Test.MockCat.TypeClassMinimalSpec (spec) where

import Test.Hspec
import Test.MockCat
import Data.Text (Text, pack)
import Test.MockCat.Verify (ResolvableParamsOf)
import Prelude hiding (readFile, writeFile, any)

import Data.Proxy (Proxy(..))
import Control.Exception (ErrorCall, displayException, evaluate)
import Data.List (isInfixOf)
import Data.Typeable (cast)
import GHC.TypeLits (symbolVal)
import Control.Monad.IO.Class (liftIO)

-- Helper for error matching
missingCallHandwritten :: String -> Selector ErrorCall
missingCallHandwritten name err =
  let needle1 = "function `" <> name <> "` was not called the expected number of times."
      needle2 = "function `_" <> name <> "` was not called the expected number of times."
   in (needle1 `isInfixOf` displayException err) || (needle2 `isInfixOf` displayException err)

-- Handwritten polymorphic return helpers
_readFile ::
  ( MockDispatch (IsMockSpec spec) spec (MockT IO) (FilePath -> Text)
  ) =>
  spec ->
  MockT IO (MockResult (ResolvableParamsOf (FilePath -> Text)))
_readFile p = MockT $ do
  fn <- unMockT (mock (label "readFile") p :: MockT IO (FilePath -> Text))
  addDefinition (Definition (Proxy :: Proxy "readFile") fn NoVerification)
  pure (MockResult ())

_writeFile ::
  ( MockDispatch (IsMockSpec spec) spec (MockT IO) (FilePath -> Text -> ())
  ) =>
  spec ->
  MockT IO (MockResult (ResolvableParamsOf (FilePath -> Text -> ())))
_writeFile p = MockT $ do
  fn <- unMockT (mock (label "writeFile") p :: MockT IO (FilePath -> Text -> ()))
  addDefinition (Definition (Proxy :: Proxy "writeFile") fn NoVerification)
  pure (MockResult ())

readFile :: FilePath -> MockT IO Text
readFile path = do
  defs <- getDefinitions
  let stubs = reverse [f | Definition (p :: Proxy sym) fn _ <- defs, symbolVal p == "readFile", Just f <- [cast fn :: Maybe (FilePath -> Text)]]
  case stubs of
    [] -> error "readFile: no stub defined or type mismatch"
    (f : _) -> pure (f path)

writeFile :: FilePath -> Text -> MockT IO ()
writeFile path content = do
  defs <- getDefinitions
  let stubs = reverse [f | Definition (p :: Proxy sym) fn _ <- defs, symbolVal p == "writeFile", Just f <- [cast fn :: Maybe (FilePath -> Text -> ())]]
  case stubs of
    [] -> error "writeFile: no stub defined or type mismatch"
    (f : _) -> pure (f path content)

spec :: Spec
spec = do
  describe "Polymorphic return tests (Clean)" do
    it "Program definition suppresses unused-do-bind warning (expects not used)" do
      -- Just verify that this compiles without warning
      runMockT do
        _readFile $ "input.txt" ~> pack "content"
        _writeFile $ "output.txt" ~> pack "content" ~> ()
        pure ()

    it "Error when read stub is defined but target function readFile is never called (expects used)" do
      (runMockT @IO do
        _readFile ("input.txt" ~> pack "content")
          `expects` called once
        -- readFile is never called
        pure ()) `shouldThrow` missingCallHandwritten "readFile"

    it "Arguments can be verified even if the helper returns () (Dynamic Resolution + Coercion)" do
      runMockT do
        _readFile ("input.txt" ~> pack "content") `expects` called once
        _ <- readFile "input.txt" >>= liftIO . evaluate
        pure ()

    it "Arguments can be verified with 'with' (Dynamic Resolution + Coercion)" do
      runMockT do
        _readFile ("input.txt" ~> pack "content") `expects` (called once `with` "input.txt")
        _ <- readFile "input.txt" >>= liftIO . evaluate
        pure ()

    it "Can verify arguments for functions returning () without type annotations (writeFile)" do
      runMockT do
        _writeFile ("output.txt" ~> pack "content" ~> ()) `expects` called once
        -- Execute the function and force evaluation
        writeFile "output.txt" (pack "content") >>= liftIO . evaluate
        pure ()