packages feed

hspec-meta-2.11.18: hspec-core/src/Test/Hspec/Core/Runner/Eval.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Test.Hspec.Core.Runner.Eval (
  EvalConfig(..)
, ColorMode(..)
, EvalTree
, Tree(..)
, EvalItem(..)
, Concurrency(..)
, runFormatter
#ifdef TEST
, mergeResults
#endif
) where

import           Prelude ()
import           Test.Hspec.Core.Compat hiding (Monad)

import           Control.Monad.IO.Class (liftIO)
import           Control.Monad.Trans.Reader

import           Test.Hspec.Core.Util
import           Test.Hspec.Core.Spec (Progress, FailureReason(..), Result(..), ResultStatus(..), ProgressCallback)
import           Test.Hspec.Core.Timer
import           Test.Hspec.Core.Format (Format)
import qualified Test.Hspec.Core.Format as Format
import           Test.Hspec.Core.Clock
import           Test.Hspec.Core.Example.Location
import           Test.Hspec.Core.Example (safeEvaluateResultStatus, exceptionToResult)

import qualified Data.List.NonEmpty as NonEmpty
import           Data.List.NonEmpty (NonEmpty(..))

import           DList (DList(..))
import qualified DList

import           Test.Hspec.Core.Runner.JobQueue (JobQueue, Open, Concurrency(..), AbortEarly(..), Report(..))
import qualified Test.Hspec.Core.Runner.JobQueue as JobQueue

data Tree c a =
    Node !String !(NonEmpty (Tree c a))
  | NodeWithCleanup !(Maybe (String, Location)) !c !(NonEmpty (Tree c a))
  | Leaf !a
  deriving (Eq, Show, Functor, Foldable, Traversable)

data EvalConfig = EvalConfig {
  evalConfigFormat :: Format
, evalConfigConcurrentJobs :: Int
, evalConfigFailFast :: Bool
, evalConfigColorMode :: ColorMode
}

data ColorMode = ColorDisabled | ColorEnabled

data Env = Env {
  envConfig :: EvalConfig
, envResults :: IORef [(Path, Format.Item)]
}

formatEvent :: Format.Event -> EvalM ()
formatEvent event = do
  format <- asks $ evalConfigFormat . envConfig
  liftIO $ format event

type EvalM = ReaderT Env IO

addResult :: Path -> Format.Item -> EvalM ()
addResult path item = do
  ref <- asks envResults
  liftIO $ modifyIORef ref ((path, item) :)

reportItemDone :: Path -> Format.Item -> EvalM ()
reportItemDone path item = do
  addResult path item
  formatEvent $ Format.ItemDone path item

isFailure :: Result -> Bool
isFailure r = case resultStatus r of
  Success{} -> False
  Pending{} -> False
  Failure{} -> True

formatResult :: Path -> Maybe Location -> (Seconds, Result) -> EvalM ()
formatResult path loc (duration, result) = do
  mode <- asks (evalConfigColorMode . envConfig)
  case result of
    Result info status -> reportItemDone path $ Format.Item loc duration info $ case status of
      Success                      -> Format.Success
      Pending loc_ reason          -> Format.Pending loc_ reason
      Failure loc_ err@(Error _ e) -> Format.Failure (loc_ <|> extractLocation e) err
      Failure loc_ err             -> Format.Failure loc_ $ case mode of
        ColorEnabled -> err
        ColorDisabled -> case err of
          NoReason -> err
          Reason _ -> err
          ExpectedButGot _ _ _ -> err
          ColorizedReason r -> Reason (stripAnsi r)
#if __GLASGOW_HASKELL__ < 900
          Error _ _ -> err
#endif

data EvalItem = EvalItem {
  evalItemDescription :: String
, evalItemLocation :: Maybe Location
, evalItemConcurrency :: Concurrency
, evalItemAction :: ProgressCallback -> IO Result
}

type EvalTree = Tree (IO ()) EvalItem

-- | Evaluate all examples of a given spec and produce a report.
runFormatter :: EvalConfig -> [EvalTree] -> IO [(Path, Format.Item)]
runFormatter config specs = do
  queue <- JobQueue.new
  enqueuedSpecs <- enqueueItems queue specs
  jobs <- JobQueue.finalize queue
  withTimer 0.05 $ \ timer -> do
    env <- mkEnv
    let
      applyReportResult :: Item -> WithReportResult () Item
      applyReportResult =  WithReportResult formatResult

      runningSpecs :: [Tree () (WithReportResult_ Item)]
      runningSpecs = applyCleanup abortEarly $ map (fmap applyReportResult) enqueuedSpecs

      runEvalM :: EvalM a -> IO a
      runEvalM = flip runReaderT env

      eval :: forall item progress a. ([String] -> item -> DList (Report progress a)) -> [(Tree () item)] -> [Report progress a]
      eval evalItem = DList.toList . foldMap foldSpec
        where
          foldSpec :: Tree () item -> DList (Report progress a)
          foldSpec = foldTree FoldTree {
            onGroupStarted = DList.singleton . Report . groupStarted
          , onGroupDone = DList.singleton . Report . groupDone
          , onCleanup
          , onLeaf = evalItem
          }

          groupStarted :: Path -> IO ()
          groupStarted = format . Format.GroupStarted

          groupDone :: Path -> IO ()
          groupDone = format . Format.GroupDone

          onCleanup :: Maybe (String, Location) -> [String] -> () -> DList (Report progress a)
          onCleanup _loc _groups () = mempty

      items :: [Report Progress Result]
      items = eval evalItem runningSpecs
        where
          evalItem :: [String] -> WithReportResult_ Item -> DList (Report Progress Result)
          evalItem groups (WithReportResult reportResult (Item requirement loc action)) = DList $
              (Report (reportItemStarted path) :)
            . (ReportResult action progress result :)
            where
              path :: Path
              path = (groups, requirement)

              progress :: ProgressCallback
              progress = reportProgress timer path

              result :: (Seconds, Either SomeException Result) -> IO AbortEarly
              result = traverse (either exceptionToResult return) >=> runEvalM . reportResult path loc

          reportItemStarted :: Path -> IO ()
          reportItemStarted = format . Format.ItemStarted

      abortEarly :: Result -> Bool
      abortEarly result = evalConfigFailFast config && isFailure result

      getResults :: IO [(Path, Format.Item)]
      getResults = reverse <$> readIORef (envResults env)

      formatItems :: IO ()
      formatItems = JobQueue.run (evalConfigConcurrentJobs config) jobs items

      formatDone :: IO ()
      formatDone = getResults >>= format . Format.Done

    format Format.Started
    formatItems `finally` formatDone
    getResults
  where
    mkEnv :: IO Env
    mkEnv = Env config <$> newIORef []

    format :: Format
    format = evalConfigFormat config

    reportProgress :: IO Bool -> Path -> Progress -> IO ()
    reportProgress timer path progress = do
      r <- timer
      when r $ do
        format (Format.Progress path progress)

type ReportResult abort = Path -> Maybe Location -> (Seconds, Result) -> EvalM abort

data WithReportResult abort a = WithReportResult {
  _reportResult :: ReportResult abort
, _item :: a
}

type WithReportResult_ = WithReportResult AbortEarly

data Item = Item {
  itemDescription :: String
, itemLocation :: Maybe Location
, itemAction :: JobQueue.Result Progress Result
}

applyFailFast :: (Result -> Bool) -> Tree c (WithReportResult () a) -> Tree c (WithReportResult_ a)
applyFailFast abortEarly = fmap applyToWithReportResult
  where
    applyToWithReportResult :: WithReportResult () a -> WithReportResult_ a
    applyToWithReportResult (WithReportResult report a) = WithReportResult (applyToReportResult report) a

    applyToReportResult :: ReportResult () -> ReportResult AbortEarly
    applyToReportResult report path loc result@(_, r) = do
      report path loc result
      return $ if abortEarly r then AbortEarly else NoAbortEarly

type Children c a = NonEmpty (Tree c (WithReportResult_ a))

applyCleanup :: (Result -> Bool) -> [Tree (IO ()) (WithReportResult () a)] -> [Tree () (WithReportResult_ a)]
applyCleanup abortEarly = map (go . applyFailFast abortEarly)
  where
    go :: Tree (IO ()) (WithReportResult_ a) -> Tree () (WithReportResult_ a)
    go t = case t of
      Node label xs -> Node label (go <$> xs)
      NodeWithCleanup loc cleanup xs -> NodeWithCleanup loc () (go <$> apply loc cleanup xs)
      Leaf a -> Leaf a

    apply :: Maybe (String, Location) -> IO () -> Children c a -> Children c a
    apply loc cleanup = forEachLeaf (addCleanupOn abortEarly) . forLastLeaf (addCleanupOn (not . abortEarly))
      where
        addCleanupOn :: (Result -> Bool) -> WithReportResult abort a -> WithReportResult abort a
        addCleanupOn p (WithReportResult report item) = WithReportResult (addCleanup p loc cleanup report) item

forEachLeaf :: (a -> b) -> NonEmpty (Tree c a) -> NonEmpty (Tree c b)
forEachLeaf f = fmap (fmap f)

forLastLeaf :: (a -> a) -> NonEmpty (Tree c a) -> NonEmpty (Tree c a)
forLastLeaf p = go
  where
    go = NonEmpty.reverse . mapHead goNode . NonEmpty.reverse

    goNode node = case node of
      Node description xs -> Node description (go xs)
      NodeWithCleanup loc_ c xs -> NodeWithCleanup loc_ c (go xs)
      Leaf item -> Leaf (p item)

mapHead :: (a -> a) -> NonEmpty a -> NonEmpty a
mapHead f xs = case xs of
  y :| ys -> f y :| ys

addCleanup :: (Result -> Bool) -> Maybe (String, Location) -> IO () -> ReportResult abort -> ReportResult abort
addCleanup shouldRunCleanup loc cleanup reportProgress path l result@(t1, r1) = do
  if shouldRunCleanup r1 then do
    (t2, r2) <- liftIO $ measure $ safeEvaluateResultStatus (cleanup >> return Success)
    let t = t1 + t2
    reportProgress path l $ (t, mergeResults loc r1 r2)
  else do
    reportProgress path l result

mergeResults :: Maybe (String, Location) -> Result -> ResultStatus -> Result
mergeResults mCallSite (Result info r1) r2 = Result info $ case (r1, r2) of
  (_, Success) -> r1
  (Failure{}, _) -> r1
  (Pending{}, Pending{}) -> r1
  (Success, Pending{}) -> r2
  (_, Failure mLoc err) -> Failure (mLoc <|> hookLoc) $ case err of
    Error message e -> Error (message <|> hookFailed) e
    _ -> err
  where
    hookLoc = snd <$> mCallSite
    hookFailed = case mCallSite of
      Just (name, _) -> Just $ "in " ++ name ++ "-hook:"
      Nothing -> Nothing

enqueueItems :: JobQueue Open Progress Result -> [Tree c EvalItem] -> IO [Tree c Item]
enqueueItems queue = mapM (traverse $ enqueueItem queue)

enqueueItem :: JobQueue Open Progress Result -> EvalItem -> IO Item
enqueueItem queue EvalItem{..} = do
  job <- JobQueue.enqueue queue evalItemConcurrency evalItemAction
  return Item {
    itemDescription = evalItemDescription
  , itemLocation = evalItemLocation
  , itemAction = job
  }

data FoldTree c a r = FoldTree {
  onGroupStarted :: Path -> r
, onGroupDone :: Path -> r
, onCleanup :: Maybe (String, Location) -> [String] -> c -> r
, onLeaf :: [String] -> a -> r
}

foldTree :: Monoid r => FoldTree c a r -> Tree c a -> r
foldTree FoldTree{..} = go []
  where
    go rGroups (Node group xs) = start <> children <> done
      where
        path = (reverse rGroups, group)
        start = onGroupStarted path
        children = foldMap (go (group : rGroups)) xs
        done =  onGroupDone path
    go rGroups (NodeWithCleanup loc action xs) = children <> cleanup
      where
        children = foldMap (go rGroups) xs
        cleanup = onCleanup loc (reverse rGroups) action
    go rGroups (Leaf a) = onLeaf (reverse rGroups) a