Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fourmolu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ haddock-style: single-line
let-style: mixed
in-style: left-align
single-constraint-parens: never # ignored until v12 / ghc-9.6
trailing-section-operators: false # needs fourmolu >= v0.17
Copy link
Member Author

@pbrisbin pbrisbin Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This made the diff a little noisier; sorry I didn't do a better job isolating it.

unicode: never # default
respectful: true # default
fixities:
Expand Down
1 change: 1 addition & 0 deletions freckle-app/freckle-app.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ library
, persistent-postgresql
, persistent-sql-lifted
, postgresql-simple
, prettyprinter
, primitive
, resource-pool >=0.4.0.0
, resourcet
Expand Down
44 changes: 19 additions & 25 deletions freckle-app/library/Freckle/App/Test/Hspec/AnnotatedException.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty)
import Data.Text qualified as T
import Freckle.App.Exception (AnnotatedException (..))
import GHC.Stack (CallStack, prettyCallStack)
import Prettyprinter
import Prettyprinter.Render.String
import Prettyprinter.Util (reflow)
import Test.HUnit.Lang (FailureReason (..), HUnitFailure (..))
import Test.Hspec

Expand Down Expand Up @@ -50,41 +53,33 @@ annotateFailureReason as =
-- some additional paragraphs based on annotations, separated by blank lines
makeMessage :: String -> [Annotation] -> String
makeMessage m as =
combineParagraphs $ stringParagraph m :| annotationParagraphs as
combineParagraphs $ pretty m :| annotationParagraphs as

-- | Like 'makeMessage' but without necessarily having an introductory paragraph present
--
-- If there is neither an introductory paragraph nor any annotations, the result is 'Nothing'.
makeMessageMaybe :: Maybe String -> [Annotation] -> Maybe String
makeMessageMaybe mm as =
fmap combineParagraphs $
nonEmpty $
fmap stringParagraph (toList mm) <> annotationParagraphs as

-- | Text that constitutes a paragraph in a potentially lengthy error message
--
-- Construct with 'stringParagraph' or 'textParagraph', which strip the text of
-- surrounding whitespace.
newtype Paragraph = Paragraph {paragraphText :: Text}

stringParagraph :: String -> Paragraph
stringParagraph = textParagraph . T.pack

textParagraph :: Text -> Paragraph
textParagraph = Paragraph . T.strip
fmap combineParagraphs
$ nonEmpty
$ fmap pretty (toList mm) <> annotationParagraphs as

-- | Combine a list of paragraphs into a single string for the final output
combineParagraphs :: Foldable t => t Paragraph -> String
combineParagraphs :: Foldable t => t (Doc ann) -> String
combineParagraphs =
T.unpack . T.intercalate "\n\n" . fmap paragraphText . toList
renderString
. layoutSmart defaultLayoutOptions
. vsep
. punctuate hardline
. toList

-- | Render a list of annotations as a list of paragraphs
--
-- The paragraphs, depending on how much information there is to display, are:
--
-- * a summary of any annotations that aren't call stacks, if any
-- * the first call stack, if there are any call stacks
annotationParagraphs :: [Annotation] -> [Paragraph]
annotationParagraphs :: [Annotation] -> [Doc ann]
annotationParagraphs annotations =
catMaybes
[ otherAnnotationsPart <$> nonEmpty otherAnnotations
Expand All @@ -94,14 +89,13 @@ annotationParagraphs annotations =
(callStacks, otherAnnotations) = tryAnnotations @CallStack annotations

-- | Construct a paragraph consisting of a bullet list of annotations
otherAnnotationsPart :: Foldable t => t Annotation -> Paragraph
otherAnnotationsPart :: Foldable t => t Annotation -> Doc ann
otherAnnotationsPart =
textParagraph
. T.intercalate "\n"
vsep
. ("Annotations:" :)
. fmap (("\t * " <>) . T.pack . show)
. fmap (indent 2 . ("*" <+>) . align . reflow . T.pack . show)
. toList

-- | Construct a paragraph that displays a call stack
callStackPart :: CallStack -> Paragraph
callStackPart = textParagraph . T.pack . prettyCallStack
callStackPart :: CallStack -> Doc ann
callStackPart = pretty . prettyCallStack
1 change: 1 addition & 0 deletions freckle-app/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ library:
- persistent-sql-lifted
- persistent-postgresql
- postgresql-simple
- prettyprinter
- primitive
- resource-pool >= 0.4.0.0 # defaultPoolConfig, etc
- resourcet
Expand Down
142 changes: 91 additions & 51 deletions freckle-app/tests/Freckle/App/Test/Hspec/AnnotatedExceptionSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,33 @@ spec :: Spec
spec = do
describe "annotateHUnitFailure" $ do
describe "does nothing if there are no annotations" $ do
it "when the failure is Reason" $
let e = HUnitFailure Nothing (Reason "x")
in annotateHUnitFailure (AnnotatedException [] e) `shouldBe` e
it "when the failure is Reason"
$ let e = HUnitFailure Nothing (Reason "x")
in annotateHUnitFailure (AnnotatedException [] e) `shouldBe` e

it "when the failure is ExpectedButGot with no message" $
let e = HUnitFailure Nothing (ExpectedButGot Nothing "a" "b")
in annotateHUnitFailure (AnnotatedException [] e) `shouldBe` e
it "when the failure is ExpectedButGot with no message"
$ let e = HUnitFailure Nothing (ExpectedButGot Nothing "a" "b")
in annotateHUnitFailure (AnnotatedException [] e) `shouldBe` e

it "when the failure is ExpectedButGot with a message" $
let e = HUnitFailure Nothing (ExpectedButGot (Just "x") "a" "b")
in annotateHUnitFailure (AnnotatedException [] e) `shouldBe` e
it "when the failure is ExpectedButGot with a message"
$ let e = HUnitFailure Nothing (ExpectedButGot (Just "x") "a" "b")
in annotateHUnitFailure (AnnotatedException [] e) `shouldBe` e

describe "can show an annotation" $ do
it "when the failure is Reason" $
annotateHUnitFailure
it "when the failure is Reason"
$ annotateHUnitFailure
AnnotatedException
{ annotations = [toAnnotation @Int 56]
, exception = HUnitFailure Nothing (Reason "x")
}
`shouldBe` HUnitFailure
Nothing
( Reason . intercalate "\n" $
[ "x"
, ""
, "Annotations:"
, "\t * Annotation @Int 56"
]
( Reason . intercalate "\n"
$ [ "x"
, ""
, "Annotations:"
, " * Annotation @Int 56"
]
)

it "when the failure is ExpectedButGot with no message" $ do
Expand All @@ -55,41 +55,41 @@ spec = do
`shouldBe` HUnitFailure
Nothing
( ExpectedButGot
( Just . intercalate "\n" $
[ "Annotations:"
, "\t * Annotation @Int 56"
]
( Just . intercalate "\n"
$ [ "Annotations:"
, " * Annotation @Int 56"
]
)
"a"
"b"
)

it "when the failure is ExpectedButGot with a message" $
annotateHUnitFailure
it "when the failure is ExpectedButGot with a message"
$ annotateHUnitFailure
AnnotatedException
{ annotations = [toAnnotation @Int 56]
, exception = HUnitFailure Nothing (ExpectedButGot (Just "x") "a" "b")
}
`shouldBe` HUnitFailure
Nothing
( ExpectedButGot
( Just . intercalate "\n" $
[ "x"
, ""
, "Annotations:"
, "\t * Annotation @Int 56"
]
( Just . intercalate "\n"
$ [ "x"
, ""
, "Annotations:"
, " * Annotation @Int 56"
]
)
"a"
"b"
)

it "can show a stack trace" $
annotateHUnitFailure
it "can show a stack trace"
$ annotateHUnitFailure
AnnotatedException
{ annotations =
[ toAnnotation @CallStack $
fromList
[ toAnnotation @CallStack
$ fromList
[
( "abc"
, SrcLoc
Expand All @@ -108,21 +108,21 @@ spec = do
}
`shouldBe` HUnitFailure
Nothing
( Reason . intercalate "\n" $
[ "x"
, ""
, "CallStack (from HasCallStack):"
, " abc, called at src/Foo.hs:7:50 in thepackage:Foo"
]
( Reason . intercalate "\n"
$ [ "x"
, ""
, "CallStack (from HasCallStack):"
, " abc, called at src/Foo.hs:7:50 in thepackage:Foo"
]
)

it "can show both an annotation and a stack trace" $
annotateHUnitFailure
it "can show both an annotation and a stack trace"
$ annotateHUnitFailure
AnnotatedException
{ annotations =
[ toAnnotation @Text "Visibility is poor"
, toAnnotation @CallStack $
fromList
, toAnnotation @CallStack
$ fromList
[
( "abc"
, SrcLoc
Expand All @@ -141,13 +141,53 @@ spec = do
}
`shouldBe` HUnitFailure
Nothing
( Reason . intercalate "\n" $
[ "x"
, ""
, "Annotations:"
, "\t * Annotation @Text \"Visibility is poor\""
, ""
, "CallStack (from HasCallStack):"
, " abc, called at src/Foo.hs:7:50 in thepackage:Foo"
( Reason . intercalate "\n"
$ [ "x"
, ""
, "Annotations:"
, " * Annotation @Text \"Visibility is poor\""
, ""
, "CallStack (from HasCallStack):"
, " abc, called at src/Foo.hs:7:50 in thepackage:Foo"
]
)

it "wraps long annotations into neat lists"
$ annotateHUnitFailure
AnnotatedException
{ annotations =
[ toAnnotation @Text
"Visibility is poor. Visability is poor. Visability is poor. Visability is poor. Visability is poor. Visability is poor. Visability is poor. Visability is poor. Visability is poor. Visability is poor. Visability is poor"
, toAnnotation @CallStack
$ fromList
[
( "abc"
, SrcLoc
{ srcLocPackage = "thepackage"
, srcLocModule = "Foo"
, srcLocFile = "src/Foo.hs"
, srcLocStartLine = 7
, srcLocStartCol = 50
, srcLocEndLine = 8
, srcLocEndCol = 23
}
)
]
]
, exception = HUnitFailure Nothing (Reason "x")
}
`shouldBe` HUnitFailure
Nothing
( Reason . intercalate "\n"
$ [ "x"
, ""
, "Annotations:"
, " * Annotation @Text \"Visibility is poor. Visability is poor. Visability is"
, " poor. Visability is poor. Visability is poor. Visability is poor. Visability"
, " is poor. Visability is poor. Visability is poor. Visability is poor."
, " Visability is poor\""
Comment on lines +185 to +188
Copy link
Member Author

@pbrisbin pbrisbin Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the commit, using a prettyprinter for this case isn't really doing a whole lot. If it weren't already a transitive dependency, I'd be hesitant to pull it in just for this. This smart reflow is really the only thing we're getting from it, so at least there's something.

, ""
, "CallStack (from HasCallStack):"
, " abc, called at src/Foo.hs:7:50 in thepackage:Foo"
]
)
Loading