grapesy-1.0.0: test-stress/Test/Stress/Cmdline.hs
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Test.Stress.Cmdline
( -- * Types
Cmdline(..)
, Role(..)
, GlobalOpts(..)
-- ** Client-specific
, Connect(..)
, Exec(..)
, Call(..)
-- ** Server-specific
, Security(..)
, TlsOpts(..)
-- ** Auxiliary
, NotPretty(..)
-- * Parser
, getCmdline
) where
import Control.Applicative ((<|>))
import Data.Foldable (asum, toList)
import Data.Maybe (fromMaybe)
import GHC.Generics (Generic)
import Network.Socket (HostName, PortNumber)
import Options.Applicative qualified as Opt
import Text.Show.Pretty (PrettyVal(..), parseValue)
import Network.GRPC.Client qualified as Client
import Network.GRPC.Common
import Network.GRPC.Common.Compression (Compression)
import Network.GRPC.Common.Compression qualified as Compr
import Network.GRPC.Server.Run
import Paths_grapesy
{-------------------------------------------------------------------------------
Definitions
-------------------------------------------------------------------------------}
-- | Command specification
data Cmdline = Cmdline {
cmdRole :: Role
, cmdGlobalOpts :: GlobalOpts
}
deriving stock (Show, Generic)
deriving anyclass (PrettyVal)
-- | Should we run the client, servers, or both?
data Role =
-- | Run the clients
Client {
-- | Connect over TLS?
clientSecurity :: Maybe (NotPretty Client.ServerValidation)
, clientServerPort :: PortNumber
, clientConnects :: [Connect]
-- | Insist on this compression scheme for all messages
, clientCompression :: Maybe (NotPretty Compression)
}
-- | Run the server
| Server {
serverConfig :: ServerConfig
}
-- | Run the automatic stress test suite
| Driver {
driverWorkingDir :: Maybe FilePath
, driverDuration :: Int
, driverGenCharts :: Bool
}
deriving stock (Show, Generic)
deriving anyclass (PrettyVal)
-- | Connections to execute
data Connect = Connect {
-- | Execute the connections concurrently or sequentially?
connectExec :: Exec
-- | Number of connections
, connectNum :: Int
-- | Number of times to repeat the calls on the connection
, callNum :: Int
-- | Calls to make on the connections
, connectCalls :: [Call]
}
deriving stock (Show, Generic)
deriving anyclass (PrettyVal)
-- | Concurrent or sequential execution
data Exec = Concurrent | Sequential
deriving stock (Show, Generic)
deriving anyclass (PrettyVal)
-- | Types of RPCs
data Call =
-- | A single message back and forth
NonStreaming
-- | Client sends @N@ messages to the server
| ClientStreaming Int
-- | Server sends @N@ messages to the client
| ServerStreaming Int
-- | Client and server send @N@ messages to each other
| BiDiStreaming Int
deriving stock (Show, Generic)
deriving anyclass (PrettyVal)
data Security =
Insecure
| Secure
deriving stock (Show, Generic)
deriving anyclass (PrettyVal)
data TlsOpts = TlsOpts {
tlsPubCert :: FilePath
, tlsChainCerts :: [FilePath]
, tlsPrivKey :: FilePath
}
deriving stock (Show, Generic)
deriving anyclass (PrettyVal)
mkConfig :: Maybe TlsOpts -> HostName -> PortNumber -> ServerConfig
mkConfig mtls host port =
case mtls of
Just TlsOpts{..} ->
ServerConfig Nothing $ Just SecureConfig {
secureHost = host
, securePort = port
, securePubCert = tlsPubCert
, secureChainCerts = tlsChainCerts
, securePrivKey = tlsPrivKey
, secureSslKeyLog = SslKeyLogNone
}
Nothing ->
(`ServerConfig` Nothing) $ Just InsecureConfig {
insecureHost = Just host
, insecurePort = port
}
data GlobalOpts = GlobalOpts {
optsTracing :: Bool
}
deriving stock (Show, Generic)
deriving anyclass (PrettyVal)
-------------------------------------------------------------------------------
-- Top-level parsers
-------------------------------------------------------------------------------
getCmdline :: IO Cmdline
getCmdline = do
defaultPub <- getDataFileName "grpc-demo.pem"
defaultPriv <- getDataFileName "grpc-demo.key"
let info :: Opt.ParserInfo Cmdline
info = Opt.info
( parseCmdline defaultPub defaultPriv
Opt.<**> Opt.helper
)
Opt.fullDesc
Opt.execParser info
parseCmdline ::
FilePath
-> FilePath
-> Opt.Parser Cmdline
parseCmdline defaultPub defaultPriv =
Cmdline
<$> (parseRole defaultPub defaultPriv <|> pure (Driver Nothing 60 False))
<*> parseGlobalOpts
parseRole :: FilePath -> FilePath -> Opt.Parser Role
parseRole defaultPub defaultPriv = Opt.subparser $ mconcat [
sub "client" "Run the client" $
parseClientRole defaultPub
, sub "server" "Run the server" $
parseServerRole defaultPub defaultPriv
, sub "driver" "Run the stress test driver" $
parseDriverRole
]
-------------------------------------------------------------------------------
-- Client option parsers
-------------------------------------------------------------------------------
parseClientRole :: FilePath -> Opt.Parser Role
parseClientRole defaultPub =
Client
<$> (fmap WrapNotPretty <$> parseClientSecurity defaultPub)
<*> parseClientPort
<*> parseClientConnects
<*> Opt.optional (WrapNotPretty <$> parseCompression)
parseClientSecurity :: FilePath -> Opt.Parser (Maybe Client.ServerValidation)
parseClientSecurity defaultPub =
Opt.optional (
Opt.flag' () (mconcat [
Opt.long "secure"
, Opt.help "Connect over TLS"
])
*> parseServerValidation
)
where
parseServerValidation :: Opt.Parser Client.ServerValidation
parseServerValidation =
aux
<$> (Opt.switch $ mconcat [
Opt.long "no-server-validation"
, Opt.help "Skip server (certificate) validation"
])
<*> (Opt.switch $ mconcat [
Opt.long "cert-store-from-system"
, Opt.help "Enable the system certificate store"
])
<*> (Opt.option Opt.str $ mconcat [
Opt.long "cert-store-from-path"
, Opt.help "Load certificate store from file or directory (set to empty to disable)"
, Opt.metavar "PATH"
, Opt.value defaultPub
, Opt.showDefault
])
where
aux :: Bool -> Bool -> FilePath -> Client.ServerValidation
aux noServerValidation certStoreFromSystem certStoreFromPath =
if noServerValidation then
Client.NoServerValidation
else
Client.ValidateServer $ mconcat . concat $ [
[ Client.certStoreFromSystem
| certStoreFromSystem
]
, [ Client.certStoreFromPath certStoreFromPath
| not (null certStoreFromPath)
]
]
parseClientPort :: Opt.Parser PortNumber
parseClientPort =
Opt.option Opt.auto (mconcat [
Opt.long "port"
, Opt.help "Connect to the server at PORT"
, Opt.metavar "PORT"
])
parseClientConnects :: Opt.Parser [Connect]
parseClientConnects =
Opt.some $ Connect
<$> Opt.flag Sequential Concurrent (mconcat [
Opt.long "concurrent"
, Opt.help "Open connections concurrently"
])
<*> Opt.option Opt.auto (mconcat [
Opt.long "num-connections"
, Opt.help "Open N connections"
, Opt.metavar "N"
, Opt.value 1
, Opt.showDefault
])
<*> Opt.option Opt.auto (mconcat [
Opt.long "num-calls"
, Opt.help "Repeat the calls N times on each connection"
, Opt.metavar "N"
, Opt.value 1
, Opt.showDefault
])
<*> Opt.some parseCall
parseCall :: Opt.Parser Call
parseCall =
nonStreaming
<|> clientStreaming
<|> serverStreaming
<|> bidiStreaming
where
nonStreaming :: Opt.Parser Call
nonStreaming =
Opt.flag' NonStreaming $ mconcat [
Opt.long "non-streaming"
, Opt.help "Make a single non-streaming call"
]
clientStreaming :: Opt.Parser Call
clientStreaming =
ClientStreaming
<$> Opt.option Opt.auto (mconcat [
Opt.long "client-streaming"
, Opt.help "Stream N messages from the client"
, Opt.metavar "N"
])
serverStreaming :: Opt.Parser Call
serverStreaming =
ServerStreaming
<$> Opt.option Opt.auto (mconcat [
Opt.long "server-streaming"
, Opt.help "Stream N messages from the server"
, Opt.metavar "N"
])
bidiStreaming :: Opt.Parser Call
bidiStreaming =
BiDiStreaming
<$> Opt.option Opt.auto (mconcat [
Opt.long "bidi-streaming"
, Opt.help "Stream N messages from both the client and server"
, Opt.metavar "N"
])
parseCompression :: Opt.Parser Compression
parseCompression = asum $ map go (toList Compr.allSupportedCompression)
where
go :: Compression -> Opt.Parser Compression
go compr = Opt.flag' compr $ mconcat [
Opt.long comprId
, Opt.help $ "Insist on " ++ comprId ++ " compression "
]
where
comprId :: String
comprId = show (Compr.compressionId compr)
-------------------------------------------------------------------------------
-- Server option parsers
-------------------------------------------------------------------------------
parseServerRole :: FilePath -> FilePath -> Opt.Parser Role
parseServerRole defaultPub defaultPriv =
aux
<$> parseServerPort
<*> parseServerSecurity defaultPub defaultPriv
where
aux :: PortNumber -> Maybe TlsOpts -> Role
aux port mtls = Server $ mkConfig mtls "127.0.0.1" port
parseServerPort :: Opt.Parser PortNumber
parseServerPort =
Opt.option Opt.auto (mconcat [
Opt.long "port"
, Opt.help "Bind the server to port PORT"
, Opt.metavar "PORT"
])
parseServerSecurity :: FilePath -> FilePath -> Opt.Parser (Maybe TlsOpts)
parseServerSecurity defaultPub defaultPriv =
Opt.optional $
Opt.flag' () (mconcat [
Opt.long "secure"
, Opt.help "Enable TLS on the servers"
])
*> parseTlsOpts defaultPub defaultPriv
parseTlsOpts :: FilePath -> FilePath -> Opt.Parser TlsOpts
parseTlsOpts defaultPub defaultPriv =
TlsOpts
<$> Opt.strOption (mconcat [
Opt.long "tls-pub"
, Opt.help "TLS public certificate (X.509 format)"
, Opt.metavar "CERT_FILE"
, Opt.showDefault
, Opt.value defaultPub
])
<*> Opt.many (Opt.strOption $ mconcat [
Opt.long "tls-cert"
, Opt.metavar "CERT_FILE"
, Opt.help "TLS chain certificate (X.509 format)"
])
<*> Opt.strOption (mconcat [
Opt.long "tls-priv"
, Opt.metavar "KEY_FILE"
, Opt.help "TLS private key"
, Opt.showDefault
, Opt.value defaultPriv
])
-------------------------------------------------------------------------------
-- Driver option parsers
-------------------------------------------------------------------------------
parseDriverRole :: Opt.Parser Role
parseDriverRole =
Driver
<$> Opt.optional (Opt.strOption (mconcat [
Opt.long "working-dir"
, Opt.help "Write result files to this directory, must already exist"
, Opt.metavar "DIR"
]))
<*> Opt.option Opt.auto (mconcat [
Opt.long "duration"
, Opt.help "Run the stress tests for this many seconds"
, Opt.metavar "SECONDS"
, Opt.value 60
, Opt.showDefault
])
<*> Opt.switch (mconcat [
Opt.long "gen-charts"
, Opt.help "Generate heap profile charts for stable components"
])
-------------------------------------------------------------------------------
-- Internal auxiliary
-------------------------------------------------------------------------------
parseGlobalOpts :: Opt.Parser GlobalOpts
parseGlobalOpts =
GlobalOpts
<$> Opt.switch (mconcat [
Opt.long "verbose"
, Opt.short 'v'
, Opt.help "Trace test execution to stdout"
])
sub :: String -> String -> Opt.Parser a -> Opt.Mod Opt.CommandFields a
sub cmd desc parser =
Opt.command cmd $
Opt.info (parser Opt.<**> Opt.helper) (Opt.progDesc desc)
-------------------------------------------------------------------------------
-- Auxiliary: pretty-val
-------------------------------------------------------------------------------
newtype NotPretty a = WrapNotPretty { unwrapNotPretty :: a }
deriving newtype (Show)
instance Show a => PrettyVal (NotPretty a) where
prettyVal (WrapNotPretty x) =
fromMaybe
(error $ "prettyVal: could not parse " ++ show x)
(parseValue $ show x)
instance PrettyVal PortNumber where
prettyVal = prettyVal . (fromIntegral :: PortNumber -> Integer)
deriving anyclass instance PrettyVal ServerConfig
deriving anyclass instance PrettyVal InsecureConfig
deriving anyclass instance PrettyVal SecureConfig
deriving anyclass instance PrettyVal SslKeyLog