content: -- | This module contains the core definitions related to ingredients.
  --
  -- Ingredients themselves are provided by other modules (usually under
  -- the @Test.Tasty.Ingredients.*@ hierarchy).
  --
  -- @since 0.8
  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'.
  --
  -- @since 0.4
  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'.
     --
     -- @since 0.10
    | TestManager
        [OptionDescription]
        (OptionSet -> TestTree -> Maybe (IO Bool))
     -- ^ @since 0.4
  
  -- | 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.
  --
  -- This function is not publicly exposed.
  tryIngredient :: Ingredient -> OptionSet -> TestTree -> Maybe (IO Bool)
  tryIngredient (TestReporter _ report) opts testTree = do -- Maybe monad
    reportFn <- report opts testTree
    return $ launchTestTree opts testTree $ \smap -> reportFn smap
  tryIngredient (TestManager _ manage) opts testTree =
    manage opts 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.
  --
  -- @since 0.4
  tryIngredients :: [Ingredient] -> OptionSet -> TestTree -> Maybe (IO Bool)
  tryIngredients ins opts' tree' =
    msum $ map (\i -> tryIngredient i opts tree) ins
    where
      (opts, tree) = applyTopLevelPlusTestOptions opts' tree'
  
  -- | 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'.
  --
  -- @since 0.4
  ingredientOptions :: Ingredient -> [OptionDescription]
  ingredientOptions (TestReporter opts _) =
    Option (Proxy :: Proxy NumThreads) : opts
  ingredientOptions (TestManager opts _) = opts
  
  -- | Like 'ingredientOptions', but folds over multiple ingredients.
  --
  -- @since 0.4
  ingredientsOptions :: [Ingredient] -> [OptionDescription]
  ingredientsOptions = uniqueOptionDescriptions . F.foldMap ingredientOptions
  
  -- | All the options relevant for this test suite. This includes the
  -- options for the test tree and ingredients, and the core options.
  --
  -- @since 0.4
  suiteOptions :: [Ingredient] -> TestTree -> [OptionDescription]
  suiteOptions ins tree = uniqueOptionDescriptions $
    coreOptions ++
    ingredientsOptions ins ++
    treeOptions 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.
  --
  -- @since 0.11.2
  composeReporters :: Ingredient -> Ingredient -> Ingredient
  composeReporters (TestReporter o1 f1) (TestReporter o2 f2) =
    TestReporter (o1 ++ o2) $ \o t ->
    case (f1 o t, f2 o t) of
      (g, Nothing) -> g
      (Nothing, g) -> g
      (Just g1, Just g2) -> Just $ \s -> do
        (h1, h2) <- concurrently (g1 s) (g2 s)
        return $ \x -> fmap (uncurry (&&)) $ concurrently (h1 x) (h2 x)
  composeReporters _ _ = error "Only TestReporters can be composed"
  