packages feed

tadka-2.0.0.0: test/props/Cause.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}

-- | Cause chain (post-v1 hardening): the "caused by" chain is depth- and
-- cycle-safe exactly like @related@. A cause that loops back by 'diagnosticId'
-- is cut (its marker renders at most once); an id-less loop is bounded by the
-- depth budget. Generated cause chains are additionally covered by the
-- render-totality property in the Phase 11 group (genGD now emits causes).
module Cause (group) where

import           Control.Exception          (SomeException, evaluate, try)
import           Control.Monad.IO.Class      (liftIO)
import qualified Data.Aeson                 as A
import           Data.Text                  (Text)
import qualified Data.Text                  as T
import qualified Data.Text.Lazy             as TL
import qualified Data.Text.Lazy.Encoding    as TLE
import           Prettyprinter              (LayoutOptions (..), PageWidth (Unbounded),
                                             layoutPretty)
import           Prettyprinter.Render.Text  (renderStrict)

import           Hedgehog
import qualified Hedgehog.Gen               as Gen

import           GenDiag                    (GD (..), selfCauseJust, selfCauseNothing)
import           Tadka
import           Tadka.Internal             (buildContext)

renderT :: Diagnostic e => Target -> e -> Text
renderT tgt e =
  case selectRenderer (withColorMode ColorNever (withUnicodeMode UnicodeAlways (withTarget tgt defaultConfig))) of
    SomeRenderer r@(Graphical _)  -> renderStrict (layoutPretty (LayoutOptions Unbounded) (render r e))
    SomeRenderer r@(Narratable _) -> render r e
    SomeRenderer r@(Json _)       -> TL.toStrict (TLE.decodeUtf8 (A.encode (render r e)))

prop_cyclicTerminates :: Property
prop_cyclicTerminates = property $ do
  tgt <- forAll (Gen.element [TGraphical, TNarratable, TJson])
  d   <- forAllWith (const "<self-causing>") (Gen.element [selfCauseJust, selfCauseNothing])
  res <- liftIO (try (evaluate (T.length (renderT tgt d))) :: IO (Either SomeException Int))
  case res of
    Right _ -> success
    Left e  -> annotate (show e) >> failure

marker :: Text
marker = "ZZCAUSEZZ"

-- A node whose cause is itself, sharing one diagnosticId: cycle detection cuts
-- the chain, so the marker (in its message) renders once.
idCyclic :: GD
idCyclic =
  let d = GD ("boom " <> marker) Nothing NoContext Nothing Nothing []
             (Just (mkDiagnosticId "self")) (Just (SomeDiagnostic d))
  in d

prop_markerAtMostOnce :: Property
prop_markerAtMostOnce = withTests 1 . property $
  mapM_ (\tgt -> assert (T.count marker (renderT tgt idCyclic) <= 1))
        [TGraphical, TNarratable, TJson]

-- Root with a distinct-id cause: a "caused by" line must appear.
leaf :: GD
leaf = GD "root cause here" Nothing NoContext Nothing Nothing [] (Just (mkDiagnosticId "leaf")) Nothing

rooted :: GD
rooted = GD "top failure" Nothing NoContext Nothing Nothing [] (Just (mkDiagnosticId "root"))
            (Just (SomeDiagnostic leaf))

prop_causedByAppears :: Property
prop_causedByAppears = withTests 1 . property $ do
  assert ("caused by" `T.isInfixOf` renderT TGraphical rooted)
  assert ("Caused by" `T.isInfixOf` renderT TNarratable rooted)

-- A *related* diagnostic with its own, distinct cause: the cause must render
-- wherever the related diagnostic itself renders, not only at the root. Each
-- renderer's related-child path is expected to call the exact same
-- cause-rendering function the root does (Graphical: 'causeLinesFor';
-- Narratable: 'causeSentences'; Json: 'toDTO's own recursive 'dtoCauses'),
-- so this exercises that shared path one level down instead of at the root.
relatedLeafCause :: GD
relatedLeafCause = GD ("relative's own root cause " <> marker) Nothing NoContext Nothing Nothing []
                      (Just (mkDiagnosticId "relatedLeafCause")) Nothing

relatedWithOwnCause :: GD
relatedWithOwnCause = GD "a related diagnostic" Nothing NoContext Nothing Nothing []
                         (Just (mkDiagnosticId "relatedWithOwnCause")) (Just (SomeDiagnostic relatedLeafCause))

rootWithRelatedCause :: GD
rootWithRelatedCause = GD "top-level failure" Nothing NoContext Nothing Nothing
                           [SomeDiagnostic relatedWithOwnCause]
                           (Just (mkDiagnosticId "rootWithRelatedCause")) Nothing

prop_causedByAppearsForRelated :: Property
prop_causedByAppearsForRelated = withTests 1 . property $
  mapM_ (\tgt -> assert (marker `T.isInfixOf` renderT tgt rootWithRelatedCause))
        [TGraphical, TNarratable, TJson]

-- === specCause: derive == manual ==========================================

rightOrErr :: Show a => Either a b -> b
rightOrErr = either (error . show) id

srcC :: NamedSource
srcC = rightOrErr (mkNamedSource "f.hs" "let a = bb")

data DErr3 = DErr3 { d3src :: NamedSource, d3prim :: Span, d3cause :: Maybe SomeDiagnostic }

deriveDiagnostic defaultSpec
  { specSourceField = Just 'd3src, specLabelFields = [('d3prim, "here")]
  , specCause = Just 'd3cause, specMessage = Just [| \_ -> "boom" |] }
  ''DErr3

data MErr3 = MErr3 { m3src :: NamedSource, m3prim :: Span, m3cause :: Maybe SomeDiagnostic }

instance Diagnostic MErr3 where
  message _ = "boom"
  context e = buildContext (m3src e) [ (m3prim e, Just "here") ]
  diagnosticCause e = m3cause e

dCauseVal, dNoCauseVal :: DErr3
dCauseVal   = DErr3 srcC (rightOrErr (mkSpan 4 1)) (Just (SomeDiagnostic leaf))
dNoCauseVal = DErr3 srcC (rightOrErr (mkSpan 4 1)) Nothing

mCauseVal, mNoCauseVal :: MErr3
mCauseVal   = MErr3 srcC (rightOrErr (mkSpan 4 1)) (Just (SomeDiagnostic leaf))
mNoCauseVal = MErr3 srcC (rightOrErr (mkSpan 4 1)) Nothing

prop_deriveCauseEqualsManual :: Property
prop_deriveCauseEqualsManual = withTests 1 . property $
  mapM_ (\tgt -> do
            renderT tgt dCauseVal   === renderT tgt mCauseVal
            renderT tgt dNoCauseVal === renderT tgt mNoCauseVal)
        [TGraphical, TNarratable, TJson]

group :: Group
group = Group "Cause chain"
  [ ("cyclic cause chains render (every target)", prop_cyclicTerminates)
  , ("id-cyclic cause marker renders at most once", prop_markerAtMostOnce)
  , ("a cause chain renders a 'caused by' line",  prop_causedByAppears)
  , ("a related diagnostic's own cause also renders, not just the root's", prop_causedByAppearsForRelated)
  , ("derived specCause == manual diagnosticCause, all handlers", prop_deriveCauseEqualsManual)
  ]