{- |
This is the main testing module. Start reading here if you want to
know what this package is all about.
There's a documentation section at the bottom of this page. You
might want to start by reading that. Otherwise, here's a quick
summary:
* You create 'Test' objects to represent your tests.
* @'run_test' :: 'Test' -> 'IO' 'Bool'@ to quickly run a test
interactively (e.g., during debugging activity).
* 'run_test_full' allows more control, including recording detailed
test results to an XML log file.
* @'test' :: 'Bool' -> 'Test'@ creates a test from pure code.
* @('?=') :: 'Eq' x => x -> x -> 'Test'@ for tests with known
answers.
* Tests can be annotated with 'title', 'argument', 'temporary',
'note' and so on.
* @'tests' :: ['Test'] -> 'Test'@ for combining multiple tests
into a single 'Test' object. Tests can be nested arbitrarily
in this mannar to group related tests together.
* @'testIO' :: 'IO' 'Bool' -> 'Test'@ for tests that need to
perform I/O.
* The 'TestM' monad supports 'liftIO' and allows limited test
annotations from within monadic code.
* @'testM' :: 'TestM' 'Bool' -> 'Test'@ to use the 'TestM' monad.
* 'throws', 'throwsIO', 'throwsM' to test for exceptions.
-}
module Test.AC.Test
(
-- * Types
Test(),
-- * Pure tests
-- ** Creation
-- *** Simple tests
test, inapplicable,
-- *** Exceptions
throws_, throws,
-- *** Comparisons
(?=), (?/=), (?<), (?<=), (?>), (?>=),
-- ** Annotations
title, argument, argument_, temporary, temporary_, note,
-- * Impure tests
-- ** In the @IO@ monad
testIO, testIO3, throws_IO, throwsIO,
-- ** In the @TestM@ monad
-- *** Types
TestM (),
-- *** Creation
testM, throws_M, throwsM,
-- *** Annotations
inapplicableM, temporaryM, temporaryM_, noteM,
-- * Combining tests
tests, alternatives,
-- * Running tests
run_test, run_test_full, TestConfig (..), default_config,
-- * Mini-guide
-- $Guide
)
where
import Control.Monad.IO.Class
import System.IO (stdout) -- For Haddock.
import Test.AC.Private
---------------------------------------------------------------------
-- | An executable test.
newtype Test = Test (LogM Bool)
---------------------------------------------------------------------
result_nostack :: Bool -> LogM Bool
result_nostack b =
if b
then log_mark "pass" >> return b
else log_mark "fail" >> return b
result :: Bool -> LogM Bool
result b =
if b
then result_nostack b
else stack_trace >> result_nostack b
stack_report_error :: String -> LogM ()
stack_report_error msg = stack_report ("Test.AC.Test." ++ msg)
---------------------------------------------------------------------
{- |
Create a 'Test' from a simple 'Bool' value.
The test passes if the value is 'True'. The test fails if the value
is 'False', or if an exception is thrown in the course of computing
the value.
-}
test :: Bool -> Test
test b0 = Test $ do
log_mark "pure"
log_element "result" $ do
mb1 <- force b0
case mb1 of
Just True -> result True
Just False -> do
stack_report_error "test: False"
result False
Nothing -> do
stack_report_error "test: Exception thrown."
result False
{- |
This test always succeeds, but writes a note in the log to say that
the test case was \"inapplicable\".
This is generally useful if you have a test generation function
which doesn't work for certain combinations of inputs. In that
instance, the test still passes, but there is a note in the log
letting you know it was only a \"null\" test.
-}
inapplicable :: Test
inapplicable = Test $ do
log_mark "inapplicable"
log_element "result" $ result_nostack True
---------------------------------------------------------------------
{- |
Test for exceptions.
Ordinarily, any test which throws an exception is deemed to have
failed. However, this test /passes/ if evaluating the argument to
WHNF causes an exception to be thrown. The test /fails/ if no
exception is thrown.
This can be useful for checking that functions reject invalid input
by throwing an exception. (Of course, you cannot check that the
/correct/ exception is thrown!)
If WHNF is not enough to trigger the exception, you can wrap the
expression in some suitable forcing function. (The function
'length' '.' 'show' can sometimes be used for this purpose.)
Note that an infinite loop is not an exception (unless the loop
exhausts some resource).
If an exception is not thrown, the actual value returned is not
recorded. See 'throws' for a function that records this
information. (Note that this requires adding a 'Show' constraint.)
-}
throws_ :: x -> Test
throws_ x0 = Test $
log_element "result" $ do
mx1 <- force x0
case mx1 of
Just _ -> do
stack_report_error "throws_: No exception was thrown."
result False
Nothing -> result True
{- |
Test for exceptions.
Ordinarily, any test which throws an exception is deemed to have
failed. However, this test /passes/ if evaluating the argument to
WHNF causes an exception to be thrown. The test /fails/ if no
exception is thrown.
This can be useful for checking that functions reject invalid input
by throwing an exception. (Of course, you cannot check that the
/correct/ exception is thrown!)
If WHNF is not enough to trigger the exception, you can wrap the
expression in some suitable forcing function. (The function
'length' '.' 'show' can sometimes be used for this purpose.)
Note that an infinite loop is not an exception (unless the loop
exhausts some resource).
If no exception is thrown, the actual value returned is recorded.
This requires adding a 'Show' constraint. See 'throws_' for a
function without this constraint.
-}
throws :: Show x => x -> Test
throws x0 = Test $
log_element "result" $ do
mx1 <- force x0
case mx1 of
Just x1 -> do
log_element_ "value" $ show x1
stack_report_error "throws: No exception was thrown."
stack_report $ "Result: " ++ show x1
result False
Nothing -> result True
---------------------------------------------------------------------
infix 4 ?=, ?/=, ?<, ?<=, ?>, ?>=
{- |
Compare two values for equality.
The right-hand value is the \"target\" value, and the left-hand
value (next to the @?@ sign) is the \"actual\" value. The test
passes if both values are equal according to '=='. The test fails
if any exceptions are thrown by '==' or 'show'.
This operator has the same precedence as '==' (i.e., 4).
-}
(?=) :: (Eq x, Show x) => x -> x -> Test
(?=) =
test_compare
(==)
(log_mark "equal-to-target")
"(?=): Values do not match."
"(?=): Exception in Prelude.(==)."
{- |
Compare two values for inequality.
The right-hand value is the \"target\" value, and the left-hand
value (next to the @?@ sign) is the \"actual\" value. The test
passes if both values are unequal according to '/='. The test fails
if any exceptions are thrown by '/=' or 'show'.
This operator has the same precedence as '/=' (i.e., 4).
-}
(?/=) :: (Eq x, Show x) => x -> x -> Test
(?/=) =
test_compare
(/=)
(log_mark "not-equal-to-target")
"(?/=): Values are equal."
"(?/=): Exception in Prelude.(/=)."
{- |
Compare two values for inequality.
The right-hand value is the \"target\" value, and the left-hand
value (next to the @?@ sign) is the \"actual\" value. The test
passes if the actual value is less than the target value according
to '<'. The test fails if any exceptions are thrown by '<' or
'show'.
This operator has the same precedence as '<' (i.e., 4).
-}
(?<) :: (Ord x, Show x) => x -> x -> Test
(?<) =
test_compare
(<)
(log_mark "less-than-target")
"(?<): Actual is no less than target."
"(?<): Exception in Prelude.(<)."
{- |
Compare two values for inequality.
The right-hand value is the \"target\" value, and the left-hand
value (next to the @?@ sign) is the \"actual\" value. The test
passes if the actual value is less than or equal to the target
value according to '<='. The test fails if any exceptions are
thrown by '<=' or 'show'.
This operator has the same precedence as '<=' (i.e., 4).
-}
(?<=) :: (Ord x, Show x) => x -> x -> Test
(?<=) =
test_compare
(<=)
(log_mark "less-than-target" >> log_mark "equal-to-target")
"(?<=): Actual is greater than target."
"(?<=): Exception in Prelude.(<=)."
{- |
Compare two values for inequality.
The right-hand value is the \"target\" value, and the left-hand
value (next to the @?@ sign) is the \"actual\" value. The test
passes if the actual value is more than the target value according
to '>'. The test fails if any exceptions are thrown by '>' or
'show'.
This operator has the same precedence as '>' (i.e., 4).
-}
(?>) :: (Ord x, Show x) => x -> x -> Test
(?>) =
test_compare
(>)
(log_mark "more-than-target")
"(?>): Actual is no greater than target."
"(?>): Exception in Prelude.(>)."
{- |
Compare two values for inequality.
The right-hand value is the \"target\" value, and the left-hand
value (next to the @?@ sign) is the \"actual\" value. The test
passes if the actual value is more than or equal to the target
value according to '>='. The test fails if any exceptions are
thrown by '>=' or 'show'.
This operator has the same precedence as '>=' (i.e., 4).
-}
(?>=) :: (Ord x, Show x) => x -> x -> Test
(?>=) =
test_compare
(>=)
(log_mark "more-than-target" >> log_mark "equal-to-target")
"(?>=): Actual is less than target."
"(?>=): Exception in Prelude.(>=)."
test_compare :: (Show x) => (x -> x -> Bool) -> LogM () -> String -> String -> x -> x -> Test
test_compare op marks msg_fail msg_exception act exp = Test $ do
log_mark "pure"
log_element "compare" $ do
marks
log_element_ "target" (show exp)
log_element_ "actual" (show act)
stack_report $ "Target: " ++ show exp
stack_report $ "Actual: " ++ show act
log_element "result" $ do
mb1 <- force (act `op` exp)
case mb1 of
Just True -> result True
Just False -> do
stack_report_error msg_fail
result False
Nothing -> do
stack_report_error msg_exception
result False
---------------------------------------------------------------------
perform_test :: String -> LogM Bool -> LogM Bool
perform_test name test = do
mb <- log_exceptions test
case mb of
Just b -> return b
Nothing -> do
stack_report_error (name ++ ": Exception accessing test object.")
log_element "result" $ result False
{- |
Attach a title to a test.
This title is an arbitrary human-readable label. It is recorded in
relation to the test, but has no other function.
-}
title :: String -> Test -> Test
title title (Test test) = Test $ do
log_element_ "title" title
stack_report ("Test '" ++ title ++ "'")
print_title title
perform_test "title" test
{- |
Attach an argument value note.
The 'String' is the argument name, and the @x@ is that argument's
value, which must implement 'show'.
-}
argument :: (Show x) => String -> x -> Test -> Test
argument name x = argument_ name (show x)
{- |
Attach an argument value note.
The first 'String' is the argument name, and the second is some
suitable textual representation of that argument's value.
-}
argument_ :: String -> String -> Test -> Test
argument_ name x (Test test) = Test $ do
log_element "argument" $ do
log_element_ "name" name
log_element_ "value" x
stack_report $ "Argument '" ++ name ++ "' = " ++ x
perform_test "argument_" test
{- |
Note down a temporary intermediate value computed in the process
of constructing a test.
The 'String' is a name for this value, and the @x@ is the value
itself, which must implement 'show'.
-}
temporary :: (Show x) => String -> x -> Test -> Test
temporary name x = temporary_ name (show x)
{- |
Note down a temporary intermediate value computed in the process
of constructing a test.
The first 'String' is the temporary name, and the second is some
suitable textual representation of the temporary's value.
-}
temporary_ :: String -> String -> Test -> Test
temporary_ name x (Test test) = Test $ do
log_element "temporary" $ do
log_element_ "name" name
log_element_ "value" x
stack_report $ "Temporary '" ++ name ++ "' = " ++ x
perform_test "temporary_" test
{- |
Add a textual note to the test log.
-}
note :: String -> Test -> Test
note txt (Test test) = Test $ do
log_element_ "note" txt
stack_report $ "Note: " ++ txt
perform_test "note" test
---------------------------------------------------------------------
{- |
Create a 'Test' from an 'IO' action that returns a 'Bool'.
The test passes if the value returned is 'True'. The test fails if
the value returned is 'False', or if an uncaught exception escapes.
-}
testIO :: IO Bool -> Test
testIO act = Test $ do
log_mark "impure"
mb0 <- log_element "run" $ log_exceptions $ rawIO act
log_element "result" $ do
mb1 <- maybe (return Nothing) force mb0
case mb1 of
Just True -> result True
Just False -> do
stack_report_error "testIO: return False"
result False
Nothing -> do
stack_report_error "testIO: Uncaught exception."
result False
{- |
Create a 'Test' from an 'IO' action with seperate set-up and
clean-up phases.
The first argument is a set-up action. This might be used to
initialise mutable storage or create disk structures, or just to
open some handles. Its result is passed to the second argument,
which then does the actual test propper. Finally, the third
argument is run (again with the set-up result as argument) to do
any post-test clean-up operations required. Its result is
discarded.
If any of these 'IO' actions throw an exception, the test is
marked failed. Note that if the set-up action throws an exception,
the test and clean-up actions are not run. (If only the main test
action throws an exception, the clean-up is still run.)
-}
testIO3 :: IO x -> (x -> IO Bool) -> (x -> IO y) -> Test
testIO3 act1 act2 act3 = Test $ do
log_mark "impure"
mx <- log_element "set-up" $ log_exceptions $ rawIO act1
case mx of
Nothing -> do
stack_report_error "testIO3: Uncaught exception in set-up."
log_element "result" $ result False
Just x -> do
mb0 <- log_element "run" $ log_exceptions $ rawIO (act2 x)
mc <- log_element "clean-up" $ log_exceptions $ rawIO (act3 x)
log_element "result" $ do
mb1 <- maybe (return Nothing) force mb0
case mb1 of
Just True ->
case mc of
Just _ -> result True
Nothing -> do
stack_report_error "testIO3: Uncaught exception in clean-up."
result False
Just False -> do
stack_report_error "testIO3: return False"
result False
Nothing -> do
stack_report_error "testIO3: Uncaught exception."
result False
{- |
Test for exceptions in the 'IO' monad.
Ordinarily, any test which throws an exception is deemed to have
failed. However, this test /passes/ if evaluating the action's
result to WHNF causes an exception to be thrown. The test /fails/
if no exception is thrown.
This can be useful for checking that a function rejects invalid
input by throwing an exception, or that invalid I/O operations are
reported. (Of course, you cannot check that the /correct/ exception
is thrown!)
Note that the 'IO' action is run /and/ its result is reduced (to
WHNF only). Note also that infinite loops are not exceptions
(unless the loop exhausts some resource).
If no exception is thrown, the actual value returned is not
recorded. See 'throwsIO' for a function which does record this
information. (This requires adding a 'Show' constraint.)
-}
throws_IO :: IO x -> Test
throws_IO act = Test $ do
mx0 <- log_element "run" $ log_exceptions $ rawIO act
log_element "result" $ do
case mx0 of
Nothing -> result True
Just x0 -> do
mx1 <- force x0
case mx1 of
Nothing -> result True
Just _ -> do
stack_report_error "throwsIO: No exception was thrown."
result False
{- |
Test for exceptions in the 'IO' monad.
Ordinarily, any test which throws an exception is deemed to have
failed. However, this test /passes/ if evaluating the action's
result to WHNF causes an exception to be thrown. The test /fails/
if no exception is thrown.
This can be useful for checking that a function rejects invalid
input by throwing an exception, or that invalid I/O operations are
reported. (Of course, you cannot check that the /correct/ exception
is thrown!)
Note that the 'IO' action is run /and/ its result is reduced (to
WHNF only). Note also that infinite loops are not exceptions
(unless the loop exhausts some resource).
If no exception is thrown, the actual value returned is recorded.
This requires adding a 'Show' constraint; see 'throws_IO' for a
function without this constraint.
-}
throwsIO :: Show x => IO x -> Test
throwsIO act = Test $ do
mx0 <- log_element "run" $ log_exceptions $ rawIO act
log_element "result" $ do
case mx0 of
Nothing -> result True
Just x0 -> do
mx1 <- force x0
case mx1 of
Nothing -> result True
Just x1 -> do
log_element_ "value" $ show x1
stack_report_error "throwsIO: No exception was thrown."
stack_report $ "Result: " ++ show x1
result False
---------------------------------------------------------------------
{- |
The test monad.
Notice the 'MonadIO' instance. This allows you to call 'liftIO' to
perform arbitrary 'IO' actions at any point within the test monad.
-}
newtype TestM x = TestM (LogM x)
instance Monad TestM where
return = TestM . return
(TestM m1) >>= f = TestM $ m1 >>= \ x -> let TestM m2 = f x in m2
instance MonadIO TestM where
liftIO act = TestM $ rawIO act
---------------------------------------------------------------------
{- |
Create a 'Test' from a 'TestM' action.
The test passes if the 'TestM' action returns 'True'. The test
fails if it returns 'False' or an uncaught exception escapes.
-}
testM :: TestM Bool -> Test
testM (TestM act) = Test $ do
log_mark "impure"
mb0 <- log_element "run" $ log_exceptions act
log_element "result" $ do
mb1 <- maybe (return Nothing) force mb0
case mb1 of
Just True -> result True
Just False -> do
stack_report_error "testM: return False"
result False
Nothing -> do
stack_report_error "testM: Uncaught exception."
result False
{- |
Check a 'TestM' action for exceptions.
Ordinarily, any test which throws an exception is deemed to have
failed. However, this test /passes/ if evaluating the action's
result to WHNF causes an exception to be thrown. The test /fails/
if no exception is thrown.
This can be useful for checking that a function rejects invalid
input by throwing an exception, or that invalid I/O operations are
reported. (Of course, you cannot check that the /correct/ exception
is thrown!)
Note that the 'TestM' action is run /and/ its result is reduced (to
WHNF only). Note also that infinite loops are not exceptions
(unless the loop exhausts some resource).
If no exception is thrown, the actual value returned is not
recorded. See 'throwsM' for a function that does record the value.
This requires adding a 'Show' constraint.
-}
throws_M :: TestM x -> Test
throws_M (TestM act) = Test $ do
log_mark "impure"
mx0 <- log_element "run" $ log_exceptions act
log_element "result" $ do
case mx0 of
Nothing -> result True
Just x0 -> do
mx1 <- force x0
case mx1 of
Nothing -> result True
Just _ -> do
stack_report_error "throwsM: No exception was thrown."
result False
{- |
Check a 'TestM' action for exceptions.
Ordinarily, any test which throws an exception is deemed to have
failed. However, this test /passes/ if evaluating the action's
result to WHNF causes an exception to be thrown. The test /fails/
if no exception is thrown.
This can be useful for checking that a function rejects invalid
input by throwing an exception, or that invalid I/O operations are
reported. (Of course, you cannot check that the /correct/ exception
is thrown!)
Note that the 'TestM' action is run /and/ its result is reduced (to
WHNF only). Note also that infinite loops are not exceptions
(unless the loop exhausts some resource).
If no exception is thrown, the actual value returns is recorded.
This requires adding a 'Show' constraint. See 'throws_M' for a
function without this constraint.
-}
throwsM :: Show x => TestM x -> Test
throwsM (TestM act) = Test $ do
log_mark "impure"
mx0 <- log_element "run" $ log_exceptions act
log_element "result" $ do
case mx0 of
Nothing -> result True
Just x0 -> do
mx1 <- force x0
case mx1 of
Nothing -> result True
Just x1 -> do
log_element_ "value" $ show x1
stack_report_error "throwsM: No exception was thrown."
stack_report $ "Result: " ++ show x1
result False
---------------------------------------------------------------------
{- |
Mark the current test as \"inapplicable\" and return 'True'.
(See 'inapplicable'.)
-}
inapplicableM :: TestM Bool
inapplicableM = TestM $ do
log_mark "inapplicable"
return True
{- |
Note down a temporary intermediate value computed in the process
of constructing a test.
The 'String' is a name for this value, and the @x@ is the value
itself, which must implement 'show'.
-}
temporaryM :: (Show x) => String -> x -> TestM ()
temporaryM name x = temporaryM_ name (show x)
{- |
Note down a temporary intermediate value computed in the process
of constructing a test.
The first 'String' is the name, and the second is some suitable
textual representation of the value.
-}
temporaryM_ :: String -> String -> TestM ()
temporaryM_ name x = TestM $ do
log_element "temporary" $ do
log_element_ "name" name
log_element_ "value" x
stack_report $ "Temporary '" ++ name ++ "' = " ++ x
{- |
Add a textual note to the log file.
-}
noteM :: String -> TestM ()
noteM txt = TestM $ do
log_element_ "note" txt
stack_report $ "Note: " ++ txt
---------------------------------------------------------------------
{- |
Combine multiple tests into a single composite test.
The composite test fails if any of its constituent tests fail.
Whether the remaining tests are run depends on the testing mode
(the 'cfg_FailAbort' parameter in 'TestConfig').
Essentially, this takes the logical-AND of several tests. You can
achieve the same result using the normal '&&' operator or the 'and'
function, operating on plain 'Bool' values rather than 'Test'
objects. However, by turning subexpressions into 'Test' objects and
using 'tests', the result of each subexpression will be logged to
file in addition to the overall result. Depending on the context,
that may or may not be helpful. You decide which you want.
-}
tests :: [Test] -> Test
tests = Test . root
where
root ts = do
mb <- log_element "tests" $ work True ts
log_element "result" $
case mb of
Just b -> result_nostack b
Nothing -> result False
work b0 ts0 = do
mts1 <- force ts0
case mts1 of
Just [] -> return (Just b0)
Just (Test t : ts2) -> do
b1 <- subroutine $ log_element "test" $ perform_test "tests" t
if b0 && b1
then work True ts2
else do
s <- fail_stop
if s
then return (Just False)
else work False ts2
Nothing -> do
stack_report_error "tests: Exception accessing test list spine."
return Nothing
{- |
Create a composite test which passes if at least one child test
passes.
All child tests are always run, regardless of error reporting mode.
No test failures are reported, unless all children fail.
Essentially, this takes the logical-OR of several tests. You can
achieve the same result using the normal '||' operator or the 'or'
function, operating on plain 'Bool' values rather than 'Test'
objects. However, by turning subexpressions into 'Test' objects and
using 'alternatives', the result of each subexpression will be
logged to file in addition to the overall result. Depending on the
context, that may or may not be helpful. You decide which you want.
-}
alternatives :: [Test] -> Test
alternatives = Test . root
where
root ts = do
mb <- log_element "alternatives" $ work False ts
log_element "result" $
case mb of
Just True -> result True
Just False -> do
stack_report_error "alternatives: All child tests failed."
result False
Nothing -> result False
work b0 ts0 = do
mts1 <- force ts0
case mts1 of
Just [] -> return (Just b0)
Just (Test t : ts2) -> do
b1 <- log_element "test" $ silence_failures $ subroutine $ perform_test "alternatives" t
work (b0 || b1) ts2
Nothing -> do
stack_report_error "alternatives: Exception accessing test list spine."
return Nothing
---------------------------------------------------------------------
{- |
Execute a test.
Ordinarily, \"the test\" will be a composite test created with
'tests', and will actually contain multiple sub-tests within it.
A 'Bool' value is returned indicating whether the test was
successful or not. Test progress information is printed to
'stdout'. If any test fails, detailed information for that test is
printed to 'stdout', and testing aborts.
For more control, see 'run_test_full'.
-}
run_test :: Test -> IO Bool
run_test = run_test_full default_config
{- |
Execute a test.
Ordinarily, \"the test\" will be a composite test created with
'tests', and will actually contain multiple sub-tests within it.
A 'Bool' value is returned indicating whether the test was
successful or not. Test progress information is printed to
'stdout'. Various testing options can be configured using the
'TestConfig' argument. In particular, it is possible to log
detailed testing data to an XML log file (the 'cfg_LogFile'
parameter).
The related 'run_test' function runs a test with the
'default_config' test settings, which are useful for quick
interactive testing during a debugging session.
-}
run_test_full :: TestConfig -> Test -> IO Bool
run_test_full c (Test t) = do
putStrLn "Starting test run..."
b <- case cfg_LogFile c of
Nothing -> run (cfg_FailAbort c) (cfg_FailReport c) $ t
Just f1 -> run_file f1 (cfg_FailAbort c) (cfg_FailReport c) $ do
log_header
case cfg_LogXSL c of
Nothing -> return ()
Just f2 -> log_XSL f2
log_element "test" t
putStr "Test run "
if b
then putStrLn "PASSED."
else putStrLn "FAILED."
putStrLn ""
return b
-- | Configuration settings for a test run.
data TestConfig =
TestConfig
{
{- |
If 'Nothing', no log file is produced. Otherwise, this is the
full path to the XML log file.
-}
cfg_LogFile :: Maybe FilePath,
{- |
Path to an XSL file. If given, the XML log file will use this
XSL as a stylesheet. This value is ignored if no XML log is
produced.
-}
cfg_LogXSL :: Maybe String,
{- |
If 'True', report test failures to 'stdout'. If 'False', just
report test progress to 'stdout'.
-}
cfg_FailReport :: Bool,
{- |
If 'True', abort testing if a test fails, otherwise continue
testing. (In other words, 'False' causes /all/ tests to be
run, regardless of test failures, while 'True' runs until a
test fails and then stops.)
-}
cfg_FailAbort :: Bool
}
{- |
The default test configuration, as used by 'run_test'.
>cfg_LogFile = Nothing
>cfg_LogXSL = Nothing
>cfg_FailReport = True
>cfg_FailAbort = True
You can use this as a starting point if you only want to customise
a few test settings. (More options may be added in future.)
-}
default_config :: TestConfig
default_config =
TestConfig
{
cfg_LogFile = Nothing,
cfg_LogXSL = Nothing,
cfg_FailReport = True,
cfg_FailAbort = True
}
---------------------------------------------------------------------
{- $Guide
Tests are represented by 'Test' objects.
You can run such a test using
@'run_test' :: 'Test' -> 'IO' 'Bool'@.
This is intended for quickly running a test or two interactively to
see if those code changes you just made fixed the bug or not. For
running an entire test suite, you probably want 'run_test_full'.
This uses a 'TestConfig' object to set testing options; in
particular, detailed test information can be written to an XML log
file. Most test annotations only affect the log file, not the
visible output on 'stdout'.
You can create a test in several ways. The easiest is
@'test' :: 'Bool' -> 'Test'@.
For example, a simple hard-coded test might look like
>t_null_empty :: Test
>t_null_empty = test $ SET.null SET.empty
You can also add a test title:
>t_null_empty :: Test
>t_null_empty =
> title "null empty" $
> test $
> SET.null SET.empty
Running this test produces a log entry which looks something like
>
> null empty
>
>
>
(Assuming the implementation of your 'SET' module isn't broken,
obviously.) I like to set the test 'title' to be the Haskell
expression that I'm testing (or some approximation of it), but
there's no law that says you have to do it like that. You can name
it whatever you like.
More often, you'll have a function that takes some inputs and
generates a test object. For example:
>p_head_member :: Ord x => [x] -> Test
>p_head_member xs =
> title "head xs `member` fromList xs" $
> argument "xs" xs $
> let set = SET.fromList xs in
> temporary "fromList xs" set $
> if LIST.null xs
> then inapplicable
> else test $ head xs `SET.member` set
Running this test might produce a log entry such as
>
> head xs `member` fromList xs
> xsfromList [3,1,4]
> fromList xsfromList [1,3,4]
>
>
>
Of course, 'head' is not defined on an empty list. In that case,
the code above is configured to mark the test as 'inapplicable'.
The resulting log entry looks like
>
> head xs `member` fromList xs
> xsfromList []
> fromList xsfromList []
>
>
>
The test is still \"successul\", but we have marked it so that
anyone reading the log will know that this particular test
\"did nothing\".
For tests with a known correct answer, you can also use the
@('?=') :: 'Eq' x => x -> x -> 'Test'@
operator. For example,
>p_size :: Ord x => [x] -> Test
>p_size xs =
> title "size (fromList xs) == length (nub xs)" $
> argument "xs" xs $
> temporary "fromList xs" (SET.fromList xs) $
> temporary "nub xs" (LIST.nub xs) $
> SET.size (SET.fromList xs) ?= LIST.length (LIST.nub xs)
Running that might produce something like
>
> size (fromList xs) == length (nub xs)
> xs[3,1,4,1]
> fromList xsfromList [1,3,4]
> nub xs[3,1,4]
>
>
>
> 3
> 3
>
>
>
In this instance, the output indicates that @size (fromList xs)@
was supposed to yield 3, and the value /actually/ produced was also
3 - and hence, the test passed. There are several similar operators
such as '?>', '?<' and so forth for inequality testing using an
'Ord' instance.
One test case probably isn't particularly useful. But using the
@'tests' :: ['Test'] -> 'Test'@
function, you can combine multiple tests into a single composite
'Test' object. You can have multiple levels of this grouping to
organise related tests together. Composite tests can of course
have titles just like any other kind of test.
Note that this package (unlike, say, QuickCheck) provides no
facility for generating test data automatically. That's your
problem. You can solve it in several ways. One idea might be
>t_size :: Test
>t_size = tests $ map p_size [ [], [5], [5,5], [4,6], [1..10] ]
Because this package doesn't deal with test data generation, you
are free to use any approach you like. In particular, different
properties can be tested with different test data (rather than
relying on the type system to select test data for you), the data
can be random or deterministic, and it can even be loaded from an
external disk file if you like. Hell, you can even spawn an
external OS process running a reference implementation, and then
compare the results from your Haskell code against that. The choice
is endless.
If you want to test code that needs to perform I/O, you can use the
@'testIO' :: 'IO' 'Bool' -> 'Test'@
function. Impure tests such as this can be annotated in all the
usual ways. For example,
>p_makedir :: FilePath -> Test
>p_makedir dir =
> title "p_makedir" $
> argument "dir" dir $
> testIO $ do
> createDirectory dir
> doesDirectoryExist dir
If the test involves setup and cleanup steps, or just resource
allocation and subsequent deallocation, then you can build those
into the main test body, or you can use 'testIO3' instead. This has
the added advantage that it handles exceptions in the test body and
still runs the cleanup stage. (We /are/ testing code which might
well crash, after all.) The choice is entirely up to you.
While you're in the 'IO' monad, you unfortunately cannot use
functions such as 'temporary' or 'note'. To get around this
limitation, you can use the 'TestM' monad. It implements 'liftIO',
so you can still perform any I/O operations you want. But it also
provides 'temporaryM' and 'noteM', which run inside the monad. The
@'testM' :: 'TestM' 'Bool' -> 'Test'@ function lets you wrap
everything up onto a regular 'Test' object when you're done.
Some functions (e.g., 'head') are supposed to throw an exception
under certain circumstances. To check that they do (rather than,
say, return gibberish data instead), you can use 'throws' (or
'throwsIO' in the 'IO' monad, or 'throwsM' in the 'TestM' monad).
-}