tasty-0.1: Test/Tasty/Core.hs
-- | Core types and definitions
{-# LANGUAGE GeneralizedNewtypeDeriving, FlexibleContexts,
ExistentialQuantification, RankNTypes #-}
module Test.Tasty.Core where
import Control.Applicative
import Test.Tasty.Options
import Test.Tasty.Patterns
import Data.Foldable
import Data.Monoid
import Data.Typeable
import qualified Data.Map as Map
import Data.Tagged
-- | A test result
data Result = Result
{ resultSuccessful :: Bool
-- ^
-- 'resultSuccessful' should be 'True' for a passed test and 'False' for
-- a failed one.
, resultDescription :: String
-- ^
-- 'resultDescription' may contain some details about the test. For
-- a passed test it's ok to leave it empty. Providers like SmallCheck and
-- QuickCheck use it to provide information about how many tests were
-- generated.
--
-- For a failed test, 'resultDescription' should typically provide more
-- information about the failure.
}
-- | Test progress information.
--
-- This may be used by a runner to provide some feedback to the user while
-- a long-running test is executing.
data Progress = Progress
{ progressText :: String
-- ^ textual information about the test's progress
, progressPercent :: Float
-- ^
-- 'progressPercent' should be a value between 0 and 1. If it's impossible
-- to compute the estimate, use 0.
}
-- | The interface to be implemented by a test provider.
--
-- The type @t@ is the concrete representation of the test which is used by
-- the provider.
class Typeable t => IsTest t where
-- | Run the test
run
:: OptionSet -- ^ options
-> t -- ^ the test to run
-> (Progress -> IO ()) -- ^ a callback to report progress
-> IO Result
-- | The list of options that affect execution of tests of this type
testOptions :: Tagged t [OptionDescription]
-- | The name of a test or a group of tests
type TestName = String
-- | The main data structure defining a test suite.
--
-- It consists of individual test cases and properties, organized in named
-- groups which form a tree-like hierarchy.
--
-- There is no generic way to create a test case. Instead, every test
-- provider (tasty-hunit, tasty-smallcheck etc.) provides a function to
-- turn a test case into a 'TestTree'.
--
-- Groups can be created using 'testGroup'.
data TestTree
= forall t . IsTest t => SingleTest TestName t
-- ^ A single test of some particular type
| TestGroup TestName [TestTree]
-- ^ Assemble a number of tests into a cohesive group
| PlusTestOptions (OptionSet -> OptionSet) TestTree
-- ^ Add some options to child tests
-- | Create a named group of test cases or other groups
testGroup :: TestName -> [TestTree] -> TestTree
testGroup = TestGroup
-- | Fold a test tree into a single value.
--
-- Apart from pure convenience, this function also does the following
-- useful things:
--
-- 1. Keeping track of the current options (which may change due to
-- `PlusTestOptions` nodes)
--
-- 2. Filtering out the tests which do not match the patterns
--
-- Thus, it is preferred to an explicit recursive traversal of the tree.
--
-- Note: right now, the patterns are looked up only once, and won't be
-- affected by the subsequent option changes. This shouldn't be a problem
-- in practice; OTOH, this behaviour may be changed later.
foldTestTree
:: Monoid b
=> (forall t . IsTest t => OptionSet -> TestName -> t -> b)
-- ^ interpret a single test
-> (TestName -> b -> b)
-- ^ interpret a test group
-> OptionSet
-- ^ initial options
-> TestTree -> b
foldTestTree fTest fGroup opts tree =
let pat = lookupOption opts
in go pat [] opts tree
where
go pat path opts tree =
case tree of
SingleTest name test
| testPatternMatches pat (path ++ [name])
-> fTest opts name test
| otherwise -> mempty
TestGroup name trees ->
fGroup name $ foldMap (go pat (path ++ [name]) opts) trees
PlusTestOptions f tree -> go pat path (f opts) tree
-- | Useful wrapper for use with foldTestTree
newtype AppMonoid f = AppMonoid { getApp :: f () }
instance Applicative f => Monoid (AppMonoid f) where
mempty = AppMonoid $ pure ()
AppMonoid f1 `mappend` AppMonoid f2 = AppMonoid $ f1 *> f2
-- | Get the list of options that are relevant for a given test tree
getTreeOptions :: TestTree -> [OptionDescription]
getTreeOptions =
Prelude.concat .
Map.elems .
foldTestTree
(\_ _ -> getTestOptions)
(const id)
mempty
where
getTestOptions
:: forall t . IsTest t
=> t -> Map.Map TypeRep [OptionDescription]
getTestOptions t =
Map.singleton (typeOf t) $
witness testOptions t