{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-} -- -- Copyright (c) 2009-2022 Stefan Wehr - http://www.stefanwehr.de -- -- This library is free software; you can redistribute it and/or -- modify it under the terms of the GNU Lesser General Public -- License as published by the Free Software Foundation; either -- version 2.1 of the License, or (at your option) any later version. -- -- This library is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -- Lesser General Public License for more details. -- -- You should have received a copy of the GNU Lesser General Public -- License along with this library; if not, write to the Free Software -- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA -- {- | This module defines the API for HTF tests, i.e. unit tests and quickcheck properties. This functionality is mainly used internally in the code generated by the @hftpp@ pre-processor. -} module Test.Framework.TestInterface ( Assertion, TestResult(..), FullTestResult(..), HTFFailureException(..) , HtfStackEntry(..), HtfStack, emptyHtfStack, mkHtfStack, formatHtfStack , failureLocationFromStack, failureLocation , restCallStack, htfStackToList , failHTF, subAssertHTF, addCallerToSubAssertStack , mkFullTestResult ) where import Test.Framework.Location import Test.Framework.Colors import Control.Monad.Trans.Control import Data.Typeable import GHC.Stack import qualified Data.List as L import qualified Control.Exception as Exc import qualified Control.Exception.Lifted as ExcLifted {- | An assertion is just an 'IO' action. Internally, the body of any test in HTF is of type 'Assertion'. If a test specification of a certain plugin has a type different from 'Assertion', the plugin's preprocessor pass must inject wrapper code to convert the test specification into an assertion. Assertions may use 'failHTF' to signal a 'TestResult' different from 'Pass'. If the assertion finishes successfully, the tests passes implicitly. Please note: the assertion must not swallow any exceptions! Otherwise, timeouts and other things might not work as expected. -} type Assertion = IO () -- | The summary result of a test. data TestResult = Pass | Pending | Fail | Error deriving (Show, Read, Eq) data HtfStackEntry = HtfStackEntry { hse_location :: Location , hse_calledFunction :: String , hse_message :: Maybe String } deriving (Eq, Ord, Show, Read) -- The first entry in the list is the location of the assertion failure data HtfStack = HtfStack { hs_assertStack :: [HtfStackEntry] , hs_subAssertStack :: [HtfStackEntry] } deriving (Eq, Ord, Show, Read) mkHtfStack :: CallStack -> HtfStack mkHtfStack cs = HtfStack (map mkHtfStackEntry (removeHtfPrefix (getCallStack cs))) [] removeHtfPrefix :: [(String, SrcLoc)] -> [(String, SrcLoc)] removeHtfPrefix [] = [] removeHtfPrefix all@((_, srcLoc) : rest) = if "Test.Framework" `L.isPrefixOf` srcLocModule srcLoc then removeHtfPrefix rest else all mkHtfStackEntry :: (String, SrcLoc) -> HtfStackEntry mkHtfStackEntry x = mkHtfStackEntry' x Nothing mkHtfStackEntry' :: (String, SrcLoc) -> Maybe String -> HtfStackEntry mkHtfStackEntry' (funName, srcLoc) mMsg = HtfStackEntry { hse_location = makeLoc (srcLocFile srcLoc) (srcLocStartLine srcLoc) , hse_calledFunction = funName , hse_message = mMsg } htfStackToList :: HtfStack -> [HtfStackEntry] htfStackToList s = hs_assertStack s ++ reverse (hs_subAssertStack s) emptyHtfStack :: HtfStack emptyHtfStack = HtfStack [] [] failureLocation :: HasCallStack => Maybe Location failureLocation = failureLocationFromStack (mkHtfStack callStack) failureLocationFromStack :: HtfStack -> Maybe Location failureLocationFromStack stack = case htfStackToList stack of [] -> Nothing e:_ -> Just (hse_location e) restCallStack :: HtfStack -> [HtfStackEntry] restCallStack stack = case htfStackToList stack of [] -> [] _:rest -> rest -- | Formats a stack trace. formatHtfStack :: HtfStack -> String formatHtfStack stack = unlines $ map formatStackElem $ zip [0..] $ htfStackToList stack where formatStackElem (pos, HtfStackEntry loc _ mMsg) = let pref = if pos > 0 then " called from " else " at " in pref ++ showLoc loc ++ showMsg mMsg showMsg Nothing = "" showMsg (Just m) = " (" ++ m ++ ")" -- | The full result of a test, as used by HTF plugins. data FullTestResult = FullTestResult { ftr_stack :: HtfStack -- ^ The stack to the location of a possible failure , ftr_message :: Maybe ColorString -- ^ An error message , ftr_result :: Maybe TestResult -- ^ The outcome of the test, 'Nothing' means timeout } deriving (Eq, Show, Read) -- | Auxiliary function for contructing a 'FullTestResult'. mkFullTestResult :: TestResult -> Maybe String -> FullTestResult mkFullTestResult r msg = FullTestResult { ftr_stack = emptyHtfStack , ftr_message = fmap noColor msg , ftr_result = Just r } -- Internal exception type for propagating exceptions. data HTFFailureException = HTFFailure FullTestResult deriving (Show, Typeable) instance Exc.Exception HTFFailureException {- | Terminate a HTF test, usually to signal a failure. The result of the test is given in the 'FullTestResult' argument. -} failHTF :: MonadBaseControl IO m => FullTestResult -> m a -- Important: force the string argument, otherwise an error embedded -- lazily inside the string might escape. failHTF r = length (show r) `seq` ExcLifted.throwIO (HTFFailure r) addCallerToSubAssertStack :: CallStack -> HtfStack -> Maybe String -> HtfStack addCallerToSubAssertStack ghcStack stack@(HtfStack s1 s2) mMsg = case removeHtfPrefix (getCallStack ghcStack) of [] -> stack (entry : _) -> HtfStack s1 ((mkHtfStackEntry' entry mMsg) : s2) {- | Opens a new assertion stack frame to allow for sensible location information. This function should be used if the function being called does not carry a 'HasCallStack' annotation. -} subAssertHTF :: (HasCallStack, MonadBaseControl IO m) => Maybe String -> m a -> m a subAssertHTF mMsg action = let stack = callStack in action `ExcLifted.catch` (\(HTFFailure res) -> let newRes = res { ftr_stack = addCallerToSubAssertStack stack (ftr_stack res) mMsg } in failHTF newRes)