{-# LANGUAGE RankNTypes, ExistentialQuantification, DeriveDataTypeable, MultiParamTypeClasses, GeneralizedNewtypeDeriving #-} module Test.Tasty.Silver.Internal where import Control.Applicative import Control.Monad.Cont import Control.Exception import Data.Typeable (Typeable) import Data.ByteString.Lazy as LB import System.IO import System.IO.Error import Test.Tasty.Providers import qualified Data.Text as T import Data.Maybe -- | See 'goldenTest1' for explanation of the fields data Golden = forall a . Golden (forall r . ValueGetter r (Maybe a)) -- Get golden value. (forall r . ValueGetter r a) -- Get actual value. (a -> a -> IO GDiff) -- Compare/diff. (a -> IO GShow) -- How to produce a show. (a -> IO ()) -- Update golden value. deriving Typeable -- | An action that yields a value (either golden or tested). -- -- CPS allows closing the file handle when using lazy IO to read data. newtype ValueGetter r a = ValueGetter { runValueGetter :: ContT r IO a } deriving (Functor, Applicative, Monad, MonadCont, MonadIO) -- | Lazily read a file. The file handle will be closed after the -- 'ValueGetter' action is run. vgReadFile :: FilePath -> ValueGetter r ByteString vgReadFile path = fromJust <$> vgReadFile1 predFalse path where predFalse :: IOException -> Bool predFalse _ = False -- | Lazily read a file. The file handle will be closed after the -- 'ValueGetter' action is run. -- Will return 'Nothing' if the file does not exist. vgReadFileMaybe :: FilePath -> ValueGetter r (Maybe ByteString) vgReadFileMaybe = vgReadFile1 (isDoesNotExistErrorType . ioeGetErrorType) -- | Reads a file, and optionally catches some exceptions. If -- an exception is catched, Nothing is returned. vgReadFile1 :: Exception e => (e -> Bool) -- ^ Which exceptions to catch. -> FilePath -> ValueGetter r (Maybe ByteString) vgReadFile1 doCatch path = do r <- ValueGetter $ ContT $ \k -> catchJust (\e -> if doCatch e then Just () else Nothing) (bracket (openBinaryFile path ReadMode) hClose (\h -> LB.hGetContents h >>= (k . Just)) ) (const $ k Nothing) return $! r -- | Ensures that the result is fully evaluated (so that lazy file handles -- can be closed) vgRun :: ValueGetter r r -> IO r vgRun (ValueGetter a) = runContT a evaluate -- | The comparison/diff result. data GDiff = Equal -- ^ Values are equal. | DiffText { gActual :: T.Text, gExpected :: T.Text } -- ^ The two values are different, show a diff between the two given texts. | ShowDiffed { gDiff :: T.Text } -- ^ The two values are different, just show the given text to the user. -- | How to show a value to the user. data GShow = ShowText T.Text -- ^ Show the given text. instance IsTest Golden where run _ golden _ = runGolden golden testOptions = return [] runGolden :: Golden -> IO Result runGolden (Golden getGolden getActual cmp _ _) = do vgRun $ do new <- getActual ref' <- getGolden case ref' of Nothing -> return $ testFailed "Missing golden value." Just ref -> do -- Output could be arbitrarily big, so don't even try to say what wen't wrong. cmp' <- liftIO $ cmp ref new case cmp' of Equal -> return $ testPassed "" _ -> return $ testFailed "Result did not match expected output. Use interactive mode to see the full output."