packages feed

tadka-2.0.0.0: test/props/Phase12.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

-- | Phase 12 properties: multi-source 'Context' construction. The crux is the
-- same as Phase 2's, extended pointwise across sources: 'mkContextMultiDegrading'
-- never changes a group's label count or order, group order itself is never
-- changed, and every single-source function is exactly the one-group special
-- case of its multi-source counterpart -- never a second, divergent
-- implementation.
module Phase12 (group) where

import           Data.Either        (isLeft)
import           Data.List.NonEmpty (NonEmpty (..), (<|))
import qualified Data.List.NonEmpty as NE
import           Data.Text          (Text)
import           Prettyprinter      (Doc, pretty)

import           Hedgehog
                   (Gen, Group (..), Property, failure, forAll, property,
                    success, withTests, (===))
import qualified Hedgehog.Gen       as Gen
import qualified Hedgehog.Range     as Range

import           Tadka
import           Tadka.Internal     (buildContextMulti)

group :: Group
group = Group "Phase 12 - multi-source context"
  [ ("mkContextMulti is Left iff any span, in any group, is out of bounds",
      prop_multiStrict)
  , ("mkContextMultiDegrading never changes any group's label count/order",
      prop_multiDegradingCount)
  , ("mkContext/mkContextDegrading are the one-group case of their multi- counterparts",
      prop_singleGroupMatchesMulti)
  , ("buildContextMulti with every group empty = NoContext",
      prop_buildContextMultiAllEmpty)
  , ("buildContextMulti dispatches to mkContextMultiDegrading",
      prop_buildContextMultiDispatch)
  , ("buildContextMulti drops an empty group without affecting the others",
      prop_buildContextMultiDropsEmptyGroups)
  ]

-- === Generators (mirroring Phase2's, one source/label-list pair per group) ==

genSourceText :: Gen Text
genSourceText =
  Gen.text (Range.linear 0 40)
    (Gen.frequency [(6, Gen.alphaNum), (2, Gen.constant ' '), (2, Gen.constant '\n')])

genNamedSource :: Gen NamedSource
genNamedSource = do
  name <- Gen.text (Range.linear 1 8) Gen.alpha
  txt  <- genSourceText
  either (const Gen.discard) pure (mkNamedSource name txt)

genSpan :: Gen Span
genSpan = do
  o <- Gen.int (Range.linear 0 50)
  l <- Gen.int (Range.linear 0 50)
  either (const Gen.discard) pure (mkSpan o l)

genLabelText :: Gen (Maybe (Doc Ann))
genLabelText =
  Gen.choice [pure Nothing, Just . pretty <$> Gen.text (Range.linear 1 10) Gen.alpha]

genLabelKind :: Gen LabelKind
genLabelKind = Gen.element [Primary, Secondary]

genLabeledSpan :: Gen (Labeled Span)
genLabeledSpan = Labeled <$> genSpan <*> genLabelKind <*> genLabelText

genLabeledSpans :: Gen (NonEmpty (Labeled Span))
genLabeledSpans = do
  x  <- genLabeledSpan
  xs <- Gen.list (Range.linear 0 6) genLabeledSpan
  pure (x :| xs)

-- One group: a source, and the non-empty label list resolved against it.
genGroup :: Gen (NamedSource, NonEmpty (Labeled Span))
genGroup = (,) <$> genNamedSource <*> genLabeledSpans

-- One to four groups, in generation order (order matters -- these properties
-- check it's preserved).
genGroups :: Gen (NonEmpty (NamedSource, NonEmpty (Labeled Span)))
genGroups = do
  g  <- genGroup
  gs <- Gen.list (Range.linear 0 3) genGroup
  pure (g :| gs)

isStale :: LabelState -> Bool
isStale (LabelStale _) = True
isStale (LabelOk _)    = False

-- Entries shape 'buildContextMulti' takes, from a group's already-resolved-
-- shaped input -- the same (span, kind, text) triples 'buildContextWith'
-- takes per label, one source per group.
asEntries :: (NamedSource, NonEmpty (Labeled Span)) -> (NamedSource, [(Span, LabelKind, Maybe (Doc Ann))])
asEntries (src, lbls) = (src, map toEntry (NE.toList lbls))
  where toEntry (Labeled sp k txt) = (sp, k, txt)

-- === Properties ==============================================================

prop_multiStrict :: Property
prop_multiStrict = property $ do
  groups <- forAll genGroups
  let anyOOB = any groupHasOOB (NE.toList groups)
      groupHasOOB (src, lbls) =
        any (\(Labeled sp _ _) -> isLeft (resolveSpan src sp)) (NE.toList lbls)
  isLeft (mkContextMulti groups) === anyOOB

prop_multiDegradingCount :: Property
prop_multiDegradingCount = property $ do
  groups <- forAll genGroups
  let states   = contextLabelStates (mkContextMultiDegrading groups)
      expected = concatMap expectGroup (NE.toList groups)
      expectGroup (src, lbls) =
        [ isLeft (resolveSpan src sp) | Labeled sp _ _ <- NE.toList lbls ]
  -- 1. total count preserved across every group
  length states === sum (fmap (NE.length . snd) groups)
  -- 2. LabelStale in exactly the positions whose span fails to resolve,
  --    per group, in group order -- never dropped, never reordered, and
  --    never attributed to the wrong group's source.
  map isStale states === expected

prop_singleGroupMatchesMulti :: Property
prop_singleGroupMatchesMulti = property $ do
  src  <- forAll genNamedSource
  lbls <- forAll genLabeledSpans
  -- The total constructor: single-source result matches the one-group
  -- multi-source result, label for label.
  contextLabelStates (mkContextDegrading src lbls)
    === contextLabelStates (mkContextMultiDegrading ((src, lbls) :| []))
  -- The strict constructor: same success/failure shape, and the same
  -- resulting labels on success.
  case (mkContext src lbls, mkContextMulti ((src, lbls) :| [])) of
    (Left _,    Left _)    -> success
    (Right c1,  Right c2)  -> contextLabelStates c1 === contextLabelStates c2
    (Left _,    Right _)   -> failure
    (Right _,   Left _)    -> failure

prop_buildContextMultiAllEmpty :: Property
prop_buildContextMultiAllEmpty = withTests 20 . property $ do
  srcs <- forAll (Gen.list (Range.linear 1 4) genNamedSource)
  case NE.nonEmpty srcs of
    Nothing -> success   -- unreachable: Range.linear 1 4 never generates []
    Just ne -> case buildContextMulti (fmap (, []) ne) of
      NoContext -> success
      _         -> failure

prop_buildContextMultiDispatch :: Property
prop_buildContextMultiDispatch = property $ do
  groups <- forAll genGroups
  let built  = buildContextMulti (fmap asEntries groups)
      viaDeg = mkContextMultiDegrading groups
  contextLabelStates built === contextLabelStates viaDeg

prop_buildContextMultiDropsEmptyGroups :: Property
prop_buildContextMultiDropsEmptyGroups = property $ do
  groups   <- forAll genGroups
  emptySrc <- forAll genNamedSource
  let withEmpty = (emptySrc, []) <| fmap asEntries groups
  contextLabelStates (buildContextMulti withEmpty)
    === contextLabelStates (buildContextMulti (fmap asEntries groups))