tadka-2.0.0.0: test/props/Phase3.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Phase 3 properties: the Diagnostic class and the shared related/cycle walk.
module Phase3 (group) where
import Data.Text (Text)
import qualified Data.Text
import Numeric.Natural (Natural)
import Prettyprinter (pretty)
import Hedgehog
(Gen, Group (..), Property, assert, forAll, forAllWith, property,
withTests, (===))
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Tadka
(Diagnostic (..), DiagnosticId, SomeDiagnostic (..),
mkDiagnosticId)
import Tadka.Internal.Related
(RelatedTree (..), TerminationReason (..), defaultRelatedDepth,
flattenRelated, walkRelated)
group :: Group
group = Group "Phase 3 - diagnostic class & related walk"
[ ("default related depth is 8", prop_defaultDepth)
, ("cycle: shared id visited once, not descended", prop_cycleVisitedOnce)
, ("Nothing-only infinite chain terminates by depth", prop_nothingInfiniteTerminates)
, ("Nothing-only chains are depth-limited (v4 behavior)", prop_nothingDepthLimited)
, ("walk is total & depth-bounded for any fuelled tree", prop_walkTotal)
]
-- | A minimal test diagnostic: only identity and related children matter for
-- the walk. Everything else is defaulted.
data TestDiag = TestDiag
{ tdId :: Maybe DiagnosticId
, tdRelated :: [SomeDiagnostic]
}
instance Diagnostic TestDiag where
message _ = pretty ("test" :: Text)
diagnosticId = tdId
related = tdRelated
-- | Random finite tree with fresh (never shared) ids, bounded by fuel.
genFuelledDiag :: (Int -> Gen (Maybe DiagnosticId)) -> Int -> Gen SomeDiagnostic
genFuelledDiag genId fuel = do
mId <- genId fuel
kids <- if fuel <= 0
then pure []
else Gen.list (Range.linear 0 3) (genFuelledDiag genId (fuel `div` 2))
pure (SomeDiagnostic (TestDiag mId kids))
genNoId :: Int -> Gen (Maybe DiagnosticId)
genNoId _ = pure Nothing
genFreshId :: Int -> Gen (Maybe DiagnosticId)
genFreshId fuel =
Gen.choice
[ pure Nothing
, Just . mkDiagnosticId <$> Gen.text (Range.linear 1 4) Gen.alpha
-- salt with fuel so ids across levels rarely collide (fresh-ish)
, pure (Just (mkDiagnosticId (pretty' fuel)))
]
where
pretty' n = "n" <> tshow n
tshow = Data.Text.pack . show
treeDepth :: RelatedTree -> Int
treeDepth (RelatedTree _ [] _) = 1
treeDepth (RelatedTree _ kids _) = 1 + maximum (map treeDepth kids)
prop_defaultDepth :: Property
prop_defaultDepth = withTests 1 . property $ defaultRelatedDepth === (8 :: Natural)
-- Two nodes sharing ids in a cycle A -> B -> A: the second A is a CycleOmitted
-- leaf, reached once, never descended.
prop_cycleVisitedOnce :: Property
prop_cycleVisitedOnce = withTests 1 . property $ do
let idA = mkDiagnosticId "A"
idB = mkDiagnosticId "B"
a = TestDiag (Just idA) [SomeDiagnostic b]
b = TestDiag (Just idB) [SomeDiagnostic a]
nodes = flattenRelated (walkRelated 8 (SomeDiagnostic a))
-- exactly: A (root), B, A(cycle marker)
length nodes === 3
length (filter (\(_, t) -> t == CycleOmitted) nodes) === 1
-- A structurally infinite Nothing-only chain still terminates, bounded by depth.
prop_nothingInfiniteTerminates :: Property
prop_nothingInfiniteTerminates = withTests 1 . property $ do
let loop = TestDiag Nothing [SomeDiagnostic loop]
nodes = flattenRelated (walkRelated 5 (SomeDiagnostic loop))
length nodes === 6 -- root + 5 descents
assert (any (\(_, t) -> t == DepthTruncated) nodes)
assert (all (\(_, t) -> t /= CycleOmitted) nodes) -- no ids => no cycle path
-- v4 fallback: Nothing-only trees are depth-limited, never cycle-omitted.
prop_nothingDepthLimited :: Property
prop_nothingDepthLimited = property $ do
limit <- fromIntegral <$> forAll (Gen.int (Range.linear 0 6))
fuel <- forAll (Gen.int (Range.linear 0 24))
root <- forAllWith (const "<diag>") (genFuelledDiag genNoId fuel)
let tree = walkRelated limit root
nodes = flattenRelated tree
assert (length nodes >= 1)
assert (all (\(_, t) -> t /= CycleOmitted) nodes)
assert (treeDepth tree <= fromIntegral limit + 1) -- root level + `limit` descents
-- The walk is total (finite result) and depth-bounded for any fuelled tree,
-- with or without ids.
prop_walkTotal :: Property
prop_walkTotal = property $ do
limit <- fromIntegral <$> forAll (Gen.int (Range.linear 0 5))
fuel <- forAll (Gen.int (Range.linear 0 20))
root <- forAllWith (const "<diag>") (genFuelledDiag genFreshId fuel)
let tree = walkRelated limit root
nodes = flattenRelated tree
assert (length nodes >= 1) -- forces full evaluation
assert (treeDepth tree <= fromIntegral limit + 1)