cloud-seeder-0.0.0.0: test-suite/Network/CloudSeeder/Test/Stubs.hs
{-# LANGUAGE UndecidableInstances #-}
module Network.CloudSeeder.Test.Stubs where
import qualified Data.Text as T
import Control.Monad.Error.Lens (throwing)
import Control.Monad.Except (MonadError)
import Control.Monad.Reader (ReaderT(..), ask)
import Control.Monad.Writer (WriterT(..), tell)
import Control.Monad.State (StateT(..), get, put)
import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad.Logger (MonadLogger(..))
import Data.ByteString (ByteString)
import Data.Type.Equality ((:~:)(..))
import System.Log.FastLogger (fromLogStr, toLogStr)
import Network.CloudSeeder.CommandLine
import Network.CloudSeeder.Interfaces
--------------------------------------------------------------------------------
-- Arguments
newtype ArgumentsT m a = ArgumentsT (ReaderT Command m a)
deriving ( Functor, Applicative, Monad, MonadTrans, MonadError e
, MonadLogger, MonadFileSystem e, MonadCloud, MonadEnvironment )
-- | Runs a computation with access to a set of command-line arguments.
runArgumentsT :: Command -> ArgumentsT m a -> m a
runArgumentsT args (ArgumentsT x) = runReaderT x args
instance Monad m => MonadArguments (ArgumentsT m) where
getArgs = ArgumentsT ask
--------------------------------------------------------------------------------
-- Logger
newtype LoggerT m a = LoggerT (WriterT [ByteString] m a)
deriving ( Functor, Applicative, Monad, MonadTrans, MonadError e
, MonadArguments, MonadFileSystem e, MonadCloud, MonadEnvironment )
-- | Runs a computation that may emit log messages, returning the result of the
-- computation combined with the set of messages logged, in order.
runLoggerT :: LoggerT m a -> m (a, [ByteString])
runLoggerT (LoggerT x) = runWriterT x
instance Monad m => MonadLogger (LoggerT m) where
monadLoggerLog _ _ _ str = LoggerT $ tell [fromLogStr (toLogStr str)]
--------------------------------------------------------------------------------
-- File System
newtype FileSystemT m a = FileSystemT (ReaderT [(T.Text, T.Text)] m a)
deriving ( Functor, Applicative, Monad, MonadTrans, MonadError e
, MonadArguments, MonadLogger, MonadCloud, MonadEnvironment )
-- | Runs a computation that may interact with the file system, given a mapping
-- from file paths to file contents.
stubFileSystemT :: [(T.Text, T.Text)] -> FileSystemT m a -> m a
stubFileSystemT fs (FileSystemT x) = runReaderT x fs
instance (AsFileSystemError e, MonadError e m) => MonadFileSystem e (FileSystemT m) where
readFile path = FileSystemT $ ask >>= \files ->
maybe (throwing _FileNotFound path)
return (lookup path files)
--------------------------------------------------------------------------------
-- Cloud
data CloudAction r where
ComputeChangeset :: StackName -> T.Text -> [(T.Text, T.Text)] -> CloudAction T.Text
DescribeStack :: StackName -> CloudAction (Maybe [(T.Text, T.Text)])
RunChangeSet :: T.Text -> CloudAction ()
deriving instance Eq (CloudAction r)
deriving instance Show (CloudAction r)
eqAction :: CloudAction a -> CloudAction b -> Maybe (a :~: b)
eqAction (ComputeChangeset a b c) (ComputeChangeset a' b' c')
= if a == a' && b == b' && c == c' then Just Refl else Nothing
eqAction (DescribeStack a) (DescribeStack a')
= if a == a' then Just Refl else Nothing
eqAction (RunChangeSet a) (RunChangeSet a')
= if a == a' then Just Refl else Nothing
eqAction _ _ = Nothing
data WithResult f where
(:->) :: f r -> r -> WithResult f
newtype CloudT m a = CloudT (StateT [WithResult CloudAction] m a)
deriving ( Functor, Applicative, Monad, MonadTrans, MonadError e
, MonadArguments, MonadFileSystem e, MonadLogger, MonadEnvironment )
stubCloudT :: Monad m => [WithResult CloudAction] -> CloudT m a -> m a
stubCloudT actions (CloudT x) = runStateT x actions >>= \case
(r, []) -> return r
(_, remainingActions) ->
fail $ "stubCloudT: expected the following unexecuted actions to be run:\n"
++ unlines (map (\(action :-> _) -> " " ++ show action) remainingActions)
stubCloudAction :: Monad m => String -> CloudAction r -> CloudT m r
stubCloudAction fnName action = CloudT $ get >>= \case
[] -> fail $ "stubCloudT: expected end of program, called " ++ fnName ++ "\n given action:\n"
++ " " ++ show action ++ "\n"
(action' :-> r) : actions
| Just Refl <- action `eqAction` action' -> put actions >> return r
| otherwise -> fail $ "stubCloudT: argument mismatch in " ++ fnName ++ "\n"
++ " given: " ++ show action ++ "\n"
++ " expected: " ++ show action' ++ "\n"
instance Monad m => MonadCloud (CloudT m) where
computeChangeset a b c = stubCloudAction "computeChangeset" (ComputeChangeset a b c)
getStackOutputs a = stubCloudAction "getStackOutputs" (DescribeStack a)
runChangeSet a = stubCloudAction "runChangeSet" (RunChangeSet a)
--------------------------------------------------------------------------------
-- Environment
newtype EnvironmentT m a = EnvironmentT (ReaderT [(T.Text, T.Text)] m a)
deriving ( Functor, Applicative, Monad, MonadTrans, MonadError e
, MonadArguments, MonadLogger, MonadFileSystem e, MonadCloud )
stubEnvironmentT :: [(T.Text, T.Text)] -> EnvironmentT m a -> m a
stubEnvironmentT fs (EnvironmentT x) = runReaderT x fs
instance Monad m => MonadEnvironment (EnvironmentT m) where
getEnv x = lookup x <$> EnvironmentT ask