-- | This module contains the core definitions related to ingredients.
--
-- Ingredients themselves are provided by other modules (usually under
-- the @Test.Tasty.Ingredients.*@ hierarchy).
module Test.Tasty.Ingredients
  ( Ingredient(..)
  , tryIngredients
  , ingredientOptions
  , ingredientsOptions
  , suiteOptions
  , composeReporters
  ) where

import Control.Monad
import Data.Proxy
import qualified Data.Foldable as F

import Test.Tasty.Core
import Test.Tasty.Run
import Test.Tasty.Options
import Test.Tasty.Options.Core
import Control.Concurrent.Async (concurrently)

-- | 'Ingredient's make your test suite tasty.
--
-- Ingredients represent different actions that you can perform on your
-- test suite. One obvious ingredient that you want to include is
-- one that runs tests and reports the progress and results.
--
-- Another standard ingredient is one that simply prints the names of all
-- tests.
--
-- Similar to test providers (see 'IsTest'), every ingredient may specify
-- which options it cares about, so that those options are presented to
-- the user if the ingredient is included in the test suite.
--
-- An ingredient can choose, typically based on the 'OptionSet', whether to
-- run. That's what the 'Maybe' is for. The first ingredient that agreed to
-- run does its work, and the remaining ingredients are ignored. Thus, the
-- order in which you arrange the ingredients may matter.
--
-- Usually, the ingredient which runs the tests is unconditional and thus
-- should be placed last in the list. Other ingredients usually run only
-- if explicitly requested via an option. Their relative order thus doesn't
-- matter.
--
-- That's all you need to know from an (advanced) user perspective. Read
-- on if you want to create a new ingredient.
--
-- There are two kinds of ingredients.
--
-- The first kind is 'TestReporter'. If the ingredient that agrees to run
-- is a 'TestReporter', then tasty will automatically launch the tests and
-- pass a 'StatusMap' to the ingredient. All the ingredient needs to do
-- then is to process the test results and probably report them to the user
-- in some way (hence the name).
--
-- 'TestManager' is the second kind of ingredient. It is typically used for
-- test management purposes (such as listing the test names), although it
-- can also be used for running tests (but, unlike 'TestReporter', it has
-- to launch the tests manually if it wants them to be run).  It is
-- therefore more general than 'TestReporter'. 'TestReporter' is provided
-- just for convenience.
--
-- The function's result should indicate whether all the tests passed.
--
-- In the 'TestManager' case, it's up to the ingredient author to decide
-- what the result should be. When no tests are run, the result should
-- probably be 'True'. Sometimes, even if some tests run and fail, it still
-- makes sense to return 'True'.
data Ingredient
  = TestReporter
      [OptionDescription]
      (OptionSet -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool)))
   -- ^ For the explanation on how the callback works, see the
   -- documentation for 'launchTestTree'.
  | TestManager
      [OptionDescription]
      (OptionSet -> TestTree -> Maybe (IO Bool))

-- | Try to run an 'Ingredient'.
--
-- If the ingredient refuses to run (usually based on the 'OptionSet'),
-- the function returns 'Nothing'.
--
-- For a 'TestReporter', this function automatically starts running the
-- tests in the background.
tryIngredient :: Ingredient -> OptionSet -> TestTree -> Maybe (IO Bool)
tryIngredient :: Ingredient -> OptionSet -> TestTree -> Maybe (IO Bool)
tryIngredient (TestReporter [OptionDescription]
_ OptionSet -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool))
report) OptionSet
opts TestTree
testTree = do -- Maybe monad
  StatusMap -> IO (Time -> IO Bool)
reportFn <- OptionSet -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool))
report OptionSet
opts TestTree
testTree
  IO Bool -> Maybe (IO Bool)
forall (m :: * -> *) a. Monad m => a -> m a
return (IO Bool -> Maybe (IO Bool)) -> IO Bool -> Maybe (IO Bool)
forall a b. (a -> b) -> a -> b
$ OptionSet
-> TestTree -> (StatusMap -> IO (Time -> IO Bool)) -> IO Bool
forall a.
OptionSet -> TestTree -> (StatusMap -> IO (Time -> IO a)) -> IO a
launchTestTree OptionSet
opts TestTree
testTree ((StatusMap -> IO (Time -> IO Bool)) -> IO Bool)
-> (StatusMap -> IO (Time -> IO Bool)) -> IO Bool
forall a b. (a -> b) -> a -> b
$ \StatusMap
smap -> StatusMap -> IO (Time -> IO Bool)
reportFn StatusMap
smap
tryIngredient (TestManager [OptionDescription]
_ OptionSet -> TestTree -> Maybe (IO Bool)
manage) OptionSet
opts TestTree
testTree =
  OptionSet -> TestTree -> Maybe (IO Bool)
