Skip to content
Open
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
9 changes: 9 additions & 0 deletions fourmolu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ column-limit: 80 # ignored until v12 / ghc-9.6
function-arrows: leading
comma-style: leading # default
import-export-style: leading
import-grouping: # needs fourmolu >= v0.17
- name: "Preludes"
rules:
- glob: Prelude
- name: "Everything else"
rules:
- match: all
priority: 100
indent-wheres: false # default
record-brace-space: true
newlines-between-decls: 1 # default
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
unicode: never # default
respectful: true # default
fixities: [] # default
44 changes: 38 additions & 6 deletions src/Graphula.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
Expand Down Expand Up @@ -133,6 +134,9 @@ module Graphula
, NodeOptions
, GenerateKey
, NoConstraint
#if MIN_VERSION_base(4,20,0)

@cdmren cdmren Aug 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I wonder if the CPP guard here causes more confusion than it alleviates; GraphulaSeed could be defined and exported regardless of the base version.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmm, yeah. There's also the fact that runGraphulaT accepts the seed as an Int. This type existing would certainly make me expect it to take a GraphulaSeed.

I wonder if GraphulaExceptionContext { seed :: Int } is better.

  • Naming implies it's only used when "exception context" is a thing (I'll also document that)
  • Wouldn't be expected to be the runGraphulaT argument
  • May make sense in a Graphula.ExceptionContext module, to isolate CPP

, GraphulaSeed(..)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The comment above deprecates this section, did you mean to add this here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I just didn't see that part of the comment. I only saw "Lower-level". I didn't realize we had deprecated re-exports here.

#endif
) where

import Prelude hiding (readFile)
Expand Down Expand Up @@ -168,7 +172,23 @@ import Test.HUnit.Lang
)
import Test.QuickCheck (Arbitrary (..))
import Test.QuickCheck.Random (QCGen, mkQCGen)
import UnliftIO.Exception (catch, throwIO)
import UnliftIO.Exception (Exception (..), SomeException, catch, throwIO)

#if MIN_VERSION_base(4,20,0)
import Control.Exception (addExceptionContext)
import Control.Exception.Annotation (ExceptionAnnotation(..))

newtype GraphulaSeed = GraphulaSeed Int
deriving stock (Show)

instance ExceptionAnnotation GraphulaSeed

addSeedContext :: Int -> SomeException -> SomeException
addSeedContext seed = addExceptionContext $ GraphulaSeed seed
#else
addSeedContext :: Int -> SomeException -> SomeException
addSeedContext _ = id
#endif

-- | A constraint over lists of nodes for 'MonadGraphula', and 'GraphulaNode'.
--
Expand Down Expand Up @@ -262,12 +282,24 @@ runGraphulaT mSeed runDB action = do
runReaderT (runGraphulaT' action) (Args (RunDB runDB) qcGen)
`catch` logFailingSeed seed

logFailingSeed :: MonadIO m => Int -> HUnitFailure -> m a
logFailingSeed seed = rethrowHUnitWith ("Graphula with seed: " ++ show seed)
logFailingSeed :: MonadIO m => Int -> SomeException -> m a
logFailingSeed seed =
throwIO
. addSeedContext seed
. whenException (prefixHUnitFailure ("Graphula with seed: " <> show seed))

prefixHUnitFailure :: String -> HUnitFailure -> HUnitFailure
prefixHUnitFailure message (HUnitFailure l r) =
HUnitFailure l . Reason $ message ++ "\n\n" ++ formatFailureReason r

rethrowHUnitWith :: MonadIO m => String -> HUnitFailure -> m a
rethrowHUnitWith message (HUnitFailure l r) =
throwIO . HUnitFailure l . Reason $ message ++ "\n\n" ++ formatFailureReason r
-- | Apply a function to a 'SomeException' when it's the expected type
whenException
:: Exception e
=> (e -> e)
-- ^ Function to apply if 'fromException' at this type returns 'Just'
-> SomeException
-> SomeException
whenException f ex = maybe ex (toException . f) $ fromException ex

type GraphulaNode m a =
( HasDependencies a
Expand Down
15 changes: 15 additions & 0 deletions test/README.lhs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,21 @@ generationFailureSpec = do
Right _ -> pure ()
```

## Seed

`HUnitFailure` exceptions will have their reason prefixed by the seed used for
Graphula's arbitrary data, making it visible in expectation-failure messages.
Re-supplying this seed to `runGraphulaT` will reproduce the same graph, to
hopefully reproduce intermittent test failures caused by randomness.

If using `base >= 4.20`, **all** exceptions will also have this seed added to
the [exception's context][ghc-docs]. This won't be visible anywhere (besides
`HUnitFailure`) by default, but can be extracted through custom exception
handling, e.g. in a `SpecHook`. We hope tools like `hspec` make using exception
context more ergonomic in the future.

[ghc-docs]: https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception-Context.html

## Running It

```haskell
Expand Down
Loading