1
1
module Freckle.App.Test.Hspec.AnnotatedException
2
2
( unwrapAnnotatedHUnitFailure
3
+ , annotateHUnitFailure
3
4
) where
4
5
5
6
import Freckle.App.Prelude
6
7
7
8
import Control.Exception qualified
9
+ import Control.Lens (Lens' , lens , over )
10
+ import Data.Annotation (Annotation , tryAnnotations )
11
+ import Data.List.NonEmpty (NonEmpty ((:|) ), nonEmpty )
12
+ import Data.Text qualified as T
8
13
import Freckle.App.Exception (AnnotatedException (.. ))
9
- import Test.HUnit.Lang (HUnitFailure )
14
+ import GHC.Stack (CallStack , prettyCallStack )
15
+ import Test.HUnit.Lang (FailureReason (.. ), HUnitFailure (.. ))
10
16
import Test.Hspec
11
17
12
18
-- | An hspec hook that lets hspec catch and pretty-print 'HUnitFailure', the
@@ -17,6 +23,85 @@ import Test.Hspec
17
23
-- you end up with an @'AnnotatedException' 'HUnitFailure'@, hspec doesn't recognize
18
24
-- it as an assertion failure and you get ugly output instead of nice output.
19
25
unwrapAnnotatedHUnitFailure :: Spec -> Spec
20
- unwrapAnnotatedHUnitFailure = around_ $
21
- Control.Exception. handle $ \ AnnotatedException {exception = e} ->
22
- Control.Exception. throw (e :: HUnitFailure )
26
+ unwrapAnnotatedHUnitFailure = around_ $ mapException annotateHUnitFailure
27
+
28
+ mapException :: (Exception e , Exception e' ) => (e -> e' ) -> IO a -> IO a
29
+ mapException f = Control.Exception. handle $ Control.Exception. throw . f
30
+
31
+ annotateHUnitFailure :: AnnotatedException HUnitFailure -> HUnitFailure
32
+ annotateHUnitFailure
33
+ AnnotatedException {exception, annotations} =
34
+ over hUnitFailureReason (annotateFailureReason annotations) exception
35
+
36
+ hUnitFailureReason :: Lens' HUnitFailure FailureReason
37
+ hUnitFailureReason =
38
+ lens
39
+ (\ (HUnitFailure _ x) -> x)
40
+ (\ (HUnitFailure l _) x -> HUnitFailure l x)
41
+
42
+ -- | Augment a 'FailureReason' with extra information derived from 'Annotation's
43
+ annotateFailureReason :: [Annotation ] -> FailureReason -> FailureReason
44
+ annotateFailureReason as =
45
+ \ case
46
+ Reason m -> Reason (makeMessage m as)
47
+ ExpectedButGot m e g -> ExpectedButGot (makeMessageMaybe m as) e g
48
+
49
+ -- | Construct a message that consists of an introductory paragraph plus
50
+ -- some additional paragraphs based on annotations, separated by blank lines
51
+ makeMessage :: String -> [Annotation ] -> String
52
+ makeMessage m as =
53
+ combineParagraphs $ stringParagraph m :| annotationParagraphs as
54
+
55
+ -- | Like 'makeMessage' but without necessarily having an introductory paragraph present
56
+ --
57
+ -- If there is neither an introductory paragraph nor any annotations, the result is 'Nothing'.
58
+ makeMessageMaybe :: Maybe String -> [Annotation ] -> Maybe String
59
+ makeMessageMaybe mm as =
60
+ fmap combineParagraphs $
61
+ nonEmpty $
62
+ fmap stringParagraph (toList mm) <> annotationParagraphs as
63
+
64
+ -- | Text that constitutes a paragraph in a potentially lengthy error message
65
+ --
66
+ -- Construct with 'stringParagraph' or 'textParagraph', which strip the text of
67
+ -- surrounding whitespace.
68
+ newtype Paragraph = Paragraph { paragraphText :: Text }
69
+
70
+ stringParagraph :: String -> Paragraph
71
+ stringParagraph = textParagraph . T. pack
72
+
73
+ textParagraph :: Text -> Paragraph
74
+ textParagraph = Paragraph . T. strip
75
+
76
+ -- | Combine a list of paragraphs into a single string for the final output
77
+ combineParagraphs :: Foldable t => t Paragraph -> String
78
+ combineParagraphs =
79
+ T. unpack . T. intercalate " \n\n " . fmap paragraphText . toList
80
+
81
+ -- | Render a list of annotations as a list of paragraphs
82
+ --
83
+ -- The paragraphs, depending on how much information there is to display, are:
84
+ --
85
+ -- * a summary of any annotations that aren't call stacks, if any
86
+ -- * the first call stack, if there are any call stacks
87
+ annotationParagraphs :: [Annotation ] -> [Paragraph ]
88
+ annotationParagraphs annotations =
89
+ catMaybes
90
+ [ otherAnnotationsPart <$> nonEmpty otherAnnotations
91
+ , callStackPart <$> listToMaybe callStacks
92
+ ]
93
+ where
94
+ (callStacks, otherAnnotations) = tryAnnotations @ CallStack annotations
95
+
96
+ -- | Construct a paragraph consisting of a bullet list of annotations
97
+ otherAnnotationsPart :: Foldable t => t Annotation -> Paragraph
98
+ otherAnnotationsPart =
99
+ textParagraph
100
+ . T. intercalate " \n "
101
+ . (" Annotations:" : )
102
+ . fmap ((" \t * " <> ) . T. pack . show )
103
+ . toList
104
+
105
+ -- | Construct a paragraph that displays a call stack
106
+ callStackPart :: CallStack -> Paragraph
107
+ callStackPart = textParagraph . T. pack . prettyCallStack
0 commit comments