manage OptionSet
opts TestTree
testTree

-- | Run the first 'Ingredient' that agrees to be run.
--
-- If no one accepts the task, return 'Nothing'. This is usually a sign of
-- misconfiguration.
tryIngredients :: [Ingredient] -> OptionSet -> TestTree -> Maybe (IO Bool)
tryIngredients :: [Ingredient] -> OptionSet -> TestTree -> Maybe (IO Bool)
tryIngredients [Ingredient]
ins OptionSet
opts TestTree
tree =
  [Maybe (IO Bool)] -> Maybe (IO Bool)
forall (t :: * -> *) (m :: * -> *) a.
(Foldable t, MonadPlus m) =>
t (m a) -> m a
msum ([Maybe (IO Bool)] -> Maybe (IO Bool))
-> [Maybe (IO Bool)] -> Maybe (IO Bool)
forall a b. (a -> b) -> a -> b
$ (Ingredient -> Maybe (IO Bool))
-> [Ingredient] -> [Maybe (IO Bool)]
forall a b. (a -> b) -> [a] -> [b]
map (\Ingredient
i -> Ingredient -> OptionSet -> TestTree -> Maybe (IO Bool)
tryIngredient Ingredient
i OptionSet
opts TestTree
tree) [Ingredient]
ins

-- | Return the options which are relevant for the given ingredient.
--
-- Note that this isn't the same as simply pattern-matching on
-- 'Ingredient'. E.g. options for a 'TestReporter' automatically include
-- 'NumThreads'.
ingredientOptions :: Ingredient -> [OptionDescription]
ingredientOptions :: Ingredient -> [OptionDescription]
ingredientOptions (TestReporter [OptionDescription]
opts OptionSet -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool))
_) =
  Proxy NumThreads -> OptionDescription
forall v. IsOption v => Proxy v -> OptionDescription
Option (Proxy NumThreads
forall k (t :: k). Proxy t
Proxy :: Proxy NumThreads) OptionDescription -> [OptionDescription] -> [OptionDescription]
forall a. a -> [a] -> [a]
: [OptionDescription]
opts
ingredientOptions (TestManager [OptionDescription]
opts OptionSet -> TestTree -> Maybe (IO Bool)
_) = [OptionDescription]
opts

-- | Like 'ingredientOption', but folds over multiple ingredients.
ingredientsOptions :: [Ingredient] -> [OptionDescription]
ingredientsOptions :: [Ingredient] -> [OptionDescription]
ingredientsOptions = [OptionDescription] -> [OptionDescription]
uniqueOptionDescriptions ([OptionDescription] -> [OptionDescription])
-> ([Ingredient] -> [OptionDescription])
-> [Ingredient]
-> [OptionDescription]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Ingredient -> [OptionDescription])
-> [Ingredient] -> [OptionDescription]
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
F.foldMap Ingredient -> [OptionDescription]
ingredientOptions

-- | All the options relevant for this test suite. This includes the
-- options for the test tree and ingredients, and the core options.
suiteOptions :: [Ingredient] -> TestTree -> [OptionDescription]
suiteOptions :: [Ingredient] -> TestTree -> [OptionDescription]
suiteOptions [Ingredient]
ins TestTree
tree = [OptionDescription] -> [OptionDescription]
uniqueOptionDescriptions ([OptionDescription] -> [OptionDescription])
-> [OptionDescription] -> [OptionDescription]
forall a b. (a -> b) -> a -> b
$
  [OptionDescription]
coreOptions [OptionDescription] -> [OptionDescription] -> [OptionDescription]
forall a. [a] -> [a] -> [a]
++
  [Ingredient] -> [OptionDescription]
ingredientsOptions [Ingredient]
ins [OptionDescription] -> [OptionDescription] -> [OptionDescription]
forall a. [a] -> [a] -> [a]
++
  TestTree -> [OptionDescription]
treeOptions TestTree
tree

-- | Compose two 'TestReporter' ingredients which are then executed
-- in parallel. This can be useful if you want to have two reporters
-- active at the same time, e.g., one which prints to the console and
-- one which writes the test results to a file.
--
-- Be aware that it is not possible to use 'composeReporters' with a 'TestManager',
-- it only works for 'TestReporter' ingredients.
composeReporters :: Ingredient -> Ingredient -> Ingredient
composeReporters :: Ingredient -> Ingredient -> Ingredient
composeReporters (TestReporter [OptionDescription]
o1 OptionSet -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool))
f1) (TestReporter [OptionDescription]
o2 OptionSet -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool))
f2) =
  [OptionDescription]
-> (OptionSet
    -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool)))
-> Ingredient
TestReporter ([OptionDescription]
o1 [OptionDescription] -> [OptionDescription] -> [OptionDescription]
forall a. [a] -> [a] -> [a]
++ [OptionDescription]
o2) ((OptionSet
  -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool)))
 -> Ingredient)
-> (OptionSet
    -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool)))
-> Ingredient
forall a b. (a -> b) -> a -> b
$ \OptionSet
o TestTree
t ->
  case (OptionSet -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool))
f1 OptionSet
o TestTree
t, OptionSet -> TestTree -> Maybe (StatusMap -> IO (Time -> IO Bool))
f2 OptionSet
o TestTree
t) of
    (Maybe (StatusMap -> IO (Time -> IO Bool))
g, Maybe (StatusMap -> IO (Time -> IO Bool))
Nothing) -> Maybe (StatusMap -> IO (Time -> IO Bool))
g
    (Maybe (StatusMap -> IO (Time -> IO Bool))
Nothing, Maybe (StatusMap -> IO (Time -> IO Bool))
g) -> Maybe (StatusMap -> IO (Time -> IO Bool))
g
    (Just StatusMap -> IO (Time -> IO Bool)
g1, Just StatusMap -> IO (Time -> IO Bool)
g2) -> (StatusMap -> IO (Time -> IO Bool))
-> Maybe (StatusMap -> IO (Time -> IO Bool))
forall a. a -> Maybe a
Just ((StatusMap -> IO (Time -> IO Bool))
 -> Maybe (StatusMap -> IO (Time -> IO Bool)))
-> (StatusMap -> IO (Time -> IO Bool))
-> Maybe (StatusMap -> IO (Time -> IO Bool))
forall a b. (a -> b) -> a -> b
$ \StatusMap
s -> do
      (Time -> IO Bool
h1, Time -> IO Bool
h2) <- IO (Time -> IO Bool)
-> IO (Time -> IO Bool) -> IO (Time -> IO Bool, Time -> IO Bool)
forall a b. IO a -> IO b -> IO (a, b)
concurrently (StatusMap -> IO (Time -> IO Bool)
g1 StatusMap
s) (StatusMap -> IO (Time -> IO Bool)
g2 StatusMap
s)
      (Time -> IO Bool) -> IO (Time -> IO Bool)
forall (m :: * -> *) a. Monad m => a -> m a
return ((Time -> IO Bool) -> IO (Time -> IO Bool))
-> (Time -> IO Bool) -> IO (Time -> IO Bool)
forall a b. (a -> b) -> a -> b
$ \Time
x -> ((Bool, Bool) -> Bool) -> IO (Bool, Bool) -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ((Bool -> Bool -> Bool) -> (Bool, Bool) -> Bool
forall a b c. (a -> b -> c) -> (a, b) -> c
uncurry Bool -> Bool -> Bool
(&&)) (IO (Bool, Bool) -> IO Bool) -> IO (Bool, Bool) -> IO Bool
forall a b. (a -> b) -> a -> b
$ IO Bool -> IO Bool -> IO (Bool, Bool)
forall a b. IO a -> IO b -> IO (a, b)
concurrently (Time -> IO Bool
h1 Time
x) (Time -> IO Bool
h2 Time
x)
composeReporters Ingredient
_ Ingredient
_ = [Char] -> Ingredient
forall a. HasCallStack => [Char] -> a
error [Char]
"Only TestReporters can be composed"