test-simple-0.1.2: src/Test/Simple.hs
{-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses, TemplateHaskell #-}
----------------------------------------------------------------------
-- |
-- Module : Test.Simple
-- Copyright : (c) Boris Sukholitko 2012
-- License : BSD3
--
-- Maintainer : boriss@gmail.com
-- Stability : experimental
--
-- Test.Simple is yet another testing library for Haskell. It has testing primitives
-- familiar to recovering Perl programmers :).
--
-- Here is example suitable for cabal test-suite integration. Note that TemplateHaskell
-- usage is optional and is needed for test failure locations only.
--
-- @
--{-\# LANGUAGE TemplateHaskell \#-}
--
--import Test.Simple
--import Control.Monad
--
--main :: IO ()
--main = testSimpleMain $ do
-- plan 7
-- ok True
-- is 1 1
-- isnt \"a\" \"b\"
-- like \"abcd\" \"bc\"
-- unlike \"a\" \"b\"
-- diag \"Successful so far, failures follow ...\"
-- $loc >> ok False \-\- location will be recorded
-- is \"a\" \"b\" >>= guard
-- diag \"I am not being called\" \-\- not reached because of the guard: MonadPlus FTW!
-- @
--
----------------------------------------------------------------------
module Test.Simple (
-- * Types
TestSimpleT, Likeable(isLike),
-- * Main
testSimpleMain, runTestSimple,
-- * Plan
plan,
-- * Test functions
ok, isnt, is, like, unlike, isRight,
-- * Diagnostics
loc, diag) where
import Control.Monad.Trans.State.Plus
import Control.Monad.State
import System.Exit (exitFailure)
import Data.List (isInfixOf)
import System.IO (hPutStrLn, stderr)
import qualified Language.Haskell.TH as TH
-- | Is used in 'like', 'unlike' tests.
class Likeable a b where
-- | Returns 'True' if @a@ is like @b@
isLike :: a -> b -> Bool
instance Eq a => Likeable [a] [a] where
isLike = flip isInfixOf
data TSOutput = StdOut String | StdErr String
data TSState = TSS { tsCounter :: Int, tsFailed :: Int, tsPlanned :: Int, tsLoc :: Maybe TH.Loc
, tsOutput :: [TSOutput] }
-- | Test.Simple is implemented as monad transformer.
newtype TestSimpleT m a = MkTST { unTST :: StatePlusT TSState m a }
deriving (Functor, MonadTrans, Monad, MonadPlus
, MonadState TSState, MonadIO)
emptyState :: TSState
emptyState = TSS 0 0 0 Nothing []
finishTS :: Monad m => TestSimpleT m a -> m (Bool, TSState)
finishTS m = do
ms <- execStatePlusT (unTST m) emptyState
finState <- execStatePlusT (unTST finish) ms
return (not $ isFailed finState, finState)
where
finish = do
s <- get
let failed = tsFailed s > 0
let mismatch = (tsPlanned s /= tsCounter s)
if failed
then diag $ "Looks like you failed " ++ show (tsFailed s)
++ " test of " ++ show (tsPlanned s) ++ "."
else if mismatch
then diag $ "Looks like you planned " ++ show (tsPlanned s)
++ " tests but ran " ++ show (tsCounter s) ++ "."
else return ()
modify finOutput
return $ not (failed || mismatch)
finOutput s = s { tsOutput = (StdOut $ "1.." ++ show (tsPlanned s)):(reverse $ tsOutput s) }
isFailed s = tsFailed s > 0 || (tsPlanned s /= tsCounter s)
-- | Runs 'TestSimpleT' transformer. Returns whether the tests where successful and resulting
-- output.
runTestSimple :: Monad m => TestSimpleT m a -> m (Bool, [String])
runTestSimple m = do
(b, s) <- finishTS m
return (b, map toStr $ tsOutput s)
where toStr (StdOut str) = str
toStr (StdErr str) = str
-- | Runs 'TestSimpleT' transformer in 'IO'. Outputs results in TAP format.
-- Exits with error on test failure.
--
testSimpleMain :: MonadIO m => TestSimpleT m a -> m ()
testSimpleMain m = do
(b, s) <- finishTS m
liftIO $ do
mapM_ printLine $ tsOutput s
unless b exitFailure
where printLine (StdOut s) = putStrLn s
printLine (StdErr s) = hPutStrLn stderr s
-- | Is @Bool@ ok?
ok :: Monad m => Bool -> TestSimpleT m Bool
ok b = do
s <- get
let oks = "ok " ++ show (tsCounter s + 1)
put $ s { tsCounter = (tsCounter s) + 1
, tsFailed = (tsFailed s) + if b then 0 else 1
, tsOutput = (StdOut $ if b then oks else "not " ++ oks):(tsOutput s)
}
unless b $ diagFailed (tsLoc s)
return b
where diagFailed (Just l) = diag $ concat [
" Failed test at ", TH.loc_filename l, " line ", show $ fst $ TH.loc_start l ]
diagFailed _ = diag $ " Failed test at unknown location."
(>>?) :: Monad m => m Bool -> m () -> m Bool
m >>? d = do
b <- m
unless b d
return b
quote :: Show a => a -> String
quote a = "'" ++ show a ++ "'"
diagVals :: Monad m => String -> String -> String -> String-> TestSimpleT m ()
diagVals as a bs b = do
diag $ concat [ spaces, as, " ", a ]
diag $ concat [ bs, " ", b ]
where spaces = take (length bs - length as) $ cycle " "
-- | Are values different?
isnt :: (Eq a, Show a, Monad m) => a -> a -> TestSimpleT m Bool
isnt a b = ok (a /= b) >>? diagVals "got:" (quote a) "expected:" "anything else"
-- | Are values equal?
is :: (Eq a, Show a, Monad m) => a -> a -> TestSimpleT m Bool
is a b = ok (a == b) >>? diagVals "got:" (quote a) "expected:" (quote b)
-- | Is @a@ like @b@?
like :: (Show a, Show b, Likeable a b, Monad m) => a -> b -> TestSimpleT m Bool
like a b = ok (isLike a b) >>? diagVals "" (quote a) "doesn't match" (quote b)
-- | Is @a@ unlike @b@?
unlike :: (Show a, Show b, Likeable a b, Monad m) => a -> b -> TestSimpleT m Bool
unlike a b = ok (not $ isLike a b) >>? diagVals "" (quote a) "matches" (quote b)
-- | Is 'Either' right?
isRight :: (Monad m, Show a) => Either a b -> TestSimpleT m Bool
isRight (Right _) = ok True
isRight (Left a) = ok False >>? diagVals "got Left:" (quote a) "expected:" "Right"
-- | Outputs diagnostics message.
diag :: Monad m => String -> TestSimpleT m ()
diag s = modify (\st -> st { tsOutput = (StdErr $ "# " ++ s):(tsOutput st) })
-- | Sets expected number of tests. Running more or less tests is considered failure.
-- Note, that plans are composable, e.g:
--
-- @
-- (plan 1 >> ok True) >> (plan 1 >> ok True)
-- @
--
-- will expect 2 tests.
plan :: Monad m => Int -> TestSimpleT m ()
plan i = modify (\st -> st { tsPlanned = tsPlanned st + i })
-- | Records current location to output in case of failures.
-- Necessary caveat: failing later without updating location produces the last location recorded.
loc :: TH.Q TH.Exp
loc = do
l <- TH.location
let ql = liftLoc l
[| modify (\s -> s { tsLoc = Just $ql }) |]
liftLoc :: TH.Loc -> TH.Q TH.Exp
liftLoc l = [| TH.Loc f p m s e |] where
f = TH.loc_filename l
p = TH.loc_package l
m = TH.loc_module l
s = TH.loc_start l
e = TH.loc_end l