{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Rank2Types #-} -- | -- Module : EasyTest.Internal -- Copyright : (c) Joel Burget, 2018-2019 -- License : MIT -- Maintainer : joelburget@gmail.com -- Stability : experimental -- -- This module defines the core internals and interface of easytest. module EasyTest.Internal ( -- * Structuring tests tests , scope , skip , example , unitTest , property , propertyWith -- * Assertions for unit tests , matches , doesn'tMatch , pending , crash -- * Running tests , run , runOnly , rerun , rerunOnly -- * Bracketed tests (requiring setup / teardown) , bracket , bracket_ , finally -- * Cabal test suite , cabalTestSuite -- * Internal , Prism , Prism' , TestType(..) , Test(..) -- * Hedgehog re-exports , 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 -- | A prism embodies one constructor of a sum type (as a lens embodies one -- part of a product type). See 'EasyTest.Prism._Just', 'EasyTest.Prism._Nothing', 'EasyTest.Prism._Left', and 'EasyTest.Prism._Right' for examples. See for more explanation. type Prism s t a b = forall p f. (Choice p, Applicative f) => p a (f b) -> p s (f t) -- | A type-restricted prism. See for more explanation. type Prism' s a = Prism s s a a type Getting r s a = (a -> Const r a) -> s -> Const r s -- | Unit- or property- test. data TestType = Unit | Prop PropertyConfig -- | A set of unit- and property-tests data Test = NamedTests ![(String, Test)] -- ^ A set of named (scoped) tests | Sequence ![Test] -- ^ A sequence of tests | Leaf !TestType !(PropertyT IO ()) -- ^ An atomic unit- or property-test | Skipped !Test -- ^ A set of tests marked to skip -- | Run a list of tests tests :: [Test] -> Test tests = Sequence -- | Label a test. Can be nested. A "." is placed between nested -- scopes, so @scope "foo" . scope "bar"@ is equivalent to @scope "foo.bar"@ scope :: String -> Test -> Test scope msg tree = let newScopes = splitSpecifier msg in foldr (\scope' test -> NamedTests [(scope', test)]) tree newScopes -- | Run a unit test (same as 'unitTest'). Example: -- -- >>> run $ example $ 1 === 2 -- > ━━━ run ━━━ -- > ✗ (unnamed) failed after 1 test. -- > -- > ┏━━ tests/Suite.hs ━━━ -- > 26 ┃ main :: IO () -- > 27 ┃ main = do -- > 28 ┃ run $ example $ 1 === (2 :: Int) -- > ┃ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- > ┃ │ Failed (- lhs =/= + rhs) -- > ┃ │ - 1 -- > ┃ │ + 2 -- > -- > This failure can be reproduced by running: -- > > recheck (Size 0) (Seed 2914818620245020776 12314041441884757111) (unnamed) -- > -- > ✗ 1 failed. example :: HasCallStack => PropertyT IO () -> Test example = Leaf Unit -- | Run a unit test (same as 'example'). Example: -- -- >>> run $ unitTest $ 1 === 2 -- > ━━━ run ━━━ -- > ✗ (unnamed) failed after 1 test. -- > -- > ┏━━ tests/Suite.hs ━━━ -- > 26 ┃ main :: IO () -- > 27 ┃ main = do -- > 28 ┃ run $ unitTest $ 1 === (2 :: Int) -- > ┃ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- > ┃ │ Failed (- lhs =/= + rhs) -- > ┃ │ - 1 -- > ┃ │ + 2 -- > -- > This failure can be reproduced by running: -- > > recheck (Size 0) (Seed 2914818620245020776 12314041441884757111) (unnamed) -- > -- > ✗ 1 failed. unitTest :: HasCallStack => PropertyT IO () -> Test unitTest = Leaf Unit -- | Run a property test. Example: -- -- >>> run $ scope "list reversal" $ property $ do -- >.. list <- forAll $ Gen.list @_ @Int (Range.linear 0 100) -- >.. (Gen.element [0..100]) -- >.. reverse (reverse list) === list -- > ━━━ run ━━━ -- > ✓ list reversal passed 100 tests. -- > ✓ 1 succeeded. property :: HasCallStack => PropertyT IO () -> Test property = Leaf (Prop defaultConfig) -- | Run a property test with a custom configuration. This allows you to configure the 'propertyTestLimit', 'propertyDiscardLimit', 'propertyShrinkLimit', or 'propertyShrinkRetries'. Example: -- -- >>> run $ scope "list reversal" $ propertyWith (defaultConfig { propertyTestLimit = 500 }) $ do -- >.. list <- forAll $ Gen.list @_ @Int (Range.linear 0 100) -- >.. (Gen.element [0..100]) -- >.. reverse (reverse list) === list -- > ━━━ run ━━━ -- > ✓ list reversal passed 500 tests. -- > ✓ 1 succeeded. propertyWith :: HasCallStack => PropertyConfig -> PropertyT IO () -> Test propertyWith = Leaf . Prop -- | Make a test with setup and teardown steps. 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) -- | A variant of 'bracket' where the return value from the setup step is not -- required. bracket_ :: IO a -> IO b -> PropertyT IO () -> PropertyT IO () bracket_ before after thing = bracket before (\_ -> do { _ <- after; pure () }) (const thing) -- | A specialised variant of 'bracket' with just a teardown step. finally :: PropertyT IO () -> IO a -> PropertyT IO () finally test after = bracket_ (pure ()) after test -- | Split a test specifier into parts 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 #-} -- | Test whether a 'Prism' matches. Example: -- -- >>> main -- > ━━━ run ━━━ -- > ✓ (unnamed) passed 1 test. -- > ✗ (unnamed) failed after 1 test. -- > -- > ┏━━ tests/Suite.hs ━━━ -- > 48 ┃ main :: IO () -- > 49 ┃ main = do -- > 50 ┃ _ <- run $ tests -- > 51 ┃ [ example $ matches _Left (Left 1 :: Either Int ()) -- > 52 ┃ , example $ matches _Left (Right () :: Either Int ()) -- > ┃ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- > 53 ┃ ] -- > 54 ┃ pure () -- > -- > Prism failed to match -- > -- > This failure can be reproduced by running: -- > > recheck (Size 0) (Seed 14003809197113786240 2614482618840800713) (unnamed) -- > -- > ✗ 1 failed, 1 succeeded. -- -- Use with 'EasyTest.Prism._Just', 'EasyTest.Prism._Nothing', 'EasyTest.Prism._Left', 'EasyTest.Prism._Right', or 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 } -- | Test whether a 'Prism' doesn't match. Compare with 'matches'. 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 } -- | Make a 'Hedgehog.Group' from a list of tests. 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) -- | Flatten a test tree. Use with 'mkGroup' 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 -- | Flatten a subtree of tests. Use with 'mkGroup' runTreeOnly :: [String] -> Test -> [([String], TestType, PropertyT IO ())] runTreeOnly = runTreeOnly' [] where -- Note: In this first case, we override a skip if this test is specifically -- run 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 -- | Skip this test tree (mark all properties as skipped). skipTree' :: [String] -> Test -> [([String], TestType, PropertyT IO ())] skipTree' stack = \case -- As a minor hack, we set any skipped test to type 'Unit' so it'll only run -- once. 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 -- | Run all tests whose scope starts with the given prefix. -- -- >>> runOnly "components.a" tests 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 -- | Rerun all tests with the given seed and whose scope starts with the given -- prefix -- -- >>> rerunOnly "components.a" (Seed 2914818620245020776 12314041441884757111) tests 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 all tests run :: Test -> IO Summary run t = do seed <- random recheckSeed seed $ mkGroup "run" $ runTree t -- | Rerun all tests with the given seed -- -- >>> rerun (Seed 2914818620245020776 12314041441884757111) tests rerun :: Seed -> Test -> IO Summary rerun = rerunOnly "" -- | Explicitly skip this set of tests. skip :: Test -> Test skip = Skipped -- | Mark a test as pending. pending :: String -> PropertyT IO () pending msg = do { footnote msg; discard } -- | Record a failure with a given message crash :: HasCallStack => String -> PropertyT IO () crash msg = withFrozenCallStack $ do { footnote msg; failure } -- | Make this a cabal test suite for use with @exitcode-stdio-1.0@ -- @test-suite@s. -- -- This simply checks to see if any tests failed and if so exits with -- 'exitFailure'. 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