{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE UndecidableInstances #-}
module FreeC.Monad.Class.Testable
( MonadTestable
, shouldReturn
, shouldReturnWith
, shouldSucceed
, shouldSucceedWith
, shouldFail
, shouldFailPretty
, shouldFailWith
, shouldReturnProperty
) where
import Control.Monad.IO.Class ( MonadIO )
import Data.Functor.Identity ( Identity(..) )
import Data.IORef
( IORef, newIORef, readIORef, writeIORef )
import Data.List ( intercalate )
import System.IO.Error
( catchIOError, ioeGetErrorString, ioeGetFileName )
import System.IO.Unsafe ( unsafePerformIO )
import Test.HUnit.Base ( assertFailure )
import Test.Hspec
hiding ( shouldReturn )
import Test.QuickCheck
import FreeC.Environment
import FreeC.Environment.ModuleInterface
import FreeC.Environment.ModuleInterface.Decoder
import qualified FreeC.IR.Syntax as IR
import FreeC.Monad.Converter
import FreeC.Monad.Reporter
import FreeC.Pretty
class Monad m => MonadTestable m err | m -> err where
shouldReturnWith'
:: (a -> String)
-> m a
-> (a -> IO b)
-> IO b
shouldFailWith'
:: (a -> String)
-> m a
-> (err -> IO b)
-> IO b
shouldReturnWith
:: (Show a, MonadTestable m err) => m a -> (a -> Expectation) -> Expectation
shouldReturnWith = shouldReturnWith' show
shouldReturn :: (Eq a, Show a, MonadTestable m err) => m a -> a -> Expectation
shouldReturn mx y = shouldReturnWith mx (`shouldBe` y)
shouldSucceed :: (Show a, MonadTestable m err) => m a -> Expectation
shouldSucceed mx = shouldSucceedWith (return () <$ mx)
shouldSucceedWith :: (MonadTestable m err) => m Expectation -> Expectation
shouldSucceedWith = flip (shouldReturnWith' (const "<expectation>")) id
shouldFail :: (Show a, MonadTestable m err) => m a -> Expectation
shouldFail = flip shouldFailWith expectAnyError
shouldFailPretty :: (Pretty a, MonadTestable m err) => m a -> Expectation
shouldFailPretty mx = shouldFailWith' showPretty mx expectAnyError
shouldFailWith :: (Show a, MonadTestable m err)
=> m a
-> (err -> Expectation)
-> Expectation
shouldFailWith = shouldFailWith' show
expectAnyError :: err -> Expectation
expectAnyError = const (return ())
instance MonadTestable Identity () where
shouldReturnWith' _ = flip ($) . runIdentity
shouldFailWith' _ _ _
= assertFailure "Expected failure, but the Identity monad cannot fail."
instance MonadTestable Maybe () where
shouldReturnWith' _ (Just x) f = f x
shouldReturnWith' _ Nothing _
= assertFailure "Unexpected failure in Maybe monad."
shouldFailWith' _ Nothing f = f ()
shouldFailWith' showValue (Just x) _ = assertFailure
$ "Expected failure in Maybe monad, "
++ "but the following value was produced: "
++ showValue x
instance Show err => MonadTestable (Either err) err where
shouldReturnWith' _ (Right x) f = f x
shouldReturnWith' _ (Left err) _
= assertFailure $ "Unexpected failure in Either monad, got " ++ show err
shouldFailWith' _ (Left err) f = f err
shouldFailWith' showValue (Right x) _ = assertFailure
$ "Expected failure in Either monad, "
++ "but the following value was produced: "
++ showValue x
instance MonadTestable IO IOError where
shouldReturnWith' _ mx f = catchIOError (mx >>= f) $ \err -> assertFailure
$ "Unexpected IO error: "
++ ioeGetErrorString err
++ maybe "" (": " ++) (ioeGetFileName err)
shouldFailWith' showValue mx f = flip catchIOError f $ do
x <- mx
assertFailure
$ "Expected IO error, but the following value was produced: "
++ showValue x
showListItem :: String -> String
showListItem = (++ "\n") . (" * " ++) . intercalate "\n " . lines
showReporterValue :: (a -> String) -> (Maybe a, [Message]) -> String
showReporterValue showValue (mx, ms) = "Reporter result where:\n"
++ showReportedValue showValue mx
++ showReportedMessages ms
showReportedValue :: (a -> String) -> Maybe a -> String
showReportedValue _ Nothing = "No value was produced."
showReportedValue showValue (Just x)
= showListItem $ "The following value was produced: " ++ showValue x
showReportedMessages :: [Message] -> String
showReportedMessages [] = showListItem $ "No messages were reported."
showReportedMessages [m]
= showListItem $ "The following message was reported:\n" ++ showPretty m
showReportedMessages ms = showListItem
$ "The following "
++ show (length ms)
++ " messages were reported:\n"
++ showPretty ms
instance MonadTestable m err => MonadTestable (ReporterT m) [Message] where
shouldReturnWith' showValue reporter setExpectation = shouldReturnWith'
(showReporterValue showValue) (runReporterT reporter)
$ \result -> case result of
(Just x, _) -> setExpectation x
(Nothing, ms) -> assertFailure
$ "Unexpected fatal message.\n" ++ showReportedMessages ms
shouldFailWith' showValue reporter setExpectation = shouldReturnWith'
(showReporterValue showValue) (runReporterT reporter)
$ \result -> case result of
(Nothing, ms) -> setExpectation ms
(Just x, ms)
| null ms -> assertFailure
$ "Expected a fatal message, but no messages were reported.\n"
++ showReportedValue showValue (Just x)
| otherwise -> assertFailure
$ "Expected a fatal message to be reported, but got none.\n"
++ showReportedValue showValue (Just x)
++ showReportedMessages ms
initTestEnvironment :: IO Environment
initTestEnvironment = do
(maybeEnv, ms) <- runReporterT $ do
preludeIface <- loadTestModuleInterface "./base/Prelude.toml"
quickCheckIface <- loadTestModuleInterface "./base/Test/QuickCheck.toml"
return
$ foldr makeModuleAvailable emptyEnv
$ [preludeIface, quickCheckIface]
case maybeEnv of
Just env -> return env
Nothing -> assertFailure
$ "Could not initialize test environment.\n" ++ showReportedMessages ms
{-# NOINLINE moduleInterfaceCache #-}
moduleInterfaceCache :: IORef [(IR.ModName, ModuleInterface)]
moduleInterfaceCache = unsafePerformIO $ newIORef []
loadTestModuleInterface
:: (MonadIO r, MonadReporter r) => FilePath -> r ModuleInterface
loadTestModuleInterface ifaceFile = do
cache <- liftIO $ readIORef moduleInterfaceCache
case lookup ifaceFile cache of
Nothing -> do
iface <- loadModuleInterface ifaceFile
let cache' = (ifaceFile, iface) : cache
liftIO $ writeIORef moduleInterfaceCache cache'
return iface
Just iface -> return iface
instance MonadTestable m err => MonadTestable (ConverterT m) [Message] where
shouldReturnWith' showValue converter setExpectation = do
env <- initTestEnvironment
shouldReturnWith' showValue (evalConverterT converter env) setExpectation
shouldFailWith' showValue converter setExpectation = do
env <- initTestEnvironment
shouldFailWith' showValue (evalConverterT converter env) setExpectation
shouldReturnProperty
:: (MonadTestable m err, Testable prop) => m prop -> Property
shouldReturnProperty mp = idempotentIOProperty
$ shouldReturnWith' (const "<property>") mp return