packages feed

wrecker-ui-2.3.0: src/Invoker.hs

{-# LANGUAGE RecordWildCards, NamedFieldPuns #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}

module Invoker where

import Control.Exception (SomeException, try)
import Control.Monad (foldM)
import Data.Aeson (eitherDecodeStrict)
import Data.Binary (Binary)
import qualified Data.ByteString as BS
import Data.Either (lefts, rights)
import Data.Text (Text, pack, unpack)
import Data.Typeable (Typeable)
import GHC.Generics
import Model (Rollup(..), WreckerRun(..))
import System.Environment (lookupEnv)
import System.IO.Temp (withSystemTempFile)
import System.Process (callProcess, readCreateProcess, shell)

-- | Represents the concurrency (number of threads) used to execute a run
newtype Concurrency =
    Concurrency Int
    deriving (Eq, Show, Typeable, Generic)

-- | The number of seconds to spend on a particular run.
newtype Seconds =
    Seconds Int
    deriving (Eq, Show, Typeable, Generic)

-- | Specifies a wrecker CLI command
data Command = Command
    { title :: Text
    , seconds :: Seconds
    , concurrency :: Concurrency
    } deriving (Eq, Show, Generic, Typeable)

-- We need these intances to serialize commands over the network
instance Binary Concurrency

instance Binary Seconds

instance Binary Command

-- | Represents the exit status of a wrecker invocation.
data Result
    = Success
    | ExecError [Text]
    | TooManyErrors
    deriving (Show)

-- | A helper function used to execute a group of wrecker invocations, given a list of concurrency levels.
--   This function is higly polymorphic, it only encodes the idea of repeatedly building a command, executing
--   it and recording its results. You have to provide the implementation of each of those individual steps
escalate ::
       Monad m
    => (Concurrency -> Command) -- ^ A function that given the concurrecy, will create a Command record
    -> (Command -> m [Either String WreckerRun]) -- ^ A function to execute the tests in wrecker
    -> (Command -> m key) -- ^ A function to create the run identifier
    -> (key -> [Either String WreckerRun] -> m ()) -- ^ A function to record the run
    -> [Concurrency] -- ^ The list of concurrency steps to execute in wrecker
    -> m Result
escalate builder executer keyGen recorder steps = foldM run Success steps
  where
    run prev step = do
        case prev of
            Success -> execution step
            err -> return err
    execution step = do
        let command = builder step
        key <- keyGen command
        results <- executer command
        recorder key results
        case lefts results of
            [] ->
                if isUnAcceptable (rights results)
                    then return TooManyErrors
                    else return Success
            errors -> return (ExecError (fmap pack errors))

-- | Whether or not there were too many errors when invoking a wrecker run
isUnAcceptable :: [WreckerRun] -> Bool
isUnAcceptable [] = False
-- | A run is not acceptable if more than 1% of the hits were errors
isUnAcceptable results =
    let (errors, hits) = foldr extract (0, 0) results
    in errors / hits > (0.01 :: Double)
  where
    extract (WreckerRun {rollup = Rollup {..}}) (errors, hits) =
        ( errors + fromIntegral (rollupUserErrorHits + rollupServerErrorHits + rollupFailedHits)
        , hits + fromIntegral rollupHits)

-- | Invokes the wrecker command line tool with the given options
wrecker :: Command -> IO (Either String WreckerRun)
wrecker (Command title (Seconds secs) (Concurrency conc)) = do
    res <- try runScript
    case res :: Either SomeException (Either String WreckerRun) of
        Left err -> return (Left (show err))
        Right runResult -> return runResult
  where
    runScript = do
        executable <- findExecutable
        withSystemTempFile "wrecker.results" $ \file handle -> do
            callProcess
                executable
                [ "--concurrency"
                , show conc
                , "--log-level"
                , "LevelInfo"
                , "--match"
                , unpack title
                , "--run-timed"
                , show secs
                , "--output-path"
                , file
                ]
            contents <- BS.hGetContents handle
            return (eitherDecodeStrict contents)

-- | Returns the list of tests that are executable in the wrecker CLI
listGroups :: IO [String]
listGroups = do
    executable <- findExecutable
    output <- readCreateProcess (shell $ executable ++ " --list-test-groups") ""
    let each = lines output
    return (fmap (dropWhile (\c -> c `elem` notMeaningfulChars)) each)
  where
    notMeaningfulChars = '.' : '>' : ' ' : ['0' .. '9']

-- | Returns the default executable name or the one present in the WRECKER_EXECUTABLE env var
findExecutable :: IO String
findExecutable = do
    executable <- lookupEnv "WRECKER_EXECUTABLE"
    return (maybe "wrecker" id executable)