packages feed

polysemy-mocks-0.1.0.0: test/Test/Polysemy/Mock/TeletypeSpec.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module Test.Polysemy.Mock.TeletypeSpec where

import Data.Functor.Identity
import Data.Kind
import Polysemy
import Polysemy.Internal (send)
import Polysemy.State
import Test.Hspec
import Test.Polysemy.Mock
import Prelude hiding (read)

data Teletype (m :: Type -> Type) a where
  Read :: Teletype m String
  Write :: String -> Teletype m ()

read :: Member Teletype r => Sem r String
read = send Read

write :: Member Teletype r => String -> Sem r ()
write = send . Write

instance forall n. Mock Teletype n where
  data MockImpl Teletype n m a where
    MockRead :: MockImpl Teletype n m String
    MockWrite :: String -> MockImpl Teletype n m ()
    MockReadReturns :: n String -> MockImpl Teletype n m ()
    MockWriteReturns :: (String -> n ()) -> MockImpl Teletype n m ()
    MockReadCalls :: MockImpl Teletype n m [()]
    MockWriteCalls :: MockImpl Teletype n m [String]

  data MockState Teletype n = MockState
    { readCalls :: [()],
      writeCalls :: [String],
      readReturns :: n String,
      writeReturns :: String -> n ()
    }

  initialMockState = MockState [] [] (error "Unimplemented") (error "Unimplemented")

  mock = interpret $ \case
    Read -> send @(MockImpl Teletype n) MockRead
    Write s -> send @(MockImpl Teletype n) $ MockWrite s
  mockToState = reinterpretH $ \case
    MockRead -> do
      state <- get @(MockState Teletype n)
      put state {readCalls = readCalls state ++ [()]}
      pureT =<< embed (readReturns state)
    MockWrite s -> do
      state <- get @(MockState Teletype n)
      put state {writeCalls = writeCalls state ++ [s]}
      pureT =<< embed (writeReturns state s)
    MockReadReturns f -> do
      state <- get @(MockState Teletype n)
      put state {readReturns = f}
      pureT ()
    MockWriteReturns f -> do
      state <- get @(MockState Teletype n)
      put state {writeReturns = f}
      pureT ()
    MockReadCalls -> do
      state <- get @(MockState Teletype n)
      pureT (readCalls state)
    MockWriteCalls -> do
      state <- get @(MockState Teletype n)
      pureT (writeCalls state)

mockWriteReturns :: (String -> m ()) -> Sem '[MockImpl Teletype m, Embed m] ()
mockWriteReturns = send . MockWriteReturns

mockReadReturns :: m String -> Sem '[MockImpl Teletype m, Embed m] ()
mockReadReturns = send . MockReadReturns

mockReadCalls :: forall m. Sem '[MockImpl Teletype m, Embed m] [()]
mockReadCalls = send @(MockImpl Teletype m) MockReadCalls

mockWriteCalls :: forall m. Sem '[MockImpl Teletype m, Embed m] [String]
mockWriteCalls = send @(MockImpl Teletype m) MockWriteCalls

program :: Member Teletype r => Sem r ()
program = do
  write "Name: "
  name <- read
  write $ "Hello " <> name

{-# ANN spec ("HLint: ignore Redundant do" :: String) #-}
spec :: Spec
spec =
  describe "program" $ do
    it "greets" $ runM @IO . evalMock $ do
      mockWriteReturns (const $ pure ())
      mockReadReturns (pure "Akshay")
      mock @Teletype @IO program
      writes <- mockWriteCalls
      embed $ writes `shouldBe` ["Name: ", "Hello Akshay"]
    it "greets without IO" $ do
      let state = runIdentity . runM @Identity . execMock $ do
            mockWriteReturns (const $ pure ())
            mockReadReturns (pure "Akshay")
            mock @Teletype @Identity program
      writeCalls state `shouldBe` ["Name: ", "Hello Akshay"]