hspec-server-0.1.0.0: Test/Hspec/Server.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.Hspec.Server where
import System.Process
import System.Exit
import System.IO
import qualified Test.Hspec.Core.Spec as Hspec
import qualified Test.HUnit as HUnit
import qualified Control.Monad.Trans.State as ST
import Control.Monad.Trans.Writer
import Control.Monad.IO.Class
import System.IO.Temp
import Data.Monoid
import qualified Data.Set as S
import Text.Regex.Posix
data ServerOS =
Ubuntu String
| Debian String
| FreeBSD String
| MacOS String
| Windows String
| OtherOS String
deriving (Show,Eq)
type ServerName = String
class ServerType a where
name :: a -> ServerName
cmd :: a -> FilePath -> [String] -> String -> IO (ExitCode,String,String)
data Localhost = Localhost deriving (Show,Eq)
localhost :: Localhost
localhost = Localhost
data Ssh = Ssh {
sshHostName :: String
, sshId :: Maybe String
, sshConf :: Maybe String
, sshPort :: Maybe Int
, sshUser :: Maybe String
} deriving (Show,Eq)
ssh :: String -> Ssh
ssh hostname = Ssh hostname Nothing Nothing Nothing Nothing
data Vagrant = Vagrant {
vHostName :: String
} deriving (Show,Eq)
vagrant :: String -> Vagrant
vagrant hostname = Vagrant hostname
instance ServerType Localhost where
name _ = "localhost"
cmd _ = readProcessWithExitCode
instance ServerType Ssh where
name = sshHostName
cmd d c arg i = do
readProcessWithExitCode "ssh" (sshOpt ++ [sshHost] ++ [c] ++ arg) i
where
sshOpt =
(maybe [] (\v-> ["-p",show v]) (sshPort d)) ++
(maybe [] (\v-> ["-i",show v]) (sshId d)) ++
(maybe [] (\v-> ["-F",show v]) (sshConf d))
sshHost = (maybe "" (\v -> v <> "@") (sshUser d)) <> sshHostName d
instance ServerType Vagrant where
name = vHostName
cmd d c arg i = withSystemTempFile "hspec-server" $ \file handle -> do
(_,conf,_) <- readProcessWithExitCode "vagrant" ["ssh-config"] []
hPutStr handle conf
hClose handle
readProcessWithExitCode "ssh" (["-F",file,name d,c]++arg) i
data ServerExampleData dat = ServerExampleData {
serverData :: !dat
, serverOS :: !ServerOS
}
type ServerExample dat = ST.StateT (ServerExampleData dat) IO
type ServerSpec dat = Writer [ServerSpecTree dat] ()
data ServerSpecTree dat
= ServerSpecGroup String [ServerSpecTree dat]
| ServerSpecItem String (ServerExample dat ())
detectOS :: ServerType dat => dat -> IO (Maybe ServerOS)
detectOS _dat = return $ Just $ Ubuntu "precise"
getServerData :: ServerType dat => ServerExample dat dat
getServerData = fmap serverData ST.get
sdescribe :: ServerType dat => String -> ServerSpec dat -> ServerSpec dat
sdescribe label sspecs = tell [ServerSpecGroup label $ execWriter sspecs]
serverSpecOS :: ServerType dat
=> dat
-> ServerOS
-> ServerSpec dat
-> Hspec.Spec
serverSpecOS serverData' serverOS' sspecs =
Hspec.fromSpecList $ map unServer $ execWriter sspecs
where
unServer (ServerSpecGroup x y) = Hspec.specGroup x $ map unServer y
unServer (ServerSpecItem x y) = Hspec.specItem x $
ST.evalStateT y ServerExampleData {
serverData = serverData'
, serverOS = serverOS'
}
serverSpec :: ServerType dat
=> dat
-> ServerSpec dat
-> Hspec.Spec
serverSpec serverData' sspecs = do
serverOS' <- Hspec.runIO $ detectOS serverData'
case serverOS' of
Just os' -> serverSpecOS serverData' os' sspecs
Nothing -> error "can not detect os"
sit :: ServerType dat => String -> ServerExample dat () -> ServerSpec dat
sit label example = tell [ServerSpecItem label example]
data ServerStatus =
SAnd (S.Set ServerStatus)
| Installed
| Enabled
| Running
| Listening
| Exit Int
| Stdout String
| Stderr String
| None
deriving (Show,Ord,Eq)
instance Monoid ServerStatus where
mempty = None
mappend None a = a
mappend a None = a
mappend (SAnd a) (SAnd b) = SAnd (a<>b)
mappend (SAnd a) b = SAnd (a <> S.singleton b)
mappend a (SAnd b) = SAnd (S.singleton a <> b)
mappend a b = SAnd (S.singleton a <> S.singleton b)
includes' :: ServerType dat => ServerStatus -> ServerStatus -> ServerExample dat ()
includes' org ex =
liftIO $ flip HUnit.assertBool (includes'' org ex) $ concat
[ "Expected status was ", show ex
, " but received status was ", show org
]
where
includes'' :: ServerStatus -> ServerStatus -> Bool
includes'' (SAnd org') (SAnd exp') = flip S.isSubsetOf org' exp'
includes'' org' (SAnd exp') = flip S.isSubsetOf (S.singleton org') exp'
includes'' (SAnd org') exp' = flip S.isSubsetOf org' (S.singleton exp')
includes'' org' exp' = flip S.isSubsetOf (S.singleton org') (S.singleton exp')
includes :: ServerType dat => ServerExample dat ServerStatus -> ServerStatus -> ServerExample dat ()
includes org' ex = do
org <- org'
org `includes'` ex
package :: ServerType dat => String -> ServerExample dat ServerStatus
package pkg = do
dat <- getServerData
(_,out,_) <- liftIO $ cmd dat "dpkg" ["-l",pkg] []
if or (map (\v -> v =~ ("^ii +" <> pkg <> " ")) (lines out))
then return Installed
else return None
process :: ServerType dat => String -> ServerExample dat ServerStatus
process ps = do
dat <- getServerData
(code,_,_) <- liftIO $ cmd dat "pgrep" [ps] []
if code == ExitSuccess
then return Running
else return None
service :: ServerType dat => String -> ServerExample dat ServerStatus
service s = do
dat <- getServerData
(_,out,_) <- liftIO $ cmd dat ("/etc/init.d/"++s) ["status"] []
if or (map (\v ->
(v =~ ("is running" :: String)) ||
(v =~ ("start/running" :: String))
) (lines out))
then return Running
else return None
port :: ServerType dat => Int -> ServerExample dat ServerStatus
port p = do
dat <- getServerData
(_,out,_) <- liftIO $ cmd dat "netstat" ["-tanp"] []
if or (map (\v ->
(v =~ ("^tcp[ 6] +[0-9]+ +[0-9]+ :::" <> show p <> " +[^ ]+ +LISTEN")) ||
(v =~ ("^tcp[ 6] +[0-9]+ +[0-9]+ [0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+:" <> show p <> " +[^ ]+ +LISTEN"))
) (lines out))
then return Listening
else return None
command :: ServerType dat => FilePath -> [String] -> String -> ServerExample dat ServerStatus
command c arg inp = do
dat <- getServerData
(code,out,err) <- liftIO $ cmd dat c arg inp
let genCode ExitSuccess = 0
genCode (ExitFailure val) = val
return $ Exit (genCode code) <> Stdout out <> Stderr err