{-# LANGUAGE OverloadedStrings #-} -- | Helps you build tests that run against a series of items. module Data.Prednote.Test ( -- * Test data types Name , Verbosity(..) , TrueVerbosity , FalseVerbosity , ShowTest(..) , TestVerbosity(..) , Pass , Test(..) , TestResult(..) -- * Pre-built tests , eachSubjectMustBeTrue , nSubjectsMustBeTrue -- * Running and showing tests , evalTest , showResult ) where import Data.Maybe (fromMaybe) import Data.Monoid ((<>), mempty) import qualified Data.Text as X import Data.Text (Text) import qualified System.Console.Rainbow as R import qualified Data.Prednote.Pdct as Pt -- # Types -- | How verbose to be when showing the results of running a Pdct on a -- single subject. data Verbosity = HideAll -- ^ Do not show any results from the Pdct | ShowDefaults -- ^ Show results according to the default settings provided in the -- Result itself | ShowAll -- ^ Show all Result deriving (Eq, Show) -- | Use this verbosity for subjects that are True type TrueVerbosity = Verbosity -- | Use this verbosity for subjects that are False type FalseVerbosity = Verbosity -- | Determines whether to show any of the results from a single test. data ShowTest = HideTest -- ^ Do not show any results from this test | ShowFirstLine TrueVerbosity FalseVerbosity -- ^ Show the first line, which indicates whether the test passed or -- failed and gives the label for the test. Whether to show -- individual subjects is determined by the TrueVerbosity and -- FalseVerbosity. deriving (Eq, Show) -- | Determines which ShowTest to use for a particular test. data TestVerbosity = TestVerbosity { onPass :: ShowTest -- ^ Use this ShowTest when the test passes , onFail :: ShowTest -- ^ Use this ShowTest when the test fails } deriving (Eq, Show) type Pass = Bool -- | The name of a test or of a group. type Name = Text -- | A single test. data Test a = Test { testName :: Name , testPass :: [Pt.Result] -> Pass -- ^ Applied to the results of all applications of testFunc; -- determines whether the test passes or fails. , testFunc :: a -> Pt.Result -- ^ This function is applied to each subject. , testVerbosity :: TestVerbosity -- ^ Default verbosity for the test. } data TestResult a = TestResult { resultName :: Name , resultPass :: Pass , resultSubjects :: [(a, Pt.Result)] , resultDefaultVerbosity :: TestVerbosity } -- # Showing tests -- | Creates a plain Chunk from a Text. plain :: X.Text -> R.Chunk plain = R.Chunk mempty showTestTitle :: Name -> Pass -> [R.Chunk] showTestTitle n p = [open, passFail, close, blank, txt, nl] where nl = plain "\n" passFail = if p then "PASS" <> R.f_green else "FAIL" <> R.f_red open = plain "[" close = plain "]" blank = plain (X.singleton ' ') txt = plain n -- | Evaluates a test for a given list of subjects. evalTest :: Test a -> [a] -> TestResult a evalTest (Test n fPass fSubj vy) ls = TestResult n p ss vy where p = fPass results results = map fSubj ls ss = zip ls results -- | Shows a result with indenting. showResult :: Pt.IndentAmt -- ^ Indent each level by this many spaces -> (a -> Text) -- ^ Shows each subject. The function should return a single-line -- text without a trailing newline. -> Maybe TestVerbosity -- ^ If Just, use this TestVerbosity when showing the test. If -- Nothing, use the default verbosity. -> TestResult a -- ^ The result to show -> [R.Chunk] showResult amt swr mayVb (TestResult n p ss dfltVb) = let vb = fromMaybe dfltVb mayVb tv = if p then onPass vb else onFail vb firstLine = showTestTitle n p in case tv of HideTest -> [] ShowFirstLine trueV falseV -> firstLine ++ concatMap (showSubject p amt swr (trueV, falseV)) ss showSubject :: Pass -> Pt.IndentAmt -> (a -> Text) -> (TrueVerbosity, FalseVerbosity) -> (a, Pt.Result) -> [R.Chunk] showSubject p amt swr (tv, fv) (a, r) = let txt = swr a vb = if p then tv else fv in case vb of HideAll -> [] ShowDefaults -> Pt.showTopResult txt amt 1 False r ShowAll -> Pt.showTopResult txt amt 1 True r -- # Pre-built tests -- | The test passes if each subject returns True. eachSubjectMustBeTrue :: Pt.Pdct a -> Name -> Test a eachSubjectMustBeTrue pd nm = Test nm pass f vy where vy = TestVerbosity { onPass = ShowFirstLine HideAll HideAll , onFail = ShowFirstLine HideAll ShowDefaults } pass = all Pt.rBool f = flip Pt.evaluate pd -- | The test passes if at least a given number of subjects are True. nSubjectsMustBeTrue :: Pt.Pdct a -> Name -> Int -- ^ The number of subjects that must be True. This should be a -- positive number. -> Test a nSubjectsMustBeTrue pd nm i = Test nm pass f vy where pass = atLeast i . filter Pt.rBool f = flip Pt.evaluate pd vy = TestVerbosity { onPass = ShowFirstLine HideAll HideAll , onFail = ShowFirstLine HideAll HideAll } -- # Basement -- | Returns True if the list has at least this many elements. Lazier -- than taking the length of the list. atLeast :: Int -> [a] -> Bool atLeast i as | i < 0 = error "atLeast: negative length parameter" | otherwise = go 0 as where go _ [] = i == 0 go soFar (_:xs) = let nFound = soFar + 1 in if nFound == i then True else go nFound xs