hedgehog-1.7: src/Hedgehog/Internal/Report.hs
{-# OPTIONS_HADDOCK not-home #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Hedgehog.Internal.Report (
-- * Report
Summary(..)
, Report(..)
, Progress(..)
, Result(..)
, FailureReport(..)
, FailedAnnotation(..)
, Style(..)
, Markup(..)
, Config(..)
, defaultConfig
, Context(..)
, Lines
, PrintPrefixIcons(..)
, renderProgress
, renderResult
, renderResultWith
, renderSummary
, renderDoc
, ppProgress
, ppResult
, ppResultWith
, ppSummary
, fromResult
, mkFailure
) where
import Control.Monad (zipWithM)
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Trans.Maybe (MaybeT(..))
import Data.Bifunctor (bimap, first, second)
import qualified Data.Char as Char
import Data.Either (partitionEithers)
import qualified Data.List as List
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (mapMaybe, catMaybes)
#if !MIN_VERSION_base(4,11,0)
import qualified Data.Semigroup as Semigroup
#endif
import Data.Traversable (for)
import Hedgehog.Internal.Config
import Hedgehog.Internal.Discovery (Pos(..), Position(..))
import qualified Hedgehog.Internal.Discovery as Discovery
import Hedgehog.Internal.Prelude
import Hedgehog.Internal.Property (CoverCount(..), CoverPercentage(..))
import Hedgehog.Internal.Property (Coverage(..), Label(..), LabelName(..))
import Hedgehog.Internal.Property (PropertyName(..), Log(..), Diff(..))
import Hedgehog.Internal.Property (ShrinkCount(..), PropertyCount(..))
import Hedgehog.Internal.Property (TestCount(..), DiscardCount(..))
import Hedgehog.Internal.Property (coverPercentage, coverageFailures)
import Hedgehog.Internal.Property (labelCovered)
import Hedgehog.Internal.Property (ShrinkPath(..), skipCompress)
import Hedgehog.Internal.Show
import Hedgehog.Internal.Source
import System.Console.ANSI (ColorIntensity(..), Color(..))
import System.Console.ANSI (ConsoleLayer(..), ConsoleIntensity(..))
import System.Console.ANSI (SGR(..), setSGRCode)
import System.Directory (makeRelativeToCurrentDirectory)
#if mingw32_HOST_OS
import System.IO (hSetEncoding, stdout, stderr, utf8)
#endif
import Text.PrettyPrint.Annotated.WL (Doc, (<#>), (<+>))
import qualified Text.PrettyPrint.Annotated.WL as WL
import Text.Printf (printf)
------------------------------------------------------------------------
-- Data
data FailedAnnotation =
FailedAnnotation {
failedSpan :: !(Maybe Span)
, failedValue :: !String
} deriving (Eq, Show)
data FailureReport =
FailureReport {
failureShrinks :: !ShrinkCount
, failureShrinkPath :: !ShrinkPath
, failureCoverage :: !(Maybe (Coverage CoverCount))
, failureAnnotations :: ![FailedAnnotation]
, failureLocation :: !(Maybe Span)
, failureMessage :: !String
, failureDiff :: !(Maybe Diff)
, failureFootnotes :: ![String]
} deriving (Eq, Show)
-- | The status of a running property test.
--
data Progress =
Running
| Shrinking !FailureReport
deriving (Eq, Show)
-- | The status of a completed property test.
--
-- In the case of a failure it provides the seed used for the test, the
-- number of shrinks, and the execution log.
--
data Result =
Failed !FailureReport
| GaveUp
| OK
deriving (Eq, Show)
-- | A report on a running or completed property test.
--
data Report a =
Report {
reportTests :: !TestCount
, reportDiscards :: !DiscardCount
, reportCoverage :: !(Coverage CoverCount)
, reportSeed :: !Seed
, reportStatus :: !a
} deriving (Show, Functor, Foldable, Traversable)
-- | A summary of all the properties executed.
--
data Summary =
Summary {
summaryWaiting :: !PropertyCount
, summaryRunning :: !PropertyCount
, summaryFailed :: !PropertyCount
, summaryGaveUp :: !PropertyCount
, summaryOK :: !PropertyCount
} deriving (Show)
instance Monoid Summary where
#if !MIN_VERSION_base(4,11,0)
mappend = (Semigroup.<>)
#endif
mempty =
Summary 0 0 0 0 0
instance Semigroup Summary where
Summary x1 x2 x3 x4 x5 <> Summary y1 y2 y3 y4 y5 =
Summary
(x1 + y1)
(x2 + y2)
(x3 + y3)
(x4 + y4)
(x5 + y5)
-- | Construct a summary from a single result.
--
fromResult :: Result -> Summary
fromResult = \case
Failed _ ->
mempty { summaryFailed = 1 }
GaveUp ->
mempty { summaryGaveUp = 1 }
OK ->
mempty { summaryOK = 1 }
summaryCompleted :: Summary -> PropertyCount
summaryCompleted (Summary _ _ x3 x4 x5) =
x3 + x4 + x5
summaryTotal :: Summary -> PropertyCount
summaryTotal (Summary x1 x2 x3 x4 x5) =
x1 + x2 + x3 + x4 + x5
------------------------------------------------------------------------
-- Pretty Printing Helpers
data Line a =
Line {
lineAnnotation :: !a
, lineNumber :: !LineNo
, _lineSource :: !String
} deriving (Eq, Ord, Show, Functor)
data Declaration a =
Declaration {
declarationFile :: !FilePath
, declarationLine :: !LineNo
, _declarationName :: !String
, declarationSource :: !(Map LineNo (Line a))
} deriving (Eq, Ord, Show, Functor)
data Style =
StyleDefault
| StyleAnnotation
| StyleFailure
deriving (Eq, Ord, Show)
data Markup =
WaitingIcon
| WaitingHeader
| RunningIcon
| RunningHeader
| ShrinkingIcon
| ShrinkingHeader
| FailedIcon
| FailedText
| GaveUpIcon
| GaveUpText
| SuccessIcon
| SuccessText
| CoverageIcon
| CoverageText
| CoverageFill
| DeclarationLocation
| StyledLineNo !Style
| StyledBorder !Style
| StyledSource !Style
| AnnotationGutter
| AnnotationValue
| FailureArrows
| FailureGutter
| FailureMessage
| DiffPrefix
| DiffInfix
| DiffSuffix
| DiffSame
| DiffRemoved
| DiffAdded
| ReproduceHeader
| ReproduceGutter
| ReproduceSource
deriving (Eq, Ord, Show)
instance Semigroup Style where
(<>) x y =
case (x, y) of
(StyleFailure, _) ->
StyleFailure
(_, StyleFailure) ->
StyleFailure
(StyleAnnotation, _) ->
StyleAnnotation
(_, StyleAnnotation) ->
StyleAnnotation
(StyleDefault, _) ->
StyleDefault
------------------------------------------------------------------------
takeAnnotation :: Log -> Maybe FailedAnnotation
takeAnnotation = \case
Annotation loc val ->
Just $ FailedAnnotation loc val
_ ->
Nothing
takeFootnote :: Log -> Maybe String
takeFootnote = \case
Footnote x ->
Just x
_ ->
Nothing
mkFailure ::
ShrinkCount
-> ShrinkPath
-> Maybe (Coverage CoverCount)
-> Maybe Span
-> String
-> Maybe Diff
-> [Log]
-> FailureReport
mkFailure shrinks shrinkPath mcoverage location message diff logs =
let
inputs =
mapMaybe takeAnnotation logs
footnotes =
mapMaybe takeFootnote logs
in
FailureReport shrinks shrinkPath mcoverage inputs location message diff footnotes
------------------------------------------------------------------------
-- Pretty Printing
ppShow :: Show x => x -> Doc a
ppShow = -- unfortunate naming clash
WL.text . show
markup :: Markup -> Doc Markup -> Doc Markup
markup =
WL.annotate
gutter :: Markup -> Doc Markup -> Doc Markup
gutter m x =
markup m ">" <+> x
icon :: PrintPrefixIcons -> Markup -> Char -> Doc Markup -> Doc Markup
icon p m i x = case p of
DisablePrefixIcons -> x
EnablePrefixIcons -> markup m (WL.char i) <+> x
ppTestCount :: TestCount -> Doc a
ppTestCount = \case
TestCount 1 ->
"1 test"
TestCount n ->
ppShow n <+> "tests"
ppDiscardCount :: DiscardCount -> Doc a
ppDiscardCount = \case
DiscardCount 1 ->
"1 discard"
DiscardCount n ->
ppShow n <+> "discards"
ppShrinkCount :: ShrinkCount -> Doc a
ppShrinkCount = \case
ShrinkCount 1 ->
"1 shrink"
ShrinkCount n ->
ppShow n <+> "shrinks"
-- | Render a compressed 'Skip'.
--
ppSkip :: Skip -> Doc a
ppSkip =
WL.text . skipCompress
-- | Render a compressed 'Skip', such that it can be read back in.
--
ppSkipReadable :: Skip -> Doc a
ppSkipReadable =
WL.text . show . skipCompress
ppRawPropertyCount :: PropertyCount -> Doc a
ppRawPropertyCount (PropertyCount n) =
ppShow n
ppWithDiscardCount :: DiscardCount -> Doc Markup
ppWithDiscardCount = \case
DiscardCount 0 ->
mempty
n ->
" with" <+> ppDiscardCount n
ppShrinkDiscard :: ShrinkCount -> DiscardCount -> Doc Markup
ppShrinkDiscard s d =
case (s, d) of
(0, 0) ->
""
(0, _) ->
" and" <+> ppDiscardCount d
(_, 0) ->
" and" <+> ppShrinkCount s
(_, _) ->
"," <+> ppShrinkCount s <+> "and" <+> ppDiscardCount d
mapSource :: (Map LineNo (Line a) -> Map LineNo (Line a)) -> Declaration a -> Declaration a
mapSource f decl =
decl {
declarationSource =
f (declarationSource decl)
}
-- | The span of non-whitespace characters for the line.
--
-- The result is @[inclusive, exclusive)@.
--
lineSpan :: Line a -> (ColumnNo, ColumnNo)
lineSpan (Line _ _ x0) =
let
(pre, x1) =
span Char.isSpace x0
(_, x2) =
span Char.isSpace (reverse x1)
start =
length pre
end =
start + length x2
in
(fromIntegral start, fromIntegral end)
takeLines :: Span -> Declaration a -> Map LineNo (Line a)
takeLines sloc =
fst . Map.split (spanEndLine sloc + 1) .
snd . Map.split (spanStartLine sloc - 1) .
declarationSource
readDeclaration :: MonadIO m => Span -> m (Maybe (Declaration ()))
readDeclaration sloc =
runMaybeT $ do
path <- liftIO . makeRelativeToCurrentDirectory $ spanFile sloc
(name, Pos (Position _ line0 _) src) <- MaybeT $
Discovery.readDeclaration path (spanEndLine sloc)
let
line =
fromIntegral line0
pure . Declaration path line name .
Map.fromList .
zip [line..] .
zipWith (Line ()) [line..] $
lines src
defaultStyle :: Declaration a -> Declaration (Style, [(Style, Doc Markup)])
defaultStyle =
fmap $ const (StyleDefault, [])
lastLineSpan :: Monad m => Span -> Declaration a -> MaybeT m (ColumnNo, ColumnNo)
lastLineSpan sloc decl =
case reverse . Map.elems $ takeLines sloc decl of
[] ->
MaybeT $ pure Nothing
x : _ ->
pure $
lineSpan x
ppFailedInputTypedArgument :: Int -> FailedAnnotation -> Doc Markup
ppFailedInputTypedArgument ix (FailedAnnotation _ val) =
WL.vsep [
WL.text "forAll" <> ppShow ix <+> "="
, WL.indent 2 . WL.vsep . fmap (markup AnnotationValue . WL.text) $ lines val
]
ppFailedInputDeclaration ::
MonadIO m
=> FailedAnnotation
-> m (Maybe (Declaration (Style, [(Style, Doc Markup)])))
ppFailedInputDeclaration (FailedAnnotation msloc val) =
runMaybeT $ do
sloc <- MaybeT $ pure msloc
decl <- fmap defaultStyle . MaybeT $ readDeclaration sloc
startCol <- fromIntegral . fst <$> lastLineSpan sloc decl
let
ppValLine =
WL.indent startCol .
(markup AnnotationGutter (WL.text "│ ") <>) .
markup AnnotationValue .
WL.text
valDocs =
fmap ((StyleAnnotation, ) . ppValLine) $
List.lines val
startLine =
fromIntegral $ spanStartLine sloc
endLine =
fromIntegral $ spanEndLine sloc
styleInput kvs =
foldr (Map.adjust . fmap . first $ const StyleAnnotation) kvs [startLine..endLine]
insertDoc =
Map.adjust (fmap . second $ const valDocs) endLine
pure $
mapSource (styleInput . insertDoc) decl
ppFailedInput ::
MonadIO m
=> Int
-> FailedAnnotation
-> m (Either (Doc Markup) (Declaration (Style, [(Style, Doc Markup)])))
ppFailedInput ix input = do
mdecl <- ppFailedInputDeclaration input
case mdecl of
Nothing ->
pure . Left $ ppFailedInputTypedArgument ix input
Just decl ->
pure $ Right decl
ppLineDiff :: LineDiff -> Doc Markup
ppLineDiff = \case
LineSame x ->
markup DiffSame $
" " <> WL.text x
LineRemoved x ->
markup DiffRemoved $
"- " <> WL.text x
LineAdded x ->
markup DiffAdded $
"+ " <> WL.text x
ppDiff :: Diff -> [Doc Markup]
ppDiff (Diff prefix removed infix_ added suffix diff) = [
markup DiffPrefix (WL.text prefix) <>
markup DiffRemoved (WL.text removed) <>
markup DiffInfix (WL.text infix_) <>
markup DiffAdded (WL.text added) <>
markup DiffSuffix (WL.text suffix)
] ++ fmap ppLineDiff (toLineDiff diff)
ppFailureLocation ::
MonadIO m
=> [Doc Markup]
-> Maybe Diff
-> Span
-> m (Maybe (Declaration (Style, [(Style, Doc Markup)])))
ppFailureLocation msgs mdiff sloc =
runMaybeT $ do
decl <- fmap defaultStyle . MaybeT $ readDeclaration sloc
(startCol, endCol) <- bimap fromIntegral fromIntegral <$> lastLineSpan sloc decl
let
arrowDoc =
WL.indent startCol $
markup FailureArrows (WL.text (replicate (endCol - startCol) '^'))
ppFailure x =
WL.indent startCol $
markup FailureGutter (WL.text "│ ") <> x
msgDocs =
fmap ((StyleFailure, ) . ppFailure . markup FailureMessage) msgs
diffDocs =
case mdiff of
Nothing ->
[]
Just diff ->
fmap ((StyleFailure, ) . ppFailure) (ppDiff diff)
docs =
[(StyleFailure, arrowDoc)] ++ msgDocs ++ diffDocs
startLine =
spanStartLine sloc
endLine =
spanEndLine sloc
styleFailure kvs =
foldr (Map.adjust . fmap . first $ const StyleFailure) kvs [startLine..endLine]
insertDoc =
Map.adjust (fmap . second $ const docs) endLine
pure $
mapSource (styleFailure . insertDoc) decl
type Annotation = (Style, [(Style, Doc Markup)])
newtype Lines = Lines Int
deriving (Eq, Num)
instance Show Lines where
showsPrec p (Lines n) = showsPrec p n
data Context = FullContext | Context Lines
deriving (Eq, Show)
applyContext :: Context -> Declaration Annotation -> Declaration Annotation
applyContext context decl = case context of
FullContext -> decl
Context n -> decl { declarationSource = limitContextTo n (declarationSource decl) }
limitContextTo :: Lines -> Map LineNo (Line Annotation) -> Map LineNo (Line Annotation)
limitContextTo (Lines context) =
let
skipBoring xs = case span isBoring xs of
(boring, []) -> take context boring
(boring, rest) -> takeEnd context boring <> keepInteresting rest
keepInteresting xs = case break isBoring xs of
(interesting, rest) -> interesting <> take context rest <> skipBoring rest
isBoring = isBoringAnnotation . lineAnnotation . snd
in
Map.fromList . skipBoring . Map.toList
takeEnd :: Int -> [a] -> [a]
takeEnd n = reverse . take n . reverse
isBoringAnnotation :: Annotation -> Bool
isBoringAnnotation = \ case
(StyleDefault, []) -> True
_ -> False
ppDeclaration :: Declaration (Style, [(Style, Doc Markup)]) -> Doc Markup
ppDeclaration decl = let source = declarationSource decl in
case Map.maxView source of
Nothing ->
mempty
Just (lastLine, _) ->
let
ppLocation =
WL.indent (digits + 1) $
markup (StyledBorder StyleDefault) "┏━━" <+>
markup DeclarationLocation (WL.text (declarationFile decl)) <+>
markup (StyledBorder StyleDefault) "━━━"
digits =
length . show . unLineNo $ lineNumber lastLine
ppLineNo =
WL.text . printf ("%" <> show digits <> "d") . unLineNo
ppEmptyNo :: Doc a
ppEmptyNo =
WL.text $ replicate digits ' '
ppSource :: Style -> LineNo -> String -> Doc Markup
ppSource style n src =
(if isOmittedLine (pred n) then addVerticalEllipsis else id) $
markup (StyledLineNo style) (ppLineNo n) <+>
markup (StyledBorder style) "┃" <+>
markup (StyledSource style) (WL.text src)
addVerticalEllipsis =
(verticalEllipsis <#>)
verticalEllipsis =
"\x22ee"
isOmittedLine n =
n >= firstLine && Map.notMember n source
firstLine =
fst $ Map.findMin source
ppAnnot (style, doc) =
markup (StyledLineNo style) ppEmptyNo <+>
markup (StyledBorder style) "┃" <+>
doc
ppLines = do
Line (style, xs) n src <- Map.elems source
ppSource style n src : fmap ppAnnot xs
in
WL.vsep (ppLocation : ppLines)
ppReproduce :: Maybe PropertyName -> Seed -> Skip -> Doc Markup
ppReproduce name seed skip =
WL.vsep [
markup ReproduceHeader
"This failure can be reproduced by running:"
, gutter ReproduceGutter . markup ReproduceSource $
"recheckAt" <+>
WL.text (showsPrec 11 seed "") <+>
ppSkipReadable skip <+>
maybe "<property>" (WL.text . unPropertyName) name
]
mergeLine :: Semigroup a => Line a -> Line a -> Line a
mergeLine (Line x no src) (Line y _ _) =
Line (x <> y) no src
mergeDeclaration :: Semigroup a => Declaration a -> Declaration a -> Declaration a
mergeDeclaration (Declaration file line name src0) (Declaration _ _ _ src1) =
Declaration file line name $
Map.unionWith mergeLine src0 src1
mergeDeclarations :: Semigroup a => [Declaration a] -> [Declaration a]
mergeDeclarations =
Map.elems .
Map.fromListWith mergeDeclaration .
fmap (\d -> ((declarationFile d, declarationLine d), d))
ppTextLines :: String -> [Doc Markup]
ppTextLines =
fmap WL.text . List.lines
ppFailureReport :: MonadIO m => Config -> Maybe PropertyName -> TestCount -> DiscardCount -> Seed -> FailureReport -> m [Doc Markup]
ppFailureReport config name tests discards seed (FailureReport _ shrinkPath mcoverage inputs0 mlocation0 msg mdiff msgs0) = do
let
basic =
-- Move the failure message to the end section if we have
-- no source location or can't find the source file.
let
msgs1 =
msgs0 ++
(if null msg then [] else [msg])
docs =
concatMap ppTextLines msgs1 ++
maybe [] ppDiff mdiff
in
(docs, Nothing)
(msgs1, mlocation) <-
case mlocation0 of
Nothing ->
return basic
Just location0 -> do
mAdvanced <-
ppFailureLocation (fmap WL.text $ List.lines msg) mdiff location0
case mAdvanced of
Just advanced ->
return (concatMap ppTextLines msgs0, Just advanced)
Nothing ->
return basic
coverageLocations <-
case mcoverage of
Nothing ->
pure []
Just coverage ->
for (coverageFailures tests coverage) $ \(MkLabel _ mclocation _ count) ->
case mclocation of
Nothing ->
pure Nothing
Just clocation ->
let
coverageMsg =
WL.cat [
"Failed ("
, WL.annotate CoverageText $
ppCoverPercentage (coverPercentage tests count) <> " coverage"
, ")"
]
in
ppFailureLocation [coverageMsg] Nothing clocation
(args, idecls) <- fmap partitionEithers $ zipWithM ppFailedInput [0..] inputs0
let
decls :: [Declaration (Style, [(Style, Doc Markup)])]
decls =
mergeDeclarations $
catMaybes (mlocation : coverageLocations) <> idecls
with xs f =
if null xs then
[]
else
[f xs]
whenSome f xs =
if null xs then
xs
else
f xs
bottom = case mcoverage of
Nothing | configPrintReproduceMessage config ->
[ppReproduce name seed (SkipToShrink tests discards shrinkPath)]
_ ->
[]
pure .
whenSome (mempty :) .
whenSome (++ [mempty]) .
WL.punctuate WL.line .
fmap (WL.vsep . fmap (indentBy $ configPrintPrefixIcons config)) .
fmap (id :: [Doc Markup] -> [Doc Markup]) .
List.filter (not . null) $
concat [
with args $
WL.punctuate WL.line
, with decls $
WL.punctuate WL.line . fmap (ppDeclaration . applyContext (configContext config))
, with msgs1 $
id
, with bottom $
id
]
indentBy :: PrintPrefixIcons -> Doc a -> Doc a
indentBy = \case
DisablePrefixIcons ->
id
EnablePrefixIcons ->
WL.indent 2
ppName :: Maybe PropertyName -> Doc a
ppName = \case
Nothing ->
"<interactive>" <> WL.space
Just "" ->
mempty
Just (PropertyName name) ->
WL.text name <> WL.space
ppProgress :: MonadIO m => Maybe PropertyName -> Report Progress -> m (Doc Markup)
ppProgress name (Report tests discards coverage _ status) =
case status of
Running ->
pure . WL.vsep $ [
icon EnablePrefixIcons RunningIcon '●' . WL.annotate RunningHeader $
ppName name <>
"passed" <+>
ppTestCount tests <>
ppWithDiscardCount discards <+>
"(running)"
] ++
ppCoverage EnablePrefixIcons tests coverage
Shrinking failure ->
pure . icon EnablePrefixIcons ShrinkingIcon '↯' . WL.annotate ShrinkingHeader $
ppName name <>
"failed" <+> ppFailedAtLocation (failureLocation failure) <#>
"after" <+>
ppTestCount tests <>
ppShrinkDiscard (failureShrinks failure) discards <+>
"(shrinking)"
ppResult :: MonadIO m => Maybe PropertyName -> Report Result -> m (Doc Markup)
ppResult = ppResultWith defaultConfig
ppResultWith :: MonadIO m => Config -> Maybe PropertyName -> Report Result -> m (Doc Markup)
ppResultWith config name (Report tests discards coverage seed result) = do
case result of
Failed failure -> do
pfailure <- ppFailureReport config name tests discards seed failure
pure . WL.vsep $ [
icon (configPrintPrefixIcons config) FailedIcon '✗' . WL.align . WL.annotate FailedText $
ppName name <>
(
if configPrintFailedAtLocation config then
"failed" <+> ppFailedAtLocation (failureLocation failure) <#> "after"
else
"failed after"
) <+>
ppTestCount tests <>
ppShrinkDiscard (failureShrinks failure) discards <>
"." <>
(
if configPrintShrinkPath config then
WL.line <>
"shrink path:" <+>
ppSkip (SkipToShrink tests discards $ failureShrinkPath failure)
else
mempty
)
] ++
ppCoverage (configPrintPrefixIcons config) tests coverage ++
pfailure
GaveUp ->
pure . WL.vsep $ [
icon (configPrintPrefixIcons config) GaveUpIcon '⚐' . WL.annotate GaveUpText $
ppName name <>
"gave up after" <+>
ppDiscardCount discards <>
", passed" <+>
ppTestCount tests <>
"."
] ++
ppCoverage (configPrintPrefixIcons config) tests coverage
OK ->
pure . WL.vsep $ [
icon (configPrintPrefixIcons config) SuccessIcon '✓' . WL.annotate SuccessText $
ppName name <>
"passed" <+>
ppTestCount tests <>
"."
] ++
ppCoverage (configPrintPrefixIcons config) tests coverage
ppFailedAtLocation :: Maybe Span -> Doc Markup
ppFailedAtLocation = \case
Just x ->
"at" <+>
WL.text (spanFile x) <> ":" <>
WL.pretty (unLineNo (spanStartLine x)) <> ":" <>
WL.pretty (unColumnNo (spanStartColumn x))
Nothing ->
mempty
ppCoverage :: PrintPrefixIcons -> TestCount -> Coverage CoverCount -> [Doc Markup]
ppCoverage icons tests x =
if Map.null (coverageLabels x) then
mempty
else
fmap (ppLabel icons tests (coverageWidth tests x)) .
List.sortOn labelLocation $
Map.elems (coverageLabels x)
data ColumnWidth =
ColumnWidth {
widthPercentage :: !Int
, widthMinimum :: !Int
, widthName :: !Int
, _widthNameFail :: !Int
}
instance Semigroup ColumnWidth where
(<>) (ColumnWidth p0 m0 n0 f0) (ColumnWidth p1 m1 n1 f1) =
ColumnWidth
(max p0 p1)
(max m0 m1)
(max n0 n1)
(max f0 f1)
instance Monoid ColumnWidth where
mempty =
ColumnWidth 0 0 0 0
mappend =
(<>)
coverageWidth :: TestCount -> Coverage CoverCount -> ColumnWidth
coverageWidth tests (Coverage labels) =
foldMap (labelWidth tests) labels
labelWidth :: TestCount -> Label CoverCount -> ColumnWidth
labelWidth tests x =
let
percentage =
length .
renderCoverPercentage .
coverPercentage tests $
labelAnnotation x
minimum_ =
if labelMinimum x == 0 then
0
else
length .
renderCoverPercentage $
labelMinimum x
name =
length .
unLabelName $
labelName x
nameFail =
if labelCovered tests x then
0
else
name
in
ColumnWidth percentage minimum_ name nameFail
ppLeftPad :: Int -> Doc a -> Doc a
ppLeftPad n doc =
let
ndoc =
length (show doc)
pad =
WL.text $
List.replicate (n - ndoc) ' '
in
pad <> doc
ppLabel :: PrintPrefixIcons -> TestCount -> ColumnWidth -> Label CoverCount -> Doc Markup
ppLabel icons tests w x@(MkLabel name _ minimum_ count) =
let
covered =
labelCovered tests x
ltext =
if not covered then
WL.annotate CoverageText
else
id
lborder =
WL.annotate (StyledBorder StyleDefault)
licon = case icons of
DisablePrefixIcons ->
mempty
EnablePrefixIcons ->
if not covered then
WL.annotate CoverageText "⚠ "
else
" "
lname =
WL.fill (widthName w) (ppLabelName name)
wminimum =
ppLeftPad (widthMinimum w) $
ppCoverPercentage minimum_
wcover i =
ppLeftPad (widthPercentage w + length i) $
WL.text i <>
ppCoverPercentage (coverPercentage tests count)
lminimum =
if widthMinimum w == 0 then
mempty
else if not covered then
" ✗ " <> wminimum
else if minimum_ == 0 then
" " <> ppLeftPad (widthMinimum w) ""
else
" ✓ " <> wminimum
lcover =
if widthMinimum w == 0 then
wcover ""
else if not covered then
wcover ""
else if minimum_ == 0 then
wcover ""
else
wcover ""
in
WL.hcat [
licon
, ltext lname
, lborder " "
, ltext lcover
, lborder " "
, ltext $ ppCoverBar (coverPercentage tests count) minimum_
, lborder "" -- "│"
, ltext lminimum
]
ppLabelName :: LabelName -> Doc a
ppLabelName (LabelName name) =
WL.text name
ppCoverPercentage :: CoverPercentage -> Doc Markup
ppCoverPercentage =
WL.text . renderCoverPercentage
ppCoverBar :: CoverPercentage -> CoverPercentage -> Doc Markup
ppCoverBar (CoverPercentage percentage) (CoverPercentage minimum_) =
let
barWidth :: Int
barWidth =
20
coverageRatio :: Double
coverageRatio =
percentage / 100.0
coverageWidth_ :: Int
coverageWidth_ =
floor $
coverageRatio * fromIntegral barWidth
minimumRatio :: Double
minimumRatio =
minimum_ / 100.0
minimumWidth :: Int
minimumWidth =
floor $
minimumRatio * fromIntegral barWidth
index :: [a] -> Int
index xs =
floor $
((coverageRatio * fromIntegral barWidth) - fromIntegral coverageWidth_) *
fromIntegral (length xs)
part xs =
xs !! index xs
fillWidth =
barWidth - coverageWidth_ - 1
fillErrorWidth =
max 0 (minimumWidth - coverageWidth_ - 1)
fillSurplusWidth =
fillWidth - fillErrorWidth
bar :: (Char, [Char]) -> Doc Markup
bar (full, parts) =
WL.hcat [
WL.text $ replicate coverageWidth_ full
, if fillWidth >= 0 then
if index parts == 0 then
if fillErrorWidth > 0 then
WL.annotate FailedText $ WL.text [part parts]
else
WL.annotate CoverageFill $ WL.text [part parts]
else
WL.text [part parts]
else
""
, WL.annotate FailedText . WL.text $
replicate fillErrorWidth (head parts)
, WL.annotate CoverageFill . WL.text $
replicate fillSurplusWidth (head parts)
--
-- Uncomment when debugging:
--
-- , WL.annotate CoverageFill . WL.text $
-- " " ++ show barWidth
-- ++ " " ++ show coverageWidth_
-- ++ " " ++ show minimumWidth
-- ++ " " ++ "/"
-- ++ " " ++ show fillErrorWidth
-- ++ " " ++ "+"
-- ++ " " ++ show fillSurplusWidth
-- ++ " " ++ "="
-- ++ " " ++ show fillWidth
]
in
bar ('█', ['·', '▏', '▎', '▍', '▌', '▋', '▊', '▉'])
-- FIXME Maybe this should be configurable?
-- Alternative histogram bars:
--bar ('⣿', ['·', '⡀', '⡄', '⡆', '⡇', '⣇', '⣧', '⣷'])
--bar ('⣿', ['⢕', '⡀', '⣀', '⣄', '⣤', '⣦', '⣶', '⣷'])
--bar ('⣿', ['⢕', '⡵', '⢗', '⣗', '⣟'])
--bar ('⣿', [' ', '⡵', '⢗', '⣗', '⣟'])
--bar ('█', ['░','▓'])
--bar ('█', ['░'])
renderCoverPercentage :: CoverPercentage -> String
renderCoverPercentage (CoverPercentage percentage) =
printf "%.0f" percentage <> "%"
ppWhenNonZero :: Doc a -> PropertyCount -> Maybe (Doc a)
ppWhenNonZero suffix n =
if n <= 0 then
Nothing
else
Just $ ppRawPropertyCount n <+> suffix
annotateSummary :: Summary -> Doc Markup -> Doc Markup
annotateSummary summary =
if summaryFailed summary > 0 then
icon EnablePrefixIcons FailedIcon '✗' . WL.annotate FailedText
else if summaryGaveUp summary > 0 then
icon EnablePrefixIcons GaveUpIcon '⚐' . WL.annotate GaveUpText
else if summaryWaiting summary > 0 || summaryRunning summary > 0 then
icon EnablePrefixIcons WaitingIcon '○' . WL.annotate WaitingHeader
else
icon EnablePrefixIcons SuccessIcon '✓' . WL.annotate SuccessText
ppSummary :: MonadIO m => Summary -> m (Doc Markup)
ppSummary summary =
let
complete =
summaryCompleted summary == summaryTotal summary
prefix end =
if complete then
mempty
else
ppRawPropertyCount (summaryCompleted summary) <>
"/" <>
ppRawPropertyCount (summaryTotal summary) <+>
"complete" <> end
addPrefix xs =
if null xs then
prefix mempty : []
else
prefix ": " : xs
suffix =
if complete then
"."
else
" (running)"
in
pure .
annotateSummary summary .
(<> suffix) .
WL.hcat .
addPrefix .
WL.punctuate ", " $
catMaybes [
ppWhenNonZero "failed" (summaryFailed summary)
, ppWhenNonZero "gave up" (summaryGaveUp summary)
, if complete then
ppWhenNonZero "succeeded" (summaryOK summary)
else
Nothing
]
renderDoc :: MonadIO m => UseColor -> Doc Markup -> m String
renderDoc color doc = do
let
dull =
SetColor Foreground Dull
vivid =
SetColor Foreground Vivid
bold =
SetConsoleIntensity BoldIntensity
start = \case
WaitingIcon ->
setSGRCode []
WaitingHeader ->
setSGRCode []
RunningIcon ->
setSGRCode []
RunningHeader ->
setSGRCode []
ShrinkingIcon ->
setSGRCode [vivid Red]
ShrinkingHeader ->
setSGRCode [vivid Red]
FailedIcon ->
setSGRCode [vivid Red]
FailedText ->
setSGRCode [vivid Red]
GaveUpIcon ->
setSGRCode [dull Yellow]
GaveUpText ->
setSGRCode [dull Yellow]
SuccessIcon ->
setSGRCode [dull Green]
SuccessText ->
setSGRCode [dull Green]
CoverageIcon ->
setSGRCode [dull Yellow]
CoverageText ->
setSGRCode [dull Yellow]
CoverageFill ->
setSGRCode [vivid Black]
DeclarationLocation ->
setSGRCode []
StyledLineNo StyleDefault ->
setSGRCode []
StyledSource StyleDefault ->
setSGRCode []
StyledBorder StyleDefault ->
setSGRCode []
StyledLineNo StyleAnnotation ->
setSGRCode [dull Magenta]
StyledSource StyleAnnotation ->
setSGRCode []
StyledBorder StyleAnnotation ->
setSGRCode []
AnnotationGutter ->
setSGRCode [dull Magenta]
AnnotationValue ->
setSGRCode [dull Magenta]
StyledLineNo StyleFailure ->
setSGRCode [vivid Red]
StyledSource StyleFailure ->
setSGRCode [vivid Red, bold]
StyledBorder StyleFailure ->
setSGRCode []
FailureArrows ->
setSGRCode [vivid Red]
FailureMessage ->
setSGRCode []
FailureGutter ->
setSGRCode []
DiffPrefix ->
setSGRCode []
DiffInfix ->
setSGRCode []
DiffSuffix ->
setSGRCode []
DiffSame ->
setSGRCode []
DiffRemoved ->
setSGRCode [dull Red]
DiffAdded ->
setSGRCode [dull Green]
ReproduceHeader ->
setSGRCode []
ReproduceGutter ->
setSGRCode []
ReproduceSource ->
setSGRCode []
end _ =
setSGRCode [Reset]
let
display =
case color of
EnableColor ->
WL.displayDecorated start end id
DisableColor ->
WL.display
#if mingw32_HOST_OS
liftIO $ do
hSetEncoding stdout utf8
hSetEncoding stderr utf8
#endif
pure .
display .
WL.renderSmart 100 $
WL.indent 2 doc
renderProgress :: MonadIO m => UseColor -> Maybe PropertyName -> Report Progress -> m String
renderProgress color name x =
renderDoc color =<< ppProgress name x
renderResult :: MonadIO m => UseColor -> Maybe PropertyName -> Report Result -> m String
renderResult = renderResultWith defaultConfig
data Config =
Config {
configContext :: Context
, configPrintFailedAtLocation :: Bool
, configPrintShrinkPath :: Bool
, configPrintReproduceMessage :: Bool
, configPrintPrefixIcons :: PrintPrefixIcons
} deriving (Eq, Show)
-- | Whether to add icons to the start of important output lines or not.
--
data PrintPrefixIcons =
DisablePrefixIcons
-- ^ Do not add icons to the start of important output lines.
| EnablePrefixIcons
-- ^ Add icons to the start of important output lines.
deriving (Eq, Show)
defaultConfig :: Config
defaultConfig = Config FullContext True True True EnablePrefixIcons
renderResultWith :: MonadIO m => Config -> UseColor -> Maybe PropertyName -> Report Result -> m String
renderResultWith config color name x =
renderDoc color =<< ppResultWith config name x
renderSummary :: MonadIO m => UseColor -> Summary -> m String
renderSummary color x =
renderDoc color =<< ppSummary x