{-| Module : TLT Description : Testing in a monad transformer layer Copyright : (c) John Maraist, 2022 License : GPL3 Maintainer : haskell-tlt@maraist.org Stability : experimental Portability : POSIX TLT is a small unit test system oriented towards examining intermediate results of computations in monad transformers. It is intended to be lightweight for the programmer, and does not require tests to be specified in some sort of formal list of tests. Rather, tests are simply commands in a monad stack which includes the transformer layer @Test.TLT@. This Haddock page is the main piece of documentation; or see also the GitHub repository . -} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Test.TLT ( -- * Overview -- |A TLT test is a command in the `TLT` monad transformer. There -- is no separation between the specification and execution of a -- test; TLT makes no record of an executable test itself, only of -- its result. So in the main instance for testing, the core `IO` -- monad should be wrapped in the `TLT` transformer, and in whatever -- other layers are also to be tested. -- -- In TLT, all tests are associated with a string which names or -- otherwise describes the test. Each test is introduced with one -- of the @~:@, @~::@, or @~::-@ infix operators. -- -- The simplest tests simply look for a `True` boolean value. These -- tests are introduced with @~::@ or @~::-@. The difference -- between the two is whether the boolean value is the result of a -- pure `Bool` expression, or whether it is returned as the result -- of a computation. In TLT, we distinguish between the two cases -- by including a trailing hyphen @-@ to operators on pure -- expressions, and omitting the hyphen from operators on monadic -- arguments. So these two tests will both pass, -- -- > "2 is 2 as single Bool" ~::- 2 == 2 -- > "2 is 2 a returned Bool" ~:: return $ 2 == 2 -- -- The @~:@ operator introduces a more general form of test. The -- right-hand side of @~:@ should be an `Assertion` formed with one -- of TLT's built-in assertion operators, or returned from a -- package's custom assertions. `Assertion`s can give more detailed -- failure information then simple `Bool`s. -- -- Syntactically, most assertions are infix operators which start -- with a @\@@ character. The value to the left of the operator is -- the expected value, and the symbol to the right is (or returns) -- the value under test. A hyphen or @P@ suffixes assertion -- operators which operate on pure values; for operators without the -- trailing hyphen, the value under test should is expected to be -- returned as the result of a monadic computation (as with @~::@ -- and @~::-@). -- -- TLT provides these assertion operators: -- -- +---------------------------------+---------------------------------------+ -- | Operator | Meaning | -- +=================================+=======================================+ -- | @/expected/ \@== /monadic/@ | The actual result must be equal | -- +---------------------------------+ to the given expected result. | -- | @/expected/ \@==- /expr/@ | | -- +---------------------------------+---------------------------------------+ -- | @/unexpected/ \@\/= /monadic/@ | The actual result must differ | -- +---------------------------------+ from the given unexpected result. | -- | @/unexpected/ \@\/=- /expr/@ | | -- +---------------------------------+---------------------------------------+ -- | @/expected/ \@< /monadic/@ | The actual result must be greater | -- +---------------------------------+ than the given lower bound. | -- | @/expected/ \@<- /expr/@ | | -- +---------------------------------+---------------------------------------+ -- | @/expected/ \@> /monadic/@ | The actual result must be less | -- +---------------------------------+ than the given upper bound. | -- | @/expected/ \@>- /expr/@ | | -- +---------------------------------+---------------------------------------+ -- | @/expected/ \@<= /monadic/@ | The actual result must be greater | -- +---------------------------------+ than or equal to the given lower | -- | @/expected/ \@<=- /expr/@ | bound. | -- +---------------------------------+---------------------------------------+ -- | @/expected/ \@>= /monadic/@ | The actual result must be less than | -- +---------------------------------+ or equal to the given upper bound. | -- | @/expected/ \@>=- /expr/@ | | -- +---------------------------------+---------------------------------------+ -- | @empty /monadic/@ | The actual result must be an empty | -- +---------------------------------+ `Traversable` structure. | -- | @emptyP /expr/@ | | -- +---------------------------------+---------------------------------------+ -- | @nonempty /monadic/@ | The actual result must be a nonempty | -- +---------------------------------+ `Traversable` structure. | -- | @nonemptyP /expr/@ | | -- +---------------------------------+---------------------------------------+ -- | @nothing /monadic/@ | The actual result must be `Nothing` | -- +---------------------------------+ (in a `Maybe`-typed value) | -- | @nothingP /expr/@ | | -- +---------------------------------+---------------------------------------+ -- | @assertFailed /message/@ | Trivial assertions, intended for the | -- +---------------------------------+ less interesting branches of | -- | @assertSuccess@ | conditional and selection expressions.| -- +---------------------------------+---------------------------------------+ -- -- Note that although the assertions are in pairs of one for testing -- a pure expression value, and one for testing the result returned -- from a monadic computation, in all of the builtin binary -- assertions the /expected/ value argument is always a pure value, -- not itself monadic. -- -- The `inGroup` function allows related tests to be reported as a -- group. The function takes two arguments, a `String` name for the -- group, and the `TLT` computation housing its tests. Groups have -- impact only in terms of organizing the output you see in the -- final report of tests run. -- -- Finally, it is straightforward to write new `Assertion`s for -- project-specific test criteria: they are simply functions -- returning monadic values. There are several functions in the -- final section of this document which transform pure predicates -- into `Assertion`s, or which transform one form of `Assertion` -- into another. -- -- The source repository for TLT lives at -- . -- * Examples -- |These examples are from the sample executables and test suite of -- the @TLT@ package. -- ** A simple example -- |The tests in this example are vacuous, but they show a simple -- setup with both passing and failing tests. -- -- > main :: IO () -- > main = do -- > tlt test -- > -- > test :: Monad m => TLT m () -- > test = do -- > "True passes" ~::- True -- > "2 is 3 as single Bool" ~::- 2 == 3 -- > "2 is 2 as single Bool" ~::- 2 == 2 -- > inGroup "== assertions" $ do -- > inGroup "pure" $ do -- > "2 is 3 as pure assertion" ~: 2 @==- 3 -- > "2 is 2 as pure assertion" ~: 2 @==- 2 -- > inGroup "monadic" $ do -- > "2 is 3 as result" ~: 2 @== return 3 -- > "2 is 2 as result" ~: 2 @== return 2 -- > inGroup "/= pure assertions" $ do -- > "2 not 3" ~: 2 @/=- 3 -- > "2 not 2" ~: 2 @/=- 2 -- > "2 not 3 as result" ~: 2 @/= return 3 -- > "2 not 2 as result" ~: 2 @/= return 2 -- -- Running these tests should give: -- -- > Running tests: -- > - 2 is 3 as single Bool: FAIL Expected True but got False -- > - == assertions: -- > - pure: -- > - 2 is 3 as pure assertion: FAIL Expected 2 but got 3 -- > - monadic: -- > - 2 is 3 as result: FAIL Expected 2 but got 3 -- > - /= pure assertions: -- > - 2 not 2: FAIL Expected other than 2 but got 2 -- > - 2 not 2 as result: FAIL Expected other than 2 but got 2 -- > Found 5 errors in 11 tests; exiting -- -- Note that only failing tests appear. This can be configured in the -- @test@ command: add a call at the beginning of @test@ to -- @reportAllTestResults@ to control this behavior: -- -- > test :: Monad m => TLT m () -- > test = do -- > reportAllTestResults True -- > "True passes" ~::- True -- > ... -- -- and the output will be -- -- > Running tests: -- > - True passes: Pass -- > - 2 is 3 as single Bool: FAIL Expected True but got False -- > - 2 is 2 as single Bool: Pass -- > - == assertions: -- > - pure: -- > - 2 is 3 as pure assertion: FAIL Expected 2 but got 3 -- > - 2 is 2 as pure assertion: Pass -- > - monadic: -- > - 2 is 3 as result: FAIL Expected 2 but got 3 -- > - 2 is 2 as result: Pass -- > - /= pure assertions: -- > - 2 not 3: Pass -- > - 2 not 2: FAIL Expected other than 2 but got 2 -- > - 2 not 3 as result: Pass -- > - 2 not 2 as result: FAIL Expected other than 2 but got 2 -- > Found 5 errors in 11 tests; exiting -- ** Testing monad transformers -- |In the previous example `TLT` was the outermost (in fact only) -- monad transformer, but it can appear at any level of the test -- suite's application stack. Using `TLT` at other than the top -- level is easiest when all of the transformers which might wrap it -- are declared as instances of `MonadTLT`. -- -- Consider an application which declares two monad transformers -- @M1T@ and @M2T@. For simplicity here we take them to be just -- aliases for `IdentityT`: -- -- > newtype Monad m => M1T m a = M1T { unwrap1 :: IdentityT m a } -- > runM1T :: Monad m => M1T m a -> m a -- > runM1T = runIdentityT . unwrap1 -- > -- > newtype Monad m => M2T m a = M2T { unwrap2 :: IdentityT m a } -- > runM2T :: Monad m => M2T m a -> m a -- > runM2T = runIdentityT . unwrap2 -- -- And we elide the usual details of including each of them in -- `Functor`, `Applicative`, `Monad` and `MonadTrans`. We can -- declare instances of each in `MonadTLT`, -- -- > instance MonadTLT m n => MonadTLT (M1T m) n where -- > liftTLT = lift . liftTLT -- -- and similarly for @M2T@. Note that this declaration does require -- @FlexibleInstances@ (because @n@ does not appear in the instance -- type), @MultiParamTypeClasses@ (because we must mention both the -- top transformer @m@ and the monadic type @n@ directly wrapped by -- `TLT` within @m@), and @UndecidableInstances@ (because @n@ is not -- smaller in the recursive context of `MonadTLT`, which is not -- actually a problem because in the definition of `MonadTLT`, @n@ -- is functionally dependent on @m@, which /is/ smaller in the -- recursive context) in the module where the `MonadTLT` instance is -- declared. -- -- Now it is convenient to test both transformers: -- -- > ttest = do -- > runM1T $ inGroup "M1T tests" $ m1tests -- > runM2T $ inGroup "M2T tests" $ m2tests -- > -- > m1tests = M1T $ do -- > "3 is 3 as pure assertion" ~: 3 @==- 3 -- > "4 is 4 as pure assertion" ~: 4 @==- 4 -- > -- > m2tests = M2T $ do -- > "5 is 5 as pure assertion" ~: 5 @==- 5 -- > "6 is 6 as pure assertion" ~: 6 @==- 6 -- -- It is not necessary, for example, to harvest test declarations -- from the executions of the @MnT@s for assembly into an overall -- test declaration. -- * The TLT transformer TLT, tlt, MonadTLT, liftTLT, -- ** Session options reportAllTestResults, setExitAfterFailDisplay, -- * Writing tests Assertion, -- ** `TLT` commands (~:), (~::), (~::-), tltFail, inGroup, -- ** Assertions -- *** About the values of pure expressions of `Eq`- and `Ord`-type (@==), (@/=), (@<), (@>), (@<=), (@>=), -- *** About monadic computations returing `Eq`s and `Ord`s (@==-), (@/=-), (@<-), (@>-), (@<=-), (@>=-), -- *** About list values empty, nonempty, emptyP, nonemptyP, -- *** About `Maybe` values nothing, nothingP, assertFailed, assertSuccess, -- ** Building new assertions -- *** Unary assertions liftAssertionPure, assertionPtoM, liftAssertionM, -- *** Binary assertions liftAssertion2Pure, assertion2PtoM, liftAssertion2M ) where import Data.Maybe import Control.Exception import Control.Monad import Control.Monad.IO.Class import Control.Monad.ST.Trans import Control.Monad.Trans.Class -- import Control.Monad.Trans.Either import Control.Monad.Trans.Free import Control.Monad.Trans.Identity import Control.Monad.Trans.Maybe import Control.Monad.Trans.Reader import Control.Monad.Trans.Resource import Control.Monad.Trans.State.Strict import qualified Control.Monad.Trans.State.Lazy as SL import qualified Control.Monad.Trans.Writer.Lazy as WL import qualified Control.Monad.Trans.Writer.Strict as WS import System.Console.ANSI import System.Exit -- * Results of tests -- |Reasons why a test might fail. data TestFail = Asserted String -- ^ A failure arising from an `Assertion` which is not met. | Erred String -- ^ A failure associated with a call to a Haskell -- function triggering an error. formatFail :: TestFail -> String formatFail (Asserted s) = s formatFail (Erred s) = "Assertion raised exception: " ++ s -- |An assertion is a computation (typically in the monad wrapped by -- `TLT`) which returns a list of zero of more reasons for the failure -- of the assertion. A successful computation returns an empty list: -- no reasons for failure, hence success. type Assertion m = m [TestFail] -- |Hierarchical structure holding the result of running tests, -- possibly grouped into tests. data TestResult = Test String [TestFail] | Group String Int Int [TestResult] -- ^ The `Int`s are respectively the total number of -- tests executed, and total number of failures -- detected. -- |Return the number of failed tests reported in a `TestResult`. failCount :: TestResult -> Int failCount (Test _ []) = 0 failCount (Test _ _) = 1 failCount (Group _ _ n _) = n testCount :: TestResult -> Int testCount (Test _ _) = 1 testCount (Group _ n _ _) = n totalFailCount :: [TestResult] -> Int totalFailCount = foldr (+) 0 . map failCount totalTestCount :: [TestResult] -> Int totalTestCount = foldr (+) 0 . map testCount -- |Report the results of tests. report :: TLTopts -> [TestResult] -> IO () report (TLTopts showPasses exitAfterFailDisplay) trs = let fails = totalFailCount trs tests = totalTestCount trs in do report' "" trs if fails > 0 then do boldRed putStrLn $ "Found " ++ show fails ++ " error" ++ (if fails > 1 then "s" else "") ++ " in " ++ show tests ++ " tests; exiting" mediumBlack when exitAfterFailDisplay exitFailure else do boldGreen putStrLn $ show tests ++ " test" ++ (if tests > 1 then "s" else "") ++ " passing." mediumBlack where report' ind trs = forM_ trs $ \ tr -> when (failCount tr > 0 || showPasses) $ case tr of Test s r -> do putStr $ ind ++ "- " ++ s ++ ": " case r of [] -> do greenPass putStrLn "" x : [] -> do redFail putStrLn $ " " ++ formatFail x _ -> do redFail putStrLn ":" forM_ r $ \ f -> putStrLn $ ind ++ "- " ++ formatFail f Group s _ _ trs' -> do putStrLn $ ind ++ "- " ++ s ++ ":" report' (" " ++ ind) trs' boldBlack = setSGR [ SetColor Foreground Vivid Black, SetConsoleIntensity BoldIntensity ] boldRed = setSGR [ SetColor Foreground Vivid Red, SetConsoleIntensity BoldIntensity ] boldGreen = setSGR [ SetColor Foreground Vivid Green, SetConsoleIntensity BoldIntensity ] mediumRed = setSGR [ SetColor Foreground Vivid Red, SetConsoleIntensity NormalIntensity ] mediumGreen = setSGR [ SetColor Foreground Vivid Green, SetConsoleIntensity NormalIntensity ] mediumBlue = setSGR [ SetColor Foreground Vivid Blue, SetConsoleIntensity NormalIntensity ] mediumBlack = setSGR [ SetColor Foreground Vivid Black, SetConsoleIntensity NormalIntensity ] greenPass = do mediumBlue putStr "Pass" mediumBlack redFail = do boldRed putStr "FAIL" mediumBlack -- |Accumulator for test results, in the style of a simplified Huet's -- zipper which only ever adds to the end of the structure. data TRBuf = Buf TRBuf Int Int String [TestResult] | Top Int Int [TestResult] -- |Add a single test result to a `TRBuf`. addResult :: TRBuf -> TestResult -> TRBuf addResult (Top tc fc trs) tr = Top (tc + testCount tr) (fc + failCount tr) $ tr : trs addResult (Buf up tc fc s trs) tr = Buf up (tc + testCount tr) (fc + failCount tr) s $ tr : trs -- |Convert the topmost group of a bottom-up `TRBuf` into a completed -- top-down report about the group. currentGroup :: TRBuf -> TestResult currentGroup (Buf up tc fc s trs) = Group s tc fc (reverse trs) -- |Derive a new `TRBuf` corresponding to finishing the current group -- and continuing to accumulate results into its enclosure. popGroup :: TRBuf -> TRBuf popGroup trb@(Buf acc _ _ _ _) = addResult acc $ currentGroup trb -- |Convert a `TRBuf` into a list of top-down `TestResult`s. closeTRBuf :: TRBuf -> [TestResult] closeTRBuf (Top _ _ ts) = reverse ts closeTRBuf b = closeTRBuf $ popGroup b -- |Record of options which may be specified for running and reporting -- TLT tests. data TLTopts = TLTopts { optShowPasses :: Bool, optQuitAfterFailReport :: Bool } -- |Default initial options defaultOpts = TLTopts False True -- |Update the display of showing passes in a `TLTopts` record. withShowPasses :: TLTopts -> Bool -> TLTopts withShowPasses (TLTopts _ f) b = TLTopts b f -- |Update the display of showing passes in a `TLTopts` record. withExitAfterFail :: TLTopts -> Bool -> TLTopts withExitAfterFail (TLTopts p _) b = TLTopts p b -- |Synonym for the elements of the `TLT` state. type TLTstate = (TLTopts, TRBuf) -- |Monad transformer for TLT tests. This layer stores the results -- from tests as they are executed. newtype Monad m => TLT m r = TLT { unwrap :: StateT TLTstate m r } deriving (Functor, Applicative, Monad, MonadTrans, MonadIO) {- ------------------------------------------------------------ -} -- |Extending `TLT` operations across other monad transformers. For -- easiest and most flexible testing, declare the monad transformers -- of your application as instances of this class. class (Monad m, Monad n) => MonadTLT m n | m -> n where -- |Lift TLT operations within a monad transformer stack. Note that -- with enough transformer types included in this class, the -- @liftTLT@ function should usually be unnecessary: the commands in -- this module which actually configure testing, or specify a test, -- already @liftTLT@ their own result. So they will all act as -- top-level transformers in @MonadTLT@. liftTLT :: TLT n a -> m a instance Monad m => MonadTLT (TLT m) m where liftTLT = id instance (MonadTLT m n, Functor f) => MonadTLT (FreeT f m) n where liftTLT = lift . liftTLT instance MonadTLT m n => MonadTLT (IdentityT m) n where liftTLT = lift . liftTLT instance MonadTLT m n => MonadTLT (MaybeT m) n where liftTLT = lift . liftTLT instance MonadTLT m n => MonadTLT (ReaderT r m) n where liftTLT = lift . liftTLT instance MonadTLT m n => MonadTLT (ResourceT m) n where liftTLT = lift . liftTLT instance MonadTLT m n => MonadTLT (StateT s m) n where liftTLT = lift . liftTLT instance MonadTLT m n => MonadTLT (SL.StateT s m) n where liftTLT = lift . liftTLT instance MonadTLT m n => MonadTLT (STT s m) n where liftTLT = lift . liftTLT instance (MonadTLT m n, Monoid w) => MonadTLT (WL.WriterT w m) n where liftTLT = lift . liftTLT instance (MonadTLT m n, Monoid w) => MonadTLT (WS.WriterT w m) n where liftTLT = lift . liftTLT {- ------------------------------------------------------------ -} -- |Execute the tests specified in a `TLT` monad, and report the -- results. tlt :: MonadIO m => TLT m r -> m () tlt (TLT t) = do liftIO $ putStrLn "Running tests:" (_, (opts, resultsBuf)) <- runStateT t $ (defaultOpts, Top 0 0 []) liftIO $ report opts $ closeTRBuf resultsBuf -- |This function controls whether `tlt` will report only tests which -- fail, suppressing any display of tests which pass, or else report -- the results of all tests. The default is the former: the idea is -- that no news should be good news, with the programmer bothered only -- with problems which need fixing. reportAllTestResults :: MonadTLT m n => Bool -> m () reportAllTestResults b = liftTLT $ TLT $ do (opts, tr) <- get put $ (opts `withShowPasses` b, tr) -- |This function controls whether `tlt` will exit after displaying -- test results which include at least one failing test. By default, -- it will exit in this situation. The idea is that a test suite can -- be broken into parts when it makes sense to run the latter parts -- only when the former parts all pass. setExitAfterFailDisplay :: MonadTLT m n => Bool -> m () setExitAfterFailDisplay b = liftTLT $ TLT $ do (opts, tr) <- get put $ (opts `withExitAfterFail` b, tr) -- |Report a failure. Useful in pattern-matching cases which are -- entirely not expected. tltFail :: MonadTLT m n => String -> String -> m () desc `tltFail` detail = liftTLT $ TLT $ do (opts, before) <- get let after = addResult before $ Test desc [Asserted detail] put (opts, after) -- |Organize the tests in the given subcomputation as a separate group -- within the test results we will report. inGroup :: MonadTLT m n => String -> m a -> m a inGroup name group = do (opts, before) <- liftTLT $ TLT get liftTLT $ TLT $ put $ (opts, Buf before 0 0 name []) result <- group (opts', after) <- liftTLT $ TLT $ get liftTLT $ TLT $ put $ (opts', popGroup after) return result -- * Specifying individual tests infix 0 ~:, ~::, ~::- -- |Label and perform a test of an `Assertion`. -- -- ===== Example -- -- > test :: Monad m => TLT m () -- > test = do -- > "2 is 2 as result" ~: 2 @== return 2 -- This test passes. -- > "2 not 3" ~: 2 @/=- 3 -- This test fails. (~:) :: MonadTLT m n => String -> Assertion m -> m () s ~: a = do (opts, oldState) <- liftTLT $ TLT $ get assessment <- a liftTLT $ TLT $ put (opts, addResult oldState $ Test s assessment) -- |Label and perform a test of a (pure) boolean value. -- -- ===== Example -- -- > test :: Monad m => TLT m () -- > test = do -- > "True passes" ~::- return True -- This test passes. -- > "2 is 2 as single Bool" ~::- return (2 == 2) -- This test passes. -- > "2 is 3!?" ~::- myFn 4 "Hammer" -- Passes if myFn (which -- > -- must be monadic) -- > -- returns True. (~::-) :: MonadTLT m n => String -> Bool -> m () s ~::- b = do (opts, oldState) <- liftTLT $ TLT $ get liftTLT $ TLT $ put (opts, addResult oldState $ Test s $ if b then [] else [Asserted $ "Expected True but got False"]) -- |Label and perform a test of a boolean value returned by a -- computation in the wrapped monad @m@. -- -- ===== Example -- -- > test :: Monad m => TLT m () -- > test = do -- > "True passes" ~::- True -- This test passes. -- > "2 is 2 as single Bool" ~::- 2 == 2 -- This test passes. -- > "2 is 3!?" ~::- 2 == 2 -- This test fails. (~::) :: MonadTLT m n => String -> m Bool -> m () s ~:: bM = do b <- bM (opts, oldState) <- liftTLT $ TLT $ get liftTLT $ TLT $ put (opts, addResult oldState $ Test s $ if b then [] else [Asserted $ "Expected True but got False"]) infix 1 @==, @/=, @<, @>, @<=, @>= infix 1 @==-, @/=-, @<-, @>-, @<=-, @>=- -- |Transform a binary function on an expected and an actual value -- (plus a binary generator of a failure message) into an `Assertion` -- for a pure given actual value. -- -- ===== Example -- -- TLT's scalar-testing operators like @\@==-@ are defined with this -- function: -- -- > (@==-) :: (Monad m, Eq a, Show a) => a -> a -> Assertion m -- > (@==-) = liftAssertion2Pure (==) $ -- > \ exp actual -> "Expected " ++ show exp ++ " but got " ++ show actual -- -- The `(==)` operator tests equality, and the result here allows the -- assertion that a value should be exactly equal to a target. The -- second argument formats the detail reported when the assertion -- fails. liftAssertion2Pure :: (Monad m) => (a -> a -> Bool) -> (a -> a -> String) -> a -> a -> Assertion m liftAssertion2Pure tester explainer exp actual = return $ if (tester exp actual) then [] else [Asserted $ explainer exp actual] -- |Given an `Assertion` for two pure values (expected and actual), -- lift it to an `Assertion` expecting the actual value to be returned -- from a computation. -- -- ===== Examples -- -- The TLT assertion `(@==)` lifts `(@==-)` from expecting a pure -- actual result to expecting a computation returning a value to test. -- -- > (@==) :: (Monad m, Eq a, Show a) => a -> m a -> Assertion m -- > (@==) = assertion2PtoM (@==-) assertion2PtoM :: (Monad m) => (a -> a -> Assertion m) -> a -> m a -> Assertion m assertion2PtoM pa exp actualM = do actual <- actualM pa exp actual -- |Transform a binary function on expected and actual values (plus -- a generator of a failure message) into an `Assertion` where the -- actual value is to be returned from a subcomputation. liftAssertion2M :: (Monad m) => (a -> a -> Bool) -> (a -> a -> String) -> a -> m a -> Assertion m liftAssertion2M tester explainer exp actualM = let assertPure = liftAssertion2Pure tester explainer exp in do actual <- actualM assertPure actual -- |Assert that two values are equal. This assertion takes an -- expected and an actual /value/; see `(@==)` to compare the result -- of a /monadic computation/ to an expected value. -- -- ===== Examples -- -- > test :: Monad m => TLT m () -- > test = do -- > "Make sure that 2 is still equal to itself" ~: 2 @==- 2 -- > "Make sure that there are four lights" ~: 4 @==- length lights (@==-) :: (Monad m, Eq a, Show a) => a -> a -> Assertion m (@==-) = liftAssertion2Pure (==) $ \ exp actual -> "Expected " ++ show exp ++ " but got " ++ show actual -- |Assert that a calculated value is as expected. This assertion -- compare the result of a /monadic computation/ to an expected value; -- see `(@==-)` to compare an /actual value/ to the expected value. -- -- ===== Examples -- -- > test :: Monad m => TLT m () -- > test = do -- > "Make sure that 2 is still equal to itself" ~: 2 @== return 2 -- > "Make sure that there are four lights" ~: 4 @== countLights -- > -- where countLights :: m Int (@==) :: (Monad m, Eq a, Show a) => a -> m a -> Assertion m (@==) = assertion2PtoM (@==-) -- |Assert that two values are not equal. This assertion takes an -- expected and an actual /value/; see `(@/=)` to compare the result -- of a /monadic computation/ to an expected value. (@/=-) :: (Monad m, Eq a, Show a) => a -> a -> Assertion m (@/=-) = liftAssertion2Pure (/=) $ \ exp actual -> "Expected other than " ++ show exp ++ " but got " ++ show actual -- |Assert that a calculated value differs from some known value. -- This assertion compares the result of a /monadic computation/ to an -- expected value; see `(@/=-)` to compare an /actual value/ to the -- expected value. (@/=) :: (Monad m, Eq a, Show a) => a -> m a -> Assertion m (@/=) = assertion2PtoM (@/=-) -- |Assert that a given boundary is strictly less than some value. -- This assertion takes an expected and an actual /value/; see `(@<)` -- to compare the result of a /monadic computation/ to an expected -- value. (@<-) :: (Monad m, Ord a, Show a) => a -> a -> Assertion m (@<-) = liftAssertion2Pure (<) $ \ exp actual -> "Lower bound (open) is " ++ show exp ++ " but got " ++ show actual -- |Assert that a given, constant boundary is strictly less than some -- calculated value. This assertion compares the result of a /monadic -- computation/ to an expected value; see `(@<-)` to compare an -- /actual value/ to the expected value. (@<) :: (Monad m, Ord a, Show a) => a -> m a -> Assertion m (@<) = assertion2PtoM (@<-) -- |Assert that a given boundary is strictly less than some value. -- This assertion takes an expected and an actual /value/; see `(@>)` -- to compare the result of a /monadic computation/ to an expected -- value. (@>-) :: (Monad m, Ord a, Show a) => a -> a -> Assertion m (@>-) = liftAssertion2Pure (>) $ \ exp actual -> "Upper bound (open) is " ++ show exp ++ " but got " ++ show actual -- |Assert that a given, constant boundary is strictly less than some -- calculated value. This assertion compares the result of a /monadic -- computation/ to an expected value; see `(@>-)` to compare an -- /actual value/ to the expected value. (@>) :: (Monad m, Ord a, Show a) => a -> m a -> Assertion m (@>) = assertion2PtoM (@>-) -- |Assert that a given boundary is strictly less than some value. -- This assertion takes an expected and an actual /value/; see `(@<=)` -- to compare the result of a /monadic computation/ to an expected -- value. (@<=-) :: (Monad m, Ord a, Show a) => a -> a -> Assertion m (@<=-) = liftAssertion2Pure (<=) $ \ exp actual -> "Lower bound (closed) is " ++ show exp ++ " but got " ++ show actual -- |Assert that a given, constant boundary is strictly less than some -- calculated value. This assertion compares the result of a /monadic -- computation/ to an expected value; see `(@<=-)` to compare an -- /actual value/ to the expected value. (@<=) :: (Monad m, Ord a, Show a) => a -> m a -> Assertion m (@<=) = assertion2PtoM (@<=-) -- |Assert that a given boundary is strictly less than some value. -- This assertion takes an expected and an actual /value/; see `(@>=)` -- to compare the result of a /monadic computation/ to an expected -- value. (@>=-) :: (Monad m, Ord a, Show a) => a -> a -> Assertion m (@>=-) = liftAssertion2Pure (>=) $ \ exp actual -> "Upper bound (closed) is " ++ show exp ++ " but got " ++ show actual -- |Assert that a given, constant boundary is strictly less than some -- calculated value. This assertion compares the result of a /monadic -- computation/ to an expected value; see `(@>=-)` to compare an -- /actual value/ to the expected value. (@>=) :: (Monad m, Ord a, Show a) => a -> m a -> Assertion m (@>=) = assertion2PtoM (@>=-) -- |This assertion always fails with the given message. assertFailed :: Monad m => String -> Assertion m assertFailed msg = return [Asserted msg] -- |This assertion always succeeds. assertSuccess :: Monad m => Assertion m assertSuccess = return [] -- |Transform a unary function on a value (plus a generator of a -- failure message) into a unary function returning an `Assertion` for -- a pure given actual value. -- -- ===== Example -- -- The TLT assertion `emptyP` is built from the `Traversable` predicate -- `null` -- -- > emptyP :: (Monad m, Traversable t) => t a -> Assertion m -- > emptyP = liftAssertionPure null -- > (\ _ -> "Expected empty structure but got non-empty") liftAssertionPure :: (Monad m) => (a -> Bool) -> (a -> String) -> a -> Assertion m liftAssertionPure tester explainer actual = return $ if (tester actual) then [] else [Asserted $ explainer actual] -- |Given an `Assertion` for a pure (actual) value, lift it to an -- `Assertion` expecting the value to be returned from a computation. -- -- ===== Example -- -- The TLT assertion `empty` on monadic computations returning lists -- is defined in terms of the corresponging assertion on pure -- list-valued expressions. -- -- > empty :: (Monad m, Traversable t) => m (t a) -> Assertion m -- > empty = assertionPtoM emptyP assertionPtoM :: (Monad m) => (a -> Assertion m) -> m a -> Assertion m assertionPtoM pa actualM = do actual <- actualM pa actual -- |Transform a unary function on an actual value (plus a generator of -- a failure message) into an `Assertion` where the value is to be -- returned from a subcomputation. liftAssertionM :: (Monad m) => (a -> Bool) -> (a -> String) -> m a -> Assertion m liftAssertionM tester explainer actualM = let assertPure = liftAssertionPure tester explainer in do actual <- actualM assertPure actual -- |Assert that a pure traversable structure (such as a list) is -- empty. emptyP :: (Monad m, Traversable t) => t a -> Assertion m emptyP = liftAssertionPure null (\ _ -> "Expected empty structure but got non-empty") -- |Assert that a traversable structure (such as a list) returned from -- a computation is empty. empty :: (Monad m, Traversable t) => m (t a) -> Assertion m empty = assertionPtoM emptyP -- |Assert that a pure traversable structure (such as a list) is -- nonempty. nonemptyP :: (Monad m, Traversable t) => t a -> Assertion m nonemptyP = liftAssertionPure (not . null) (\ _ -> "Expected non-empty structure but got empty") -- |Assert that a traversable structure (such as a list) returned from -- a computation is non-empty. nonempty :: (Monad m, Traversable t) => m (t a) -> Assertion m nonempty = assertionPtoM nonemptyP -- |Assert that a `Maybe` value is `Nothing`. nothingP :: Monad m => Maybe a -> Assertion m nothingP = liftAssertionPure isNothing (\ _ -> "Expected empty Maybe value but got non-Nothing") -- |Assert that a `Maybe` result ofa computation is `Nothing`. nothing :: Monad m => m (Maybe a) -> Assertion m nothing = assertionPtoM nothingP