hedgehog-utils-0.1.0.0: test/hspec/Hedgehog/Utils/MetaTestingSpec.hs
module Hedgehog.Utils.MetaTestingSpec
( spec
) where
import Hedgehog.Utils.MetaTesting qualified as UUT
import TestsPrelude hiding (withFrozenCallStack)
import Hedgehog.Utils.MetaTesting (Outcome)
import Hedgehog.Utils qualified as Utils
import Hedgehog.Gen qualified as Gen
import Hedgehog.Internal.Report qualified as Report
import Hedgehog.Internal.Show qualified as Shw
import Hedgehog.Internal.Source qualified as Src
import Data.List qualified as L
-- --- preamble, documentation ---
{-| Enable tests that will deliberately fail.
Useful for meta-testing the tests, and to be able to inspect failure reports.
Always checked in as 'False'.
-}
enableFailingTests :: Bool
enableFailingTests = False
{-| Enable tests on the source locations in failure reports.
Not overly refactor- or future-proof, but very helpful to at least have at hand.
=== Debugging hints
Likely causes to source locations...
...missing:
-> missing HasCallStack
...wrong/pointing into MetaTesting.hs:
-> missing withFrozenCallStack
-}
enableCheckSourceLocationFilename :: Bool
enableCheckSourceLocationFilename = True
-- | Name of this filename; used in source location tests
fname_self :: FilePath
fname_self = "MetaTestingSpec.hs"
-- | Don't import/use 'GHC.Stack.withFrozenCallStack' in this file.
--
-- For tests, the withFrozenCallStack highest in the call chain should be one _in the code under test_, so proper callstack freezing in code under test is observable in testing.
withFrozenCallStack :: don't use me
withFrozenCallStack =
error ""
-- | If this is not an ambiguous definition error, withFrozenCallStack is not imported (good).
_shouldn't_be_ambiguous :: a
_shouldn't_be_ambiguous = undefined withFrozenCallStack
-- --- specs ---
spec :: Spec
spec = do
describe "smoke tests" spec_smoke
describe "main" spec_main
describe "source locations" spec_sourceLocations
describe "main, provoke failing" spec_mainFailing
describe "inner bottoms" spec_innerBottoms
describe "genSeed" spec_genSeed
describe "randomOutcomeProp" spec_randomOutcomeProp
describe "seed integrity (nested props)" spec_seedIntegrity
-- --- example properties ---
bottomIO :: (MonadTest m, MonadIO m) => m a
bottomIO = do
footnote "inner is bottomIO"
evalIO (error "error: bottomIO")
bottom :: a
bottom = error "I am a bottom"
data SpecTestException = SpecTestException deriving Show
instance Exception SpecTestException
throwin :: (MonadTest m, MonadIO m) => m a
throwin = do
footnote "inner is throwin"
evalIO (throwIO SpecTestException)
-- --- smoke tests ---
spec_smoke :: Spec
spec_smoke = unitTests $ do
it "assertPassing " $ UUT.assertPassing success
it "assertFailing " $ UUT.assertFailing failure
it "evalCheck: pass " $ (UUT.evalCheck success) >>= (===Report.OK)
it "evalCheck: fail " $ (UUT.evalCheck failure) >>= (assert . UUT.isFailed)
it "evalCheck: throw" $ (UUT.evalCheck throwin) >>= (assert . UUT.isFailed)
-- --- main spec ---
type WithInner =
( Prop -> Prop
, Prop -> Prop
, Prop -> Prop
)
type WithInnerP =
( Property -> Prop
, Property -> Prop
, Property -> Prop
)
spec_main :: Spec
spec_main = unitTests $ do
mkSpec "evalCheck " evalChecks
mkSpec "evalCheck + outcomeOf" outcomeOf
mkSpec "UUT.assert* " uut_asserts
mkSpecP "checkSilently " checkSilentlies
where
evalChecks =
( \p -> do res <- UUT.evalCheck p; res===Report.OK
, \p -> do res <- UUT.evalCheck p; assert (UUT.isFailed res)
, \p -> do res <- UUT.evalCheck p; res===Report.GaveUp
)
outcomeOf =
( \p -> do res <- UUT.evalCheck p; UUT.outcomeOf res===UUT.Passing
, \p -> do res <- UUT.evalCheck p; UUT.outcomeOf res===UUT.Failing
, \p -> do res <- UUT.evalCheck p; UUT.outcomeOf res===UUT.GivinUp
)
checkSilentlies =
( \p -> do res <- UUT.checkSilently p; assert res
, \p -> do res <- UUT.checkSilently p; assert (not res)
, \p -> do res <- UUT.checkSilently p; assert (not res)
)
uut_asserts =
( UUT.assertPassing
, UUT.assertFailing
, UUT.assertGivingUp
)
mkSpec :: String -> WithInner -> Spec
mkSpec s (asrtSucc,asrtFail,asrtGU) =
describe s $ do
it "inner succeeds " $ asrtSucc success
it "inner fails " $ asrtFail failure
it "inner IO-throws" $ asrtFail throwin
it "inner gives up " $ asrtGU discard
mkSpecP :: String -> WithInnerP -> Spec
mkSpecP s = mkSpec s . map3 (. property)
-- --- source locations ---
spec_sourceLocations :: Spec
spec_sourceLocations = unitTests $ do
let
fReportOf = \case
Report.Failed fReport -> pure fReport
Report.OK -> annotate "did not fail" >> failure
Report.GaveUp -> annotate "did not fail" >> failure
checkFailureLocation :: MonadTest m => Report.FailureReport -> m ()
checkFailureLocation rep = do
annotate (Shw.showPretty rep)
case Report.failureLocation rep of
Nothing -> annotate "source location is missing" >> failure
Just span' ->
when enableCheckSourceLocationFilename $
let (Src.Span fpath _ _ _ _) = span' in
diff fname_self L.isSuffixOf fpath
describe "assert*" $ do
let
checkIsFromAssertion =
diff "Assertion failed:" L.isInfixOf . Report.failureMessage
it "assertFailing" $ do
result <- UUT.evalCheck (UUT.assertFailing success)
fReport <- fReportOf result
checkIsFromAssertion fReport
checkFailureLocation fReport
it "assertPassing" $ do
result <- UUT.evalCheck (UUT.assertPassing failure)
fReport <- fReportOf result
checkIsFromAssertion fReport
checkFailureLocation fReport
describe "evalCheck*" $ do
it "evalCheck" $ do
result <- UUT.evalCheck failure
fReport <- fReportOf result
checkFailureLocation fReport
it "evalCheckWith" $ do
let
uut_evalCheckWith =
UUT.evalCheckWith
UUT.defaultMetaConfig
UUT.defaultSize0
UUT.defaultSeed
rResult <- uut_evalCheckWith failure
fReport <- fReportOf (Report.reportStatus rResult)
checkFailureLocation fReport
-- --- main - failing ---
spec_mainFailing :: Spec
spec_mainFailing =
unitTests
$ whenEnableFailing "(inner is,assert that)..."
$ do
describe "UUT.assert*, non-bottom inners" $ do
it "succ ,Fail " $ UUT.assertFailing success
xt "succ ,GaveUp" $ UUT.assertGivingUp success
xt "fail ,Succ " $ UUT.assertPassing failure
xt "fail ,GaveUp" $ UUT.assertGivingUp failure
xt "g.up ,Succ " $ UUT.assertPassing discard
xt "g.up ,Fail " $ UUT.assertFailing discard
xt "pThrw,GaveUp" $ UUT.assertGivingUp throwin
xt "pThrw,succ " $ UUT.assertPassing throwin
-- (not necessarily exhaustive)
describe "evalCheck" $ do
xt "fail ,OK " $ UUT.evalCheck failure >>= (=== Report.OK)
xt "g.up ,OK " $ UUT.evalCheck discard >>= (=== Report.OK)
xt "IOerr,GaveUp" $ UUT.evalCheck bottomIO >>= (=== Report.GaveUp)
xt "succ ,Failed" $ UUT.evalCheck success >>= (assert . UUT.isFailed)
-- --- inner bottoms ---
-- documentation of the current behaviour; possibly, other behaviours could be defensible as well
spec_innerBottoms :: Spec
spec_innerBottoms = unitTests $ do
describe "(inner-is,assert-that):" $ do
it "bottom ,fail" $ UUT.assertFailing bottom
it "bottomIO,fail" $ UUT.assertFailing bottomIO
whenEnableFailing "(inner is,assert that)..." $ do
xt "bottom ,succ" $ UUT.assertPassing bottom
xt "bottomIO,succ" $ UUT.assertPassing bottomIO
xt "bottom ,g.up" $ UUT.assertGivingUp bottom
xt "bottomIO,g.up" $ UUT.assertGivingUp bottomIO
-- --- genSeed ---
spec_genSeed :: Spec
spec_genSeed =
modifyMaxSuccess (const 1000) $
it "coverage" $ do
s <- forAll genSeed
cover 1 "defaultSeed" (s==UUT.defaultSeed)
-- --- randomOutcomeProp ---
-- | convenience helper
chk :: MonadIO m => Seed -> Property -> m Outcome
chk seed pr =
UUT.outcomeOf' <$> UUT.evalCheckPropertyWith UUT.defaultSize0 seed pr
spec_randomOutcomeProp :: Spec
spec_randomOutcomeProp = do
let
f :: MonadIO m => Seed -> m Outcome
f s = chk s UUT.randomOutcomeProp
modifyMaxSuccess (const 1000) $ do
it "coverage (outcome)" $ do
s <- forAll genSeed
x <- f s
cover 15 "Failing" (x==UUT.Failing)
cover 15 "GivinUp" (x==UUT.GivinUp)
cover 15 "Passing" (x==UUT.Passing)
( modifyMaxSuccess (const 100) .
modifyMaxDiscardRatio (const 1) ) $
describe "property is" $ do
it "deterministic" $ do
s <- forAll genSeed
Utils.deterministic (f s)
it "deterministic (interleaved)" $ do
(s0,s1) <- forAll genSeeds
Utils.deterministicF f (s0,s1)
it "seed-sensitive" $ do
(s0,s1) <- forAll genSeeds
Utils.notConstF_M f (s0,s1)
-- --- seed integrity ---
-- to be able to vary an outer and an inner seed in a controlled way, use a meta-property nested inside another meta-property
-- use tagged phantom types to enforce correct pairing of seed<->outcome for each of the inner and outer properties
data I -- (I)nner
data O -- (O)uter
type Outcome' :: Type -> Type
type Seed' :: Type -> Type
newtype Outcome' b = Outcome' Outcome deriving (Eq,Show,Enum,Bounded)
newtype Seed' b = Seed' Seed deriving (Eq,Show)
-- | tagged version of 'chk'; using this to run properties enforces that the same tag is used for the seed and the outcome
chk' :: ∀ b m. MonadIO m
=> Seed' b
-> Property
-> m (Outcome' b)
chk' (Seed' seed) pr = Outcome' <$> chk seed pr
-- | two nested property tests
nested_ :: MonadIO m
=> Property -- ^ inner property to run
-> (Outcome' I -> Prop) -- ^ assertion for the outcome of the inner property
-> Seed' I
-> Seed' O
-> m (Outcome' O)
nested_ innerProp asrt i o = chk' o outerProp
where
outerProp = property $ do
r <- chk' i innerProp
asrt r
spec_seedIntegrity :: Spec
spec_seedIntegrity =
let
f :: MonadIO m
=> (Outcome' I -> Prop)
-> Seed' I
-> Seed' O
-> m (Outcome' O)
f = nested_ UUT.randomOutcomeProp
gen_i = Seed' @I <$> genSeed
gen_o = Seed' @O <$> genSeed
gen_asrt = Gen.enumBounded :: Gen (Outcome' I)
gen =
(,,,,)
<$> gen_asrt
<*> gen_i
<*> gen_i
<*> gen_o
<*> gen_o
in
modifyMaxSuccess (const 50) $ do
describe "pre-conditions" $ do
modifyMaxSuccess (const 200) $
it "coverage (outer outcome)" $ do
(asrt,i,_,o,_) <- forAll gen
res <- f (===asrt) i o
cover 20 "Passing" (res == Outcome' UUT.Passing)
cover 40 "Failing" (res == Outcome' UUT.Failing)
it "outer not giving up" $ do
(asrt,i,_,o,_) <- forAll gen
res <- f (===asrt) i o
res /== Outcome' UUT.GivinUp
describe "outer outcome is" $ do
it "deterministic" $ do
(asrt,i,_,o,_) <- forAll gen
Utils.deterministic $ f (===asrt) i o
it "deterministic (interleaved)" $ do
(asrt,o0,o1,i0,i1) <- forAll gen
let f' = uncurry (f (===asrt))
Utils.deterministicF f' ((o0,i0),(o1,i1))
let
notConstF_M = Utils.notConstF_M
constF_M = Utils.constF_M
notConst (asrt,i0,i1,o ,_ ) = notConstF_M (\i -> f (===asrt) i o) (i0,i1)
isConst (asrt,i ,_ ,o0,o1) = constF_M (\o -> f (===asrt) i o) (o0,o1)
it "provedly non-constant w.r.t. inner seed" $ forAll gen >>= notConst
it "seemingly constant w.r.t. outer seed" $ forAll gen >>= isConst
-- --- general helpers ---
whenEnableFailing :: HasCallStack => String -> Spec -> Spec
xwhenEnableFailing :: HasCallStack => String -> Spec -> Spec
whenEnableFailing t = when enableFailingTests . describe (failDesc t)
xwhenEnableFailing t = when enableFailingTests . xdescribe (failDesc t)
failDesc :: String -> String
failDesc = \case
"" -> "SHOULD FAIL"
t -> "SHOULD FAIL: " ++ t
genSeed :: Gen Seed
genSeed = UUT.genSeed
genSeeds :: Gen (Seed,Seed)
genSeeds = (,) <$> UUT.genSeed <*> UUT.genSeed
map3 :: (a -> b) -> (a,a,a) -> (b,b,b)
map3 f (x,y,z) = (f x,f y,f z)
_silence_unusedWarnings :: _
_silence_unusedWarnings = undefined xwhenEnableFailing