{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types #-}
module EasyTest.Internal
(
tests
, scope
, skip
, example
, unitTest
, property
, propertyWith
, matches
, doesn'tMatch
, pending
, crash
, run
, runOnly
, rerun
, rerunOnly
, bracket
, bracket_
, finally
, cabalTestSuite
, Prism
, Prism'
, TestType(..)
, Test(..)
, Property
, PropertyT
, MonadTest
, (===)
, (/==)
, Seed
, Summary(..)
, PropertyConfig(..)
, defaultConfig
) where
import Control.Applicative (Const(..))
import qualified Control.Exception as Ex
import Control.Monad.Except
import Control.Monad.Trans.Maybe
import Control.Monad.Writer
import Data.List (intercalate)
import Data.List.Split (splitOn)
import Data.Monoid (First(..))
import Data.Profunctor.Choice
import Data.Profunctor.Unsafe
import Data.String (fromString)
import System.Exit
import Hedgehog hiding (Test, test, property)
import Hedgehog.Internal.Gen hiding (discard)
import Hedgehog.Internal.Property hiding (Test, property, test)
import Hedgehog.Internal.Report (Summary (..))
import Hedgehog.Internal.Seed (random)
import Hedgehog.Internal.Source (HasCallStack, withFrozenCallStack)
import qualified Hedgehog.Internal.Tree as HT
import EasyTest.Internal.Hedgehog
type Prism s t a b = forall p f. (Choice p, Applicative f) => p a (f b) -> p s (f t)
type Prism' s a = Prism s s a a
type Getting r s a = (a -> Const r a) -> s -> Const r s
data TestType = Unit | Prop PropertyConfig
data Test
= NamedTests ![(String, Test)]
| Sequence ![Test]
| Leaf !TestType !(PropertyT IO ())
| Skipped !Test
tests :: [Test] -> Test
tests = Sequence
scope :: String -> Test -> Test
scope msg tree =
let newScopes = splitSpecifier msg
in foldr (\scope' test -> NamedTests [(scope', test)]) tree newScopes
example :: HasCallStack => PropertyT IO () -> Test
example = Leaf Unit
unitTest :: HasCallStack => PropertyT IO () -> Test
unitTest = Leaf Unit
property :: HasCallStack => PropertyT IO () -> Test
property = Leaf (Prop defaultConfig)
propertyWith :: HasCallStack => PropertyConfig -> PropertyT IO () -> Test
propertyWith = Leaf . Prop
bracket :: IO a -> (a -> IO ()) -> (a -> PropertyT IO ()) -> PropertyT IO ()
bracket setup teardown test
= PropertyT $ TestT $ ExceptT $ WriterT $ GenT $ \size seed ->
HT.Tree $ MaybeT $ do
a <- setup
case test a of
PropertyT (TestT (ExceptT (WriterT (GenT innerTest)))) -> Ex.finally
(runMaybeT $ HT.runTree $ innerTest size seed)
(teardown a)
bracket_ :: IO a -> IO b -> PropertyT IO () -> PropertyT IO ()
bracket_ before after thing
= bracket before (\_ -> do { _ <- after; pure () }) (const thing)
finally :: PropertyT IO () -> IO a -> PropertyT IO ()
finally test after = bracket_ (pure ()) after test
splitSpecifier :: String -> [String]
splitSpecifier str = case splitOn "." str of
[""] -> []
lst -> lst
foldMapOf :: Getting r s a -> (a -> r) -> s -> r
foldMapOf l f = getConst #. l (Const #. f)
{-# INLINE foldMapOf #-}
preview :: Getting (First a) s a -> s -> Maybe a
preview l = getFirst #. foldMapOf l (First #. Just)
{-# INLINE preview #-}
matches :: HasCallStack => Prism' s a -> s -> PropertyT IO ()
matches p s = withFrozenCallStack $ case preview p s of
Just _ -> success
Nothing -> do { footnote "Prism failed to match"; failure }
doesn'tMatch :: HasCallStack => Prism' s a -> s -> PropertyT IO ()
doesn'tMatch p s = withFrozenCallStack $ case preview p s of
Nothing -> success
Just _ -> do { footnote "Prism matched"; failure }
mkGroup :: GroupName -> [([String], TestType, PropertyT IO ())] -> Group
mkGroup name props = Group name $ props <&> \(path, ty, test) ->
let name' = case path of
[] -> "(unnamed)"
_ -> fromString (intercalate "." path)
propConf = case ty of
Unit -> PropertyConfig 1 1 0 0
Prop conf -> conf
in (name', Property propConf test)
runTree :: Test -> [([String], TestType, PropertyT IO ())]
runTree = runTree' []
runTree' :: [String] -> Test -> [([String], TestType, PropertyT IO ())]
runTree' stack = \case
Leaf ty prop -> [(reverse stack, ty, prop)]
Sequence trees -> concatMap (runTree' stack) trees
NamedTests trees -> concatMap go trees
Skipped test -> skipTree' stack test
where go (name, tree) = runTree' (name:stack) tree
runTreeOnly :: [String] -> Test -> [([String], TestType, PropertyT IO ())]
runTreeOnly = runTreeOnly' [] where
runTreeOnly' stack [] tree = runTree' stack tree
runTreeOnly' stack (_:_) tree@Leaf{} = skipTree' stack tree
runTreeOnly' stack scopes (Sequence trees)
= concatMap (runTreeOnly' stack scopes) trees
runTreeOnly' stack _ (Skipped tree) = skipTree' stack tree
runTreeOnly' stack (scope':scopes) (NamedTests trees)
= concatMap go trees
where go (name, tree) =
if name == scope'
then runTreeOnly' (name:stack) scopes tree
else skipTree' (name:stack) tree
skipTree' :: [String] -> Test -> [([String], TestType, PropertyT IO ())]
skipTree' stack = \case
Leaf _ty _test -> [(reverse stack, Unit, discard)]
Sequence trees -> concatMap (skipTree' stack) trees
NamedTests trees -> concatMap go trees
Skipped test -> skipTree' stack test
where go (name, tree) = skipTree' (name:stack) tree
runOnly :: String -> Test -> IO Summary
runOnly prefix t = do
let props = runTreeOnly (splitSpecifier prefix) t
group = mkGroup (fromString $ "runOnly " ++ show prefix) props
seed <- random
recheckSeed seed group
rerunOnly :: String -> Seed -> Test -> IO Summary
rerunOnly prefix seed t = do
let props = runTreeOnly (splitSpecifier prefix) t
name = fromString $ "rerunOnly " ++ show prefix
recheckSeed seed $ mkGroup name props
run :: Test -> IO Summary
run t = do
seed <- random
recheckSeed seed $ mkGroup "run" $ runTree t
rerun :: Seed -> Test -> IO Summary
rerun = rerunOnly ""
skip :: Test -> Test
skip = Skipped
pending :: String -> PropertyT IO ()
pending msg = do { footnote msg; discard }
crash :: HasCallStack => String -> PropertyT IO ()
crash msg = withFrozenCallStack $ do { footnote msg; failure }
cabalTestSuite :: IO Summary -> IO ()
cabalTestSuite getSummary = do
summary <- getSummary
if summaryFailed summary > 0 then exitFailure else pure ()
(<&>) :: Functor f => f a -> (a -> b) -> f b
(<&>) = flip fmap