packages feed

graphula 2.1.2.0 → 2.1.3.0

raw patch · 6 files changed

+204/−17 lines, 6 filesdep −markdown-unlitPVP ok

version bump matches the API change (PVP)

Dependencies removed: markdown-unlit

API changes (from Hackage documentation)

+ Graphula.ExceptionContext: GraphulaExceptionContext :: Int -> GraphulaExceptionContext
+ Graphula.ExceptionContext: [graphulaExceptionContextSeed] :: GraphulaExceptionContext -> Int
+ Graphula.ExceptionContext: instance GHC.Show.Show Graphula.ExceptionContext.GraphulaExceptionContext
+ Graphula.ExceptionContext: newtype GraphulaExceptionContext
+ Graphula.ExceptionContext: throwWithGraphulaExceptionContext :: MonadIO m => GraphulaExceptionContext -> SomeException -> m a

Files

README.md view
@@ -11,6 +11,7 @@  <!-- ```haskell+{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-}@@ -32,7 +33,11 @@  module Main (module Main) where -import Control.Exception (try, Exception(..))+import Control.Exception (try, Exception(..), SomeException)+#if MIN_VERSION_base(4,20,0)+import Control.Exception (someExceptionContext)+import Control.Exception.Context (getExceptionAnnotations)+#endif import Control.Monad.IO.Class import Control.Monad.IO.Unlift import Control.Monad.Logger (NoLoggingT)@@ -42,10 +47,12 @@ import Database.Persist.TH import GHC.Generics (Generic) import Graphula+#if MIN_VERSION_base(4,20,0)+import Graphula.ExceptionContext (GraphulaExceptionContext (..))+#endif import Test.Hspec import Test.QuickCheck import Test.QuickCheck.Arbitrary.Generic-import Text.Markdown.Unlit ()  instance (ToBackendKey SqlBackend a) => Arbitrary (Key a) where   arbitrary = toSqlKey <$> arbitrary@@ -193,6 +200,57 @@     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++```haskell+seedPrefixesHUnitFailureSpec :: IO ()+seedPrefixesHUnitFailureSpec = do+  result <- try $ runGraphulaT (Just 1) runDB $ do+    liftIO $ (1 :: Int) `shouldBe` 2++  case result :: Either SomeException () of+    Left ex -> show ex `shouldContain` "Graphula with seed: 1"+    Right () -> expectationFailure "expected an exception"++#if MIN_VERSION_base(4,20,0)+seedExceptionContextSpec :: IO ()+seedExceptionContextSpec = do+  result <- try $ runGraphulaT (Just 2) runDB $ do+    liftIO $ (1 :: Int) `shouldBe` 2++  case result :: Either SomeException () of+    Left ex -> seedsInContext ex `shouldBe` [2]+    Right () -> expectationFailure "expected an exception"++seedExceptionContextNonHUnitFailureSpec :: IO ()+seedExceptionContextNonHUnitFailureSpec = do+  result <- try $ runGraphulaT (Just 3) runDB $ do+    liftIO $ ioError $ userError "boom"++  case result :: Either SomeException () of+    Left ex -> seedsInContext ex `shouldBe` [3]+    Right () -> expectationFailure "expected an exception"++seedsInContext :: SomeException -> [Int]+seedsInContext ex =+  map graphulaExceptionContextSeed+    $ getExceptionAnnotations (someExceptionContext ex)+#endif+```+ ## Running It  ```haskell@@ -226,6 +284,11 @@     it "generates and links arbitrary graphs of data" simpleSpec     it "allows logging graphs" loggingSpec     it "shows informative generation failures" generationFailureSpec+    it "prefixes HUnitFailure reasons with the seed" seedPrefixesHUnitFailureSpec+#if MIN_VERSION_base(4,20,0)+    it "adds the seed to exception context, for HUnitFailure" seedExceptionContextSpec+    it "adds the seed to exception context, for other exceptions" seedExceptionContextNonHUnitFailureSpec+#endif  runDB :: MonadUnliftIO m => ReaderT SqlBackend (NoLoggingT (ResourceT m)) a -> m a runDB f = runSqlite "test.db" $ do
graphula.cabal view
@@ -1,6 +1,6 @@ cabal-version:      1.12 name:               graphula-version:            2.1.2.0+version:            2.1.3.0 license:            MIT license-file:       LICENSE maintainer:         Freckle Education@@ -31,6 +31,7 @@         Graphula.Class         Graphula.Dependencies         Graphula.Dependencies.Generic+        Graphula.ExceptionContext         Graphula.Idempotent         Graphula.Key         Graphula.Logged@@ -74,11 +75,12 @@         ghc-options: -Wno-redundant-constraints  test-suite readme-    type:             exitcode-stdio-1.0-    main-is:          README.lhs-    hs-source-dirs:   test-    other-modules:    Paths_graphula-    default-language: Haskell2010+    type:               exitcode-stdio-1.0+    main-is:            README.lhs+    build-tool-depends: markdown-unlit:markdown-unlit+    hs-source-dirs:     test+    other-modules:      Paths_graphula+    default-language:   Haskell2010     ghc-options:         -Weverything -Wno-all-missed-specialisations -Wno-implicit-prelude         -Wno-missing-import-lists -Wno-safe -Wno-unsafe -pgmL@@ -90,7 +92,6 @@         generic-arbitrary >=0.1.0,         graphula,         hspec >=2.5.5,-        markdown-unlit >=0.5.0,         monad-logger >=0.3.30,         persistent >=2.8.2,         persistent-sqlite >=2.8.2,
src/Graphula.hs view
@@ -156,6 +156,7 @@ import Database.Persist.Sql (SqlBackend) import Graphula.Class import Graphula.Dependencies+import Graphula.ExceptionContext import Graphula.Idempotent import Graphula.Logged import Graphula.NoConstraint@@ -168,7 +169,7 @@   ) import Test.QuickCheck (Arbitrary (..)) import Test.QuickCheck.Random (QCGen, mkQCGen)-import UnliftIO.Exception (catch, throwIO)+import UnliftIO.Exception (Exception (..), SomeException, catch)  -- | A constraint over lists of nodes for 'MonadGraphula', and 'GraphulaNode'. --@@ -262,12 +263,23 @@   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 =+  throwWithGraphulaExceptionContext (GraphulaExceptionContext seed)+    . whenException (prefixHUnitFailure ("Graphula with seed: " <> show seed)) -rethrowHUnitWith :: MonadIO m => String -> HUnitFailure -> m a-rethrowHUnitWith message (HUnitFailure l r) =-  throwIO . HUnitFailure l . Reason $ message ++ "\n\n" ++ formatFailureReason r+prefixHUnitFailure :: String -> HUnitFailure -> HUnitFailure+prefixHUnitFailure message (HUnitFailure l r) =+  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
src/Graphula/Arbitrary.hs view
@@ -1,4 +1,5 @@ -- | 'Arbitrary' operations that respect Graphula's seed+{-# OPTIONS_GHC -Wno-deprecations #-} module Graphula.Arbitrary   ( generate   ) where
+ src/Graphula/ExceptionContext.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DerivingStrategies #-}++module Graphula.ExceptionContext+  ( GraphulaExceptionContext (..)+  , throwWithGraphulaExceptionContext+  ) where++import Prelude++import Control.Exception (SomeException (..), throwIO)+import Control.Monad.IO.Class (MonadIO, liftIO)++#if MIN_VERSION_base(4,20,0)+import Control.Exception (ExceptionWithContext (..), someExceptionContext)+import Control.Exception.Annotation (ExceptionAnnotation)+import Control.Exception.Context (addExceptionAnnotation)+#endif++newtype GraphulaExceptionContext = GraphulaExceptionContext+  { graphulaExceptionContextSeed :: Int+  }+  deriving stock (Show)++#if MIN_VERSION_base(4,20,0)+instance ExceptionAnnotation GraphulaExceptionContext+#endif++-- | Attach the seed as exception context, then rethrow+--+-- On @base < 4.20@, where exception context does not exist, this simply+-- rethrows the given exception unchanged.+throwWithGraphulaExceptionContext+  :: MonadIO m+  => GraphulaExceptionContext+  -> SomeException+  -> m a+#if MIN_VERSION_base(4,20,0)+throwWithGraphulaExceptionContext ctx ex@(SomeException e) =+  liftIO+    . throwIO+    $ ExceptionWithContext+      (addExceptionAnnotation ctx (someExceptionContext ex))+      e+#else+throwWithGraphulaExceptionContext _ctx ex = liftIO $ throwIO ex+#endif
test/README.lhs view
@@ -11,6 +11,7 @@  <!-- ```haskell+{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-}@@ -32,7 +33,11 @@  module Main (module Main) where -import Control.Exception (try, Exception(..))+import Control.Exception (try, Exception(..), SomeException)+#if MIN_VERSION_base(4,20,0)+import Control.Exception (someExceptionContext)+import Control.Exception.Context (getExceptionAnnotations)+#endif import Control.Monad.IO.Class import Control.Monad.IO.Unlift import Control.Monad.Logger (NoLoggingT)@@ -42,10 +47,12 @@ import Database.Persist.TH import GHC.Generics (Generic) import Graphula+#if MIN_VERSION_base(4,20,0)+import Graphula.ExceptionContext (GraphulaExceptionContext (..))+#endif import Test.Hspec import Test.QuickCheck import Test.QuickCheck.Arbitrary.Generic-import Text.Markdown.Unlit ()  instance (ToBackendKey SqlBackend a) => Arbitrary (Key a) where   arbitrary = toSqlKey <$> arbitrary@@ -193,6 +200,57 @@     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++```haskell+seedPrefixesHUnitFailureSpec :: IO ()+seedPrefixesHUnitFailureSpec = do+  result <- try $ runGraphulaT (Just 1) runDB $ do+    liftIO $ (1 :: Int) `shouldBe` 2++  case result :: Either SomeException () of+    Left ex -> show ex `shouldContain` "Graphula with seed: 1"+    Right () -> expectationFailure "expected an exception"++#if MIN_VERSION_base(4,20,0)+seedExceptionContextSpec :: IO ()+seedExceptionContextSpec = do+  result <- try $ runGraphulaT (Just 2) runDB $ do+    liftIO $ (1 :: Int) `shouldBe` 2++  case result :: Either SomeException () of+    Left ex -> seedsInContext ex `shouldBe` [2]+    Right () -> expectationFailure "expected an exception"++seedExceptionContextNonHUnitFailureSpec :: IO ()+seedExceptionContextNonHUnitFailureSpec = do+  result <- try $ runGraphulaT (Just 3) runDB $ do+    liftIO $ ioError $ userError "boom"++  case result :: Either SomeException () of+    Left ex -> seedsInContext ex `shouldBe` [3]+    Right () -> expectationFailure "expected an exception"++seedsInContext :: SomeException -> [Int]+seedsInContext ex =+  map graphulaExceptionContextSeed+    $ getExceptionAnnotations (someExceptionContext ex)+#endif+```+ ## Running It  ```haskell@@ -226,6 +284,11 @@     it "generates and links arbitrary graphs of data" simpleSpec     it "allows logging graphs" loggingSpec     it "shows informative generation failures" generationFailureSpec+    it "prefixes HUnitFailure reasons with the seed" seedPrefixesHUnitFailureSpec+#if MIN_VERSION_base(4,20,0)+    it "adds the seed to exception context, for HUnitFailure" seedExceptionContextSpec+    it "adds the seed to exception context, for other exceptions" seedExceptionContextNonHUnitFailureSpec+#endif  runDB :: MonadUnliftIO m => ReaderT SqlBackend (NoLoggingT (ResourceT m)) a -> m a runDB f = runSqlite "test.db" $ do