{-# LANGUAGE GADTs, Rank2Types, OverloadedStrings #-}
module Web.Api.WebDriver.Monad (
WebDriver
, execWebDriver
, debugWebDriver
, checkWebDriver
, WebDriverT()
, execWebDriverT
, debugWebDriverT
, checkWebDriverT
, liftWebDriverT
, IdentityT(..)
, evalWDAct
, Http.evalIO
, evalWDActMockIO
, Http.evalMockIO
, WebDriverConfig(..)
, defaultWebDriverConfig
, defaultWebDriverState
, defaultWebDriverEnvironment
, defaultWDEnv
, defaultWebDriverLogOptions
, fromState
, modifyState
, fromEnv
, comment
, wait
, throwError
, throwJsonError
, throwHttpException
, throwIOException
, expect
, assert
, catchError
, catchJsonError
, catchHttpException
, catchIOException
, parseJson
, lookupKeyJson
, constructFromJson
, httpGet
, httpSilentGet
, httpPost
, httpSilentPost
, httpDelete
, httpSilentDelete
, hPutStrLn
, hPutStrLnBlocking
, readFilePath
, writeFilePath
, fileExists
, Http.E(..)
, Http.JsonError(..)
, WDError(..)
, Http.R(..)
, Http.LogOptions(..)
, WDEnv(..)
, ResponseFormat(..)
, ApiVersion(..)
, Outcome(..)
, Http.Url
, Http.HttpResponse(..)
, WDLog(..)
, Http.P(..)
, WDAct(..)
, Http.S(..)
, WDState(..)
, getAssertions
, Http.logEntries
) where
import Prelude hiding (readFile, writeFile)
import Control.Concurrent.MVar
( MVar )
import Control.Exception
( IOException, try )
import Control.Lens
( (^.), (^?) )
import Control.Monad
( ap )
import Data.Aeson
( Value(), Result(Success), toJSON, (.=), FromJSON, fromJSON, object )
import Data.Aeson.Encode.Pretty
( encodePretty )
import Data.Aeson.Lens
( key, _Value, _String )
import qualified Data.ByteString.Char8 as SC
( unpack )
import Data.ByteString.Lazy
( ByteString, readFile, writeFile )
import qualified Data.ByteString.Lazy.Char8 as LC
( unpack, pack )
import Data.Functor.Identity
( Identity(..) )
import Data.IORef
( IORef, newIORef, readIORef, writeIORef )
import qualified Data.Map.Strict as M
( Map, fromList )
import Data.Text
( pack, unpack, Text )
import qualified Network.HTTP.Client as N
( HttpException(..), HttpExceptionContent(..) )
import Network.Wreq
( Status, statusMessage, statusCode, responseStatus, defaults )
import System.Directory
( doesFileExist )
import System.IO
( Handle, hGetLine, hSetEcho, hGetEcho, hFlush, stdout, stdin )
import System.IO.Error
( eofErrorType, doesNotExistErrorType, mkIOError )
import Test.QuickCheck
( Property )
import qualified Control.Monad.Script.Http as Http
import qualified Data.MockIO as Mock
import qualified Data.MockIO.FileSystem as FS
import Web.Api.WebDriver.Types
import Web.Api.WebDriver.Assert
newtype WebDriverT m a = WDT
{ unWDT :: Http.HttpT WDError WDEnv WDLog WDState WDAct m a }
instance Functor (WebDriverT m) where
fmap f = WDT . fmap f . unWDT
instance Applicative (WebDriverT m) where
pure = return
(<*>) = ap
instance Monad (WebDriverT m) where
return = WDT . return
(WDT x) >>= f = WDT (x >>= (unWDT . f))
liftWebDriverT :: (Monad m) => m a -> WebDriverT m a
liftWebDriverT = WDT . Http.liftHttpT
data WebDriverConfig eff = WDConfig
{ _initialState :: Http.S WDState
, _environment :: Http.R WDError WDLog WDEnv
, _evaluator :: forall a. Http.P WDAct a -> eff a
}
defaultWebDriverConfig :: WebDriverConfig IO
defaultWebDriverConfig = WDConfig
{ _initialState = defaultWebDriverState
, _environment = defaultWebDriverEnvironment
, _evaluator = Http.evalIO evalWDAct
}
defaultWebDriverState :: Http.S WDState
defaultWebDriverState = Http.S
{ Http._httpOptions = defaults
, Http._httpSession = Nothing
, Http._userState = WDState
{ _sessionId = Nothing
}
}
defaultWebDriverEnvironment :: Http.R WDError WDLog WDEnv
defaultWebDriverEnvironment = Http.R
{ Http._logHandle = stdout
, Http._logLock = Nothing
, Http._uid = ""
, Http._logOptions = defaultWebDriverLogOptions
, Http._httpErrorInject = promoteHttpResponseError
, Http._env = defaultWDEnv
}
defaultWDEnv :: WDEnv
defaultWDEnv = WDEnv
{ _remoteHostname = "localhost"
, _remotePort = 4444
, _remotePath = ""
, _dataPath = ""
, _responseFormat = SpecFormat
, _apiVersion = CR_2018_03_04
, _stdin = stdin
, _stdout = stdout
}
defaultWebDriverLogOptions :: Http.LogOptions WDError WDLog
defaultWebDriverLogOptions = Http.trivialLogOptions
{ Http._logColor = True
, Http._logJson = True
, Http._logHeaders = False
, Http._logSilent = False
, Http._printUserError = printWDError
, Http._printUserLog = printWDLog
}
execWebDriverT
:: (Monad eff, Monad (m eff))
=> WebDriverConfig eff
-> (forall u. eff u -> m eff u)
-> WebDriverT (m eff) a
-> m eff (Either (Http.E WDError) a, Http.S WDState, Http.W WDError WDLog)
execWebDriverT config lift = Http.execHttpTM
(_initialState config) (_environment config) (_evaluator config) lift . unWDT
debugWebDriverT
:: (Monad eff, Monad (m eff))
=> WebDriverConfig eff
-> (forall u. eff u -> m eff u)
-> WebDriverT (m eff) a
-> m eff (Either String a, AssertionSummary)
debugWebDriverT config lift session = do
(result, _, w) <- execWebDriverT config lift session
let output = case result of
Right a -> Right a
Left e -> Left $ Http.printError (printWDError True) e
return (output, summarize $ getAssertions $ Http.logEntries w)
checkWebDriverT
:: (Monad eff, Monad (m eff))
=> WebDriverConfig eff
-> (forall u. eff u -> m eff u)
-> (m eff (Either (Http.E WDError) t, Http.S WDState, Http.W WDError WDLog) -> IO q)
-> (q -> Bool)
-> WebDriverT (m eff) t
-> Property
checkWebDriverT config lift cond check =
Http.checkHttpTM
(_initialState config)
(_environment config)
(_evaluator config)
lift cond check . unWDT
type WebDriver eff a = WebDriverT (IdentityT eff) a
newtype IdentityT m a = IdentityT { runIdentityT :: m a }
instance (Monad m) => Monad (IdentityT m) where
return = IdentityT . return
x >>= f = IdentityT $ runIdentityT x >>= (runIdentityT . f)
instance (Functor m) => Functor (IdentityT m) where
fmap f = IdentityT . fmap f . runIdentityT
instance (Monad m) => Applicative (IdentityT m) where
pure = return
(<*>) = ap
execWebDriver
:: (Monad eff)
=> WebDriverConfig eff
-> WebDriver eff a
-> eff (Either (Http.E WDError) a, Http.S WDState, Http.W WDError WDLog)
execWebDriver config = runIdentityT . execWebDriverT config IdentityT
debugWebDriver
:: (Monad eff)
=> WebDriverConfig eff
-> WebDriver eff a
-> eff (Either String a, AssertionSummary)
debugWebDriver config session = do
(result, _, w) <- execWebDriver config session
let output = case result of
Right a -> Right a
Left e -> Left $ Http.printError (printWDError True) e
return (output, summarize $ getAssertions $ Http.logEntries w)
checkWebDriver
:: (Monad eff)
=> WebDriverConfig eff
-> (eff (Either (Http.E WDError) t, Http.S WDState, Http.W WDError WDLog) -> IO q)
-> (q -> Bool)
-> WebDriver eff t
-> Property
checkWebDriver config cond = checkWebDriverT config IdentityT (cond . runIdentityT)
fromState :: (Http.S WDState -> a) -> WebDriverT m a
fromState = WDT . Http.gets
modifyState :: (Http.S WDState -> Http.S WDState) -> WebDriverT m ()
modifyState = WDT . Http.modify
fromEnv :: (Http.R WDError WDLog WDEnv -> a) -> WebDriverT m a
fromEnv = WDT . Http.reader
logEntry :: WDLog -> WebDriverT m ()
logEntry = WDT . Http.logEntry
comment :: String -> WebDriverT m ()
comment = WDT . Http.comment
wait :: Int -> WebDriverT m ()
wait = WDT . Http.wait
throwError :: WDError -> WebDriverT m a
throwError = WDT . Http.throwError
throwJsonError :: Http.JsonError -> WebDriverT m a
throwJsonError = WDT . Http.throwJsonError
throwHttpException :: N.HttpException -> WebDriverT m a
throwHttpException = WDT . Http.throwHttpException
throwIOException :: IOException -> WebDriverT m a
throwIOException = WDT . Http.throwIOException
catchError :: WebDriverT m a -> (WDError -> WebDriverT m a) -> WebDriverT m a
catchError x h = WDT $ Http.catchError (unWDT x) (unWDT . h)
catchJsonError :: WebDriverT m a -> (Http.JsonError -> WebDriverT m a) -> WebDriverT m a
catchJsonError x h = WDT $ Http.catchJsonError (unWDT x) (unWDT . h)
catchHttpException :: WebDriverT m a -> (N.HttpException -> WebDriverT m a) -> WebDriverT m a
catchHttpException x h = WDT $ Http.catchHttpException (unWDT x) (unWDT . h)
catchIOException :: WebDriverT m a -> (IOException -> WebDriverT m a) -> WebDriverT m a
catchIOException x h = WDT $ Http.catchIOException (unWDT x) (unWDT . h)
parseJson :: ByteString -> WebDriverT m Value
parseJson = WDT . Http.parseJson
lookupKeyJson :: Text -> Value -> WebDriverT m Value
lookupKeyJson key = WDT . Http.lookupKeyJson key
constructFromJson :: (FromJSON a) => Value -> WebDriverT m a
constructFromJson = WDT . Http.constructFromJson
httpGet :: Http.Url -> WebDriverT m Http.HttpResponse
httpGet = WDT . Http.httpGet
httpSilentGet :: Http.Url -> WebDriverT m Http.HttpResponse
httpSilentGet = WDT . Http.httpSilentGet
httpPost :: Http.Url -> ByteString -> WebDriverT m Http.HttpResponse
httpPost url = WDT . Http.httpPost url
httpSilentPost :: Http.Url -> ByteString -> WebDriverT m Http.HttpResponse
httpSilentPost url = WDT . Http.httpSilentPost url
httpDelete :: Http.Url -> WebDriverT m Http.HttpResponse
httpDelete = WDT . Http.httpDelete
httpSilentDelete :: Http.Url -> WebDriverT m Http.HttpResponse
httpSilentDelete = WDT . Http.httpSilentDelete
hPutStrLn :: Handle -> String -> WebDriverT m ()
hPutStrLn h = WDT . Http.hPutStrLn h
hPutStrLnBlocking :: MVar () -> Handle -> String -> WebDriverT m ()
hPutStrLnBlocking lock h = WDT . Http.hPutStrLnBlocking lock h
promptWDAct :: WDAct a -> WebDriverT m a
promptWDAct = WDT . Http.prompt . Http.P
instance Assert (WebDriverT m) where
assert = logEntry . LogAssertion
getAssertions :: [WDLog] -> [Assertion]
getAssertions xs = get xs
where
get [] = []
get (w:ws) = case w of
LogAssertion a -> a : get ws
_ -> get ws
data WDError
= NoSession
| ResponseError ResponseErrorCode String String (Maybe Value) Status
| UnableToConnect
| RemoteEndTimeout
| UnhandledHttpException N.HttpException
| ImageDecodeError String
| UnexpectedValue String
| UnexpectedResult Outcome String
deriving Show
data WDEnv = WDEnv
{
_remoteHostname :: String
, _remotePort :: Int
, _remotePath :: String
, _dataPath :: FilePath
, _responseFormat :: ResponseFormat
, _apiVersion :: ApiVersion
, _stdin :: Handle
, _stdout :: Handle
}
data ApiVersion
= CR_2018_03_04
deriving (Eq, Show)
data ResponseFormat
= SpecFormat
| ChromeFormat
deriving (Eq, Show)
newtype WDState = WDState
{ _sessionId :: Maybe String
} deriving Show
data WDLog
= LogAssertion Assertion
| LogSession SessionVerb
| LogUnexpectedResult Outcome String
deriving Show
printWDLog :: Bool -> WDLog -> String
printWDLog _ w = show w
data Outcome = IsSuccess | IsFailure
deriving (Eq, Show)
data SessionVerb
= Close | Open
deriving (Eq, Show)
data WDAct a where
ReadFilePath :: FilePath -> WDAct (Either IOException ByteString)
WriteFilePath :: FilePath -> ByteString -> WDAct (Either IOException ())
FileExists :: FilePath -> WDAct (Either IOException Bool)
HGetLine :: Handle -> WDAct (Either IOException String)
HGetLineNoEcho :: Handle -> WDAct (Either IOException String)
expect
:: (Monad m, Eq a, Show a)
=> a
-> a
-> WebDriverT m a
expect x y = if x == y
then return y
else throwError $ UnexpectedValue $
"expected " ++ show x ++ " but got " ++ show y
promoteHttpResponseError :: N.HttpException -> Maybe WDError
promoteHttpResponseError e = case e of
N.HttpExceptionRequest _ (N.StatusCodeException s r) -> do
err <- r ^? key "value" . key "error" . _Value
code <- case fromJSON err of
Success m -> return m
_ -> Nothing
msg <- fmap unpack (r ^? key "value" . key "message" . _String)
str <- fmap unpack (r ^? key "value" . key "stacktrace" . _String)
let obj = r ^? key "value" . key "data" . _Value
status <- s ^? responseStatus
return $ ResponseError code msg str obj status
N.HttpExceptionRequest _ (N.ConnectionFailure _) -> Just UnableToConnect
N.HttpExceptionRequest _ N.ConnectionTimeout -> Just RemoteEndTimeout
_ -> Just $ UnhandledHttpException e
printWDError :: Bool -> WDError -> String
printWDError json e = case e of
NoSession -> "No session in progress"
ResponseError code msg trace obj status ->
let
code = status ^. statusCode
smsg = status ^. statusMessage
in
(("Response: " ++ show code ++ " " ++ SC.unpack smsg ++ "\n") ++) $
LC.unpack $ encodePretty $ object
[ "error" .= toJSON code
, "message" .= toJSON msg
, "stacktrace" .= toJSON trace
, "data" .= (toJSON <$> obj)
]
UnableToConnect -> "Unable to connect to WebDriver server"
RemoteEndTimeout -> "Remote End Timeout"
UnhandledHttpException e -> "Unhandled HTTP Exception: " ++ show e
ImageDecodeError msg -> "Image decode: " ++ msg
UnexpectedValue msg -> "Unexpected value: " ++ msg
UnexpectedResult r msg -> case r of
IsSuccess -> "Unexpected success: " ++ msg
IsFailure -> "Unexpected failure: " ++ msg
promptForString
:: (Monad m)
=> String
-> WebDriverT m String
promptForString prompt = do
outH <- fromEnv (_stdout . Http._env)
inH <- fromEnv (_stdin . Http._env)
hPutStrLn outH prompt
result <- promptWDAct $ HGetLine inH
case result of
Right string -> return string
Left e -> throwIOException e
promptForSecret
:: (Monad m)
=> String
-> WebDriverT m String
promptForSecret prompt = do
outH <- fromEnv (_stdout . Http._env)
inH <- fromEnv (_stdin . Http._env)
hPutStrLn outH prompt
result <- promptWDAct $ HGetLineNoEcho inH
case result of
Right string -> return string
Left e -> throwIOException e
readFilePath
:: (Monad m)
=> FilePath
-> WebDriverT m ByteString
readFilePath path = do
result <- promptWDAct $ ReadFilePath path
case result of
Right bytes -> return bytes
Left e -> throwIOException e
writeFilePath
:: (Monad m)
=> FilePath
-> ByteString
-> WebDriverT m ()
writeFilePath path bytes = do
result <- promptWDAct $ WriteFilePath path bytes
case result of
Right () -> return ()
Left e -> throwIOException e
fileExists
:: (Monad m)
=> FilePath
-> WebDriverT m Bool
fileExists path = do
result <- promptWDAct $ FileExists path
case result of
Right p -> return p
Left e -> throwIOException e
evalWDAct :: WDAct a -> IO a
evalWDAct act = case act of
ReadFilePath path -> try $ readFile path
WriteFilePath path bytes -> try $ writeFile path bytes
FileExists path -> try $ doesFileExist path
HGetLine handle -> try $ do
hFlush handle
hGetLine handle
HGetLineNoEcho handle -> try $ do
hFlush handle
echo <- hGetEcho handle
hSetEcho handle False
secret <- hGetLine handle
hSetEcho handle echo
return secret
evalWDActMockIO :: WDAct a -> Mock.MockIO u a
evalWDActMockIO act = case act of
ReadFilePath path -> do
Mock.incrementTimer 1
world <- Mock.getMockWorld
let result = FS.getLines (Left path) $ Mock._files world
case result of
Nothing -> do
return $ Left $ mkIOError doesNotExistErrorType "" Nothing (Just path)
Just lns -> return $ Right $ LC.pack $ unlines lns
WriteFilePath path bytes -> do
Mock.incrementTimer 1
fmap Right $ Mock.modifyMockWorld $ \w -> w
{ Mock._files = FS.writeLines (Left path) [LC.unpack bytes] $ Mock._files w }
FileExists path -> do
Mock.incrementTimer 1
world <- Mock.getMockWorld
return $ Right $ FS.fileExists (Left path) $ Mock._files world
HGetLine handle -> do
Mock.incrementTimer 1
world <- Mock.getMockWorld
let dne = mkIOError doesNotExistErrorType "" (Just handle) Nothing
let eof = mkIOError eofErrorType "" (Just handle) Nothing
let result = FS.readLine dne eof (Right handle) $ Mock._files world
case result of
Left err -> return $ Left err
Right (str, fs) -> do
Mock.modifyMockWorld $ \w -> w { Mock._files = fs }
return $ Right str
HGetLineNoEcho handle -> do
Mock.incrementTimer 1
world <- Mock.getMockWorld
let dne = mkIOError doesNotExistErrorType "" (Just handle) Nothing
let eof = mkIOError eofErrorType "" (Just handle) Nothing
let result = FS.readLine dne eof (Right handle) $ Mock._files world
case result of
Left err -> return $ Left err
Right (str, fs) -> do
Mock.modifyMockWorld $ \w -> w { Mock._files = fs }
return $ Right str