{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, LambdaCase #-} module Test.Tasty.ExpectedFailure (expectFail, expectFailBecause, ignoreTest, ignoreTestBecause, wrapTest) where import Test.Tasty.Options import Test.Tasty.Runners import Test.Tasty.Providers #if MIN_VERSION_tasty(1,3,1) import Test.Tasty.Providers.ConsoleFormat ( ResultDetailsPrinter(..) ) #endif import Test.Tasty ( Timeout(..), askOption, localOption ) import Data.Typeable import Data.Tagged import Data.Maybe import Data.Monoid import Control.Exception ( displayException, evaluate, try, SomeException ) import Control.Concurrent.Timeout ( timeout ) data WrappedTest t = WrappedTest Timeout (IO Result -> IO Result) t deriving Typeable instance forall t. IsTest t => IsTest (WrappedTest t) where run opts (WrappedTest tmout wrap t) prog = -- Re-implement timeouts and exception handling *inside* the -- wrapper. The primary location for timeout and exception -- handling is in `executeTest` in the Tasty module's -- Test.Tasty.Run implementation, but that handling is above the -- level of this wrapper which therefore cannot absorb timeouts -- and exceptions as *expected* failures. let (pre,post) = case tmout of NoTimeout -> (fmap Just, fromJust) Timeout t s -> (timeout t, fromMaybe (timeoutResult t s)) timeoutResult t s = Result { resultOutcome = Failure $ TestTimedOut t , resultDescription = "Timed out after " <> s , resultShortDescription = "TIMEOUT" , resultTime = fromIntegral t #if MIN_VERSION_tasty(1,3,1) , resultDetailsPrinter = ResultDetailsPrinter . const . const $ return () #endif } exceptionResult e = Result { resultOutcome = Failure $ TestThrewException e , resultDescription = "Exception: " ++ displayException e , resultShortDescription = "FAIL" , resultTime = 0 #if MIN_VERSION_tasty(1,3,1) , resultDetailsPrinter = ResultDetailsPrinter . const . const $ return () #endif } forceList = foldr seq () in wrap $ try (pre (run opts t prog -- Ensure exceptions trying to show the -- failure result are caught as "expected" -- (see Issue #24 and note below) >>= \r -> evaluate (forceList (resultDescription r) `seq` forceList (resultShortDescription r) `seq` resultOutcome r `seq` r))) >>= \case Right r -> return (post r) Left (e :: SomeException) -> return $ exceptionResult e testOptions = retag (testOptions :: Tagged t [OptionDescription]) -- Note regarding post-run evaluate above: -- -- The normal behavior of tasty-expected-failure is to run the -- test, show the failure result, but then note that the failure -- is expected and not count that against a test failure. If the -- test unexpectedly succeeds, a message to that effect is -- printed, but there is no resultDescription display of the test -- inputs. -- -- As of Tasty 1.4, the core tasty code was enhanced to fix issue -- #280 in tasty: essentially the test result report is forced. -- However, when used with tests expected to fail that also throw -- exceptions when attempting to show the result, the forcing in -- Tasty 1.4 causes an exception to be thrown after the -- tasty-expected-failure protections but still within the realm -- where tasty would count it as a failure. The fix here attempts -- to `show` the failing value here in tasty-expected-failure; if -- an exception occurs during that `show` then code here will -- report it (somewhat incorrectly) via the exceptionResult above, -- where tasty's subsequent forcing of the text of that -- exceptionResult no longer causes an exception *there*. Since -- the value is only shown if there was already a failure, the -- reason is misleading but the outcome is consistent with the -- intent of tasty-expected-failure handling. -- | 'wrapTest' allows you to modify the behaviour of the tests, e.g. by -- modifying the result or not running the test at all. It is used to implement -- 'expectFail' and 'ignoreTest'. wrapTest :: (IO Result -> IO Result) -> TestTree -> TestTree wrapTest wrap = go where go (SingleTest n t) = askOption $ \(old_timeout :: Timeout) -> localOption NoTimeout $ -- disable Tasty's timeout; handled here instead SingleTest n (WrappedTest old_timeout wrap t) go (TestGroup name tests) = TestGroup name (map go tests) go (PlusTestOptions plus tree) = PlusTestOptions plus (go tree) go (WithResource spec gentree) = WithResource spec (go . gentree) go (AskOptions f) = AskOptions (go . f) -- | Marks all tests in the given test suite as expected failures: The tests will -- still be run, but if they succeed, it is reported as a test suite failure, -- and conversely a the failure of the test is ignored. -- -- Any output of a failing test is still printed. -- -- This is useful if, in a test driven development, tests are written and -- commited to the master branch before their implementation: It allows the -- tests to fail (as expected) without making the whole test suite fail. -- -- Similarly, regressions and bugs can be documented in the test suite this -- way, until a fix is commited, and if a fix is applied (intentionally or -- accidentially), the test suite will remind you to remove the 'expectFail' -- marker. expectFail :: TestTree -> TestTree expectFail = expectFail' Nothing -- | Like 'expectFail' but with additional comment expectFailBecause :: String -> TestTree -> TestTree expectFailBecause reason = expectFail' (Just reason) expectFail' :: Maybe String -> TestTree -> TestTree expectFail' reason = wrapTest (fmap change) where change r | resultSuccessful r = r { resultOutcome = Failure TestFailed , resultDescription = resultDescription r <> " (unexpected success" <> comment <> ")" , resultShortDescription = resultShortDescription r <> " (unexpected" <> comment <> ")" } | otherwise = r { resultOutcome = Success , resultDescription = resultDescription r <> " (expected failure)" , resultShortDescription = resultShortDescription r <> " (expected" <> comment <> ")" } "" `append` s = s t `append` s | last t == '\n' = t ++ s ++ "\n" | otherwise = t ++ "\n" ++ s comment = maybe "" (mappend ": ") reason -- | Prevents the tests from running and reports them as succeeding. -- -- This may be be desireable as an alternative to commenting out the tests. This -- way, they are still typechecked (preventing bitrot), and the test report -- lists them, which serves as a reminder that there are ignored tests. -- -- Note that any setup/teardown actions executed by 'Test.Tasty.withResource' -- are still executed. You can bypass this manually as in the following example: -- -- @ -- askOption $ \\(MyFlag b) -> if b -- then withResource mytest -- else ignoreTest . mytest $ return junkvalue -- @ ignoreTest :: TestTree -> TestTree ignoreTest = ignoreTest' Nothing -- | Like 'ignoreTest' but with additional comment ignoreTestBecause :: String -> TestTree -> TestTree ignoreTestBecause reason = ignoreTest' (Just reason) ignoreTest' :: Maybe String -> TestTree -> TestTree ignoreTest' reason = wrapTest $ const $ return $ (testPassed $ fromMaybe "" reason) { resultShortDescription = "IGNORED" }