Hello community, here is the log from the commit of package ghc-retry for openSUSE:Factory checked in at 2018-05-30 12:12:29 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/ghc-retry (Old) and /work/SRC/openSUSE:Factory/.ghc-retry.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ghc-retry" Wed May 30 12:12:29 2018 rev:10 rq:607874 version:0.7.6.2 Changes: -------- --- /work/SRC/openSUSE:Factory/ghc-retry/ghc-retry.changes 2017-09-15 22:10:19.764810325 +0200 +++ /work/SRC/openSUSE:Factory/.ghc-retry.new/ghc-retry.changes 2018-05-30 12:27:03.502793209 +0200 @@ -1,0 +2,11 @@ +Mon May 14 17:02:11 UTC 2018 - psim...@suse.com + +- Update retry to version 0.7.6.2. + * Clarify the semantics of `limitRetriesByDelay`. + * Add `limitRetriesByCumulativeDelay` + * Improve haddocks for fullJitterBackoff. + * Add Semigroup instance when the Semigroup class is available through base. + * Loosen dependency upper bounds. + * Add skipAsyncExceptions helper function + +------------------------------------------------------------------- Old: ---- retry-0.7.4.2.tar.gz New: ---- retry-0.7.6.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ ghc-retry.spec ++++++ --- /var/tmp/diff_new_pack.hEUulV/_old 2018-05-30 12:27:04.278766451 +0200 +++ /var/tmp/diff_new_pack.hEUulV/_new 2018-05-30 12:27:04.282766313 +0200 @@ -1,7 +1,7 @@ # # spec file for package ghc-retry # -# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %global pkg_name retry %bcond_with tests Name: ghc-%{pkg_name} -Version: 0.7.4.2 +Version: 0.7.6.2 Release: 0 Summary: Retry combinators for monadic actions that may fail License: BSD-3-Clause @@ -34,10 +34,12 @@ BuildRequires: ghc-transformers-devel %if %{with tests} BuildRequires: ghc-HUnit-devel -BuildRequires: ghc-QuickCheck-devel -BuildRequires: ghc-hspec-devel +BuildRequires: ghc-hedgehog-devel BuildRequires: ghc-mtl-devel BuildRequires: ghc-stm-devel +BuildRequires: ghc-tasty-devel +BuildRequires: ghc-tasty-hedgehog-devel +BuildRequires: ghc-tasty-hunit-devel BuildRequires: ghc-time-devel %endif @@ -80,7 +82,7 @@ %ghc_pkg_recache %files -f %{name}.files -%doc LICENSE +%license LICENSE %files devel -f %{name}-devel.files %doc README.md changelog.md ++++++ retry-0.7.4.2.tar.gz -> retry-0.7.6.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/retry-0.7.4.2/README.md new/retry-0.7.6.2/README.md --- old/retry-0.7.4.2/README.md 2016-11-23 19:32:49.000000000 +0100 +++ new/retry-0.7.6.2/README.md 2017-10-24 17:33:08.000000000 +0200 @@ -33,4 +33,5 @@ - John Wiegley - Michael Snoyman - Michael Xavier +- Marco Zocca (@ocramz) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/retry-0.7.4.2/changelog.md new/retry-0.7.6.2/changelog.md --- old/retry-0.7.4.2/changelog.md 2016-11-23 19:32:49.000000000 +0100 +++ new/retry-0.7.6.2/changelog.md 2018-03-22 18:45:26.000000000 +0100 @@ -1,3 +1,25 @@ +0.7.6.2 +* Loosen bounds on exceptions again. + +0.7.6.1 +* Loosen bounds on exceptions. + +0.7.6.0 +* Clarify the semantics of `limitRetriesByDelay`. +* Add `limitRetriesByCumulativeDelay` + +0.7.5.1 +* Improve haddocks for fullJitterBackoff. + +0.7.5.0 +* Add Semigroup instance when the Semigroup class is available through base. + +0.7.4.3 +* Loosen dependency upper bounds. + +0.7.5 +* Add skipAsyncExceptions helper function + 0.7.4.2 * Loosen HUnit dependency for tests. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/retry-0.7.4.2/retry.cabal new/retry-0.7.6.2/retry.cabal --- old/retry-0.7.4.2/retry.cabal 2016-11-23 19:32:49.000000000 +0100 +++ new/retry-0.7.6.2/retry.cabal 2018-03-22 18:45:18.000000000 +0100 @@ -14,7 +14,7 @@ case we should hang back for a bit and retry the query instead of simply raising an exception. -version: 0.7.4.2 +version: 0.7.6.2 synopsis: Retry combinators for monadic actions that may fail license: BSD3 license-file: LICENSE @@ -29,24 +29,35 @@ README.md changelog.md +flag lib-Werror + default: False + manual: True + library exposed-modules: Control.Retry build-depends: base >= 4.6 && < 5 , data-default-class - , exceptions >= 0.5 && < 0.9 + , exceptions >= 0.5 && < 0.11 , ghc-prim < 0.6 , random >= 1 && < 1.2 , transformers < 0.7 hs-source-dirs: src default-language: Haskell2010 + if flag(lib-Werror) + ghc-options: -Werror + + ghc-options: -Wall + test-suite test type: exitcode-stdio-1.0 - main-is: main.hs + main-is: Main.hs hs-source-dirs: test,src ghc-options: -threaded + other-modules: Control.Retry + Tests.Control.Retry build-depends: base ==4.* , exceptions @@ -54,14 +65,21 @@ , data-default-class , random , time - , QuickCheck >= 2.7 && < 2.10 - , HUnit >= 1.2.5.2 && < 1.6 - , hspec >= 1.9 + , HUnit >= 1.2.5.2 + , tasty + , tasty-hunit + , tasty-hedgehog + , hedgehog , stm , ghc-prim , mtl default-language: Haskell2010 + if flag(lib-Werror) + ghc-options: -Werror + + ghc-options: -Wall + source-repository head type: git location: git://github.com/Soostone/retry.git diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/retry-0.7.4.2/src/Control/Retry.hs new/retry-0.7.6.2/src/Control/Retry.hs --- old/retry-0.7.4.2/src/Control/Retry.hs 2016-11-23 19:32:49.000000000 +0100 +++ new/retry-0.7.6.2/src/Control/Retry.hs 2018-02-05 21:10:48.000000000 +0100 @@ -51,6 +51,7 @@ , recovering , stepping , recoverAll + , skipAsyncExceptions , logRetries , defaultLogMsg @@ -63,6 +64,7 @@ -- * Policy Transformers , limitRetriesByDelay + , limitRetriesByCumulativeDelay , capDelay -- * Development Helpers @@ -72,7 +74,6 @@ ------------------------------------------------------------------------------- import Control.Applicative -import Control.Arrow import Control.Concurrent #if MIN_VERSION_base(4, 7, 0) import Control.Exception (AsyncException, SomeAsyncException) @@ -87,14 +88,17 @@ import Control.Monad.Trans.State import Data.Default.Class import Data.List (foldl') -import Data.Functor.Identity import Data.Maybe import GHC.Generics import GHC.Prim import GHC.Types (Int(I#)) import System.Random +# if MIN_VERSION_base(4, 9, 0) +import Data.Semigroup +# else import Data.Monoid -import Prelude hiding (catch) +# endif +import Prelude ------------------------------------------------------------------------------- @@ -148,12 +152,28 @@ def = constantDelay 50000 <> limitRetries 5 +-- Base 4.9.0 adds a Data.Semigroup module. This has fewer +-- dependencies than the semigroups package, so we're using base's +-- only if its available. +# if MIN_VERSION_base(4, 9, 0) +instance Monad m => Semigroup (RetryPolicyM m) where + (RetryPolicyM a) <> (RetryPolicyM b) = RetryPolicyM $ \ n -> runMaybeT $ do + a' <- MaybeT $ a n + b' <- MaybeT $ b n + return $! max a' b' + + +instance Monad m => Monoid (RetryPolicyM m) where + mempty = retryPolicy $ const (Just 0) + mappend = (<>) +# else instance Monad m => Monoid (RetryPolicyM m) where mempty = retryPolicy $ const (Just 0) (RetryPolicyM a) `mappend` (RetryPolicyM b) = RetryPolicyM $ \ n -> runMaybeT $ do a' <- MaybeT $ a n b' <- MaybeT $ b n return $! max a' b' +#endif ------------------------------------------------------------------------------- @@ -254,13 +274,16 @@ ------------------------------------------------------------------------------- -- | Add an upperbound to a policy such that once the given time-delay --- amount has been reached or exceeded, the policy will stop retrying --- and fail. +-- amount *per try* has been reached or exceeded, the policy will stop +-- retrying and fail. If you need to stop retrying once *cumulative* +-- delay reaches a time-delay amount, use +-- 'limitRetriesByCumulativeDelay' limitRetriesByDelay - :: Int + :: Monad m + => Int -- ^ Time-delay limit in microseconds. - -> RetryPolicy - -> RetryPolicy + -> RetryPolicyM m + -> RetryPolicyM m limitRetriesByDelay i p = RetryPolicyM $ \ n -> (>>= limit) `liftM` getRetryPolicyM p n where @@ -268,6 +291,24 @@ ------------------------------------------------------------------------------- +-- | Add an upperbound to a policy such that once the cumulative delay +-- over all retries has reached or exceeded the given limit, the +-- policy will stop retrying and fail. +limitRetriesByCumulativeDelay + :: Monad m + => Int + -- ^ Time-delay limit in microseconds. + -> RetryPolicyM m + -> RetryPolicyM m +limitRetriesByCumulativeDelay cumulativeLimit p = RetryPolicyM $ \ stat -> + (>>= limit stat) `liftM` getRetryPolicyM p stat + where + limit status curDelay + | rsCumulativeDelay status `boundedPlus` curDelay > cumulativeLimit = Nothing + | otherwise = Just curDelay + + +------------------------------------------------------------------------------- -- | Implement a constant delay with unlimited retries. constantDelay :: Int @@ -281,7 +322,7 @@ -- increase by a factor of two. exponentialBackoff :: Int - -- ^ First delay in microseconds + -- ^ Base delay in microseconds -> RetryPolicy exponentialBackoff base = retryPolicy $ \ RetryStatus { rsIterNumber = n } -> Just $! base `boundedMult` boundedPow 2 n @@ -294,8 +335,12 @@ -- -- temp = min(cap, base * 2 ** attempt) -- --- sleep = temp / 2 + random_between(0, temp / 2) -fullJitterBackoff :: MonadIO m => Int -> RetryPolicyM m +-- sleep = temp \/ 2 + random_between(0, temp \/ 2) +fullJitterBackoff + :: MonadIO m + => Int + -- ^ Base delay in microseconds + -> RetryPolicyM m fullJitterBackoff base = RetryPolicyM $ \ RetryStatus { rsIterNumber = n } -> do let d = (base `boundedMult` boundedPow 2 n) `div` 2 rand <- liftIO $ randomRIO (0, d) @@ -408,14 +453,28 @@ -> m a recoverAll set f = recovering set handlers f where + handlers = skipAsyncExceptions ++ [h] + h _ = Handler $ \ (_ :: SomeException) -> return True + + +------------------------------------------------------------------------------- +-- | List of pre-made handlers that will skip retries on +-- 'AsyncException' and 'SomeAsyncException'. Append your handlers to +-- this list as a convenient way to make sure you're not catching +-- async exceptions like user interrupt. +skipAsyncExceptions + :: ( MonadIO m + ) + => [RetryStatus -> Handler m Bool] +skipAsyncExceptions = handlers + where + asyncH _ = Handler $ \ (_ :: AsyncException) -> return False #if MIN_VERSION_base(4, 7, 0) - someAsyncH _ = Handler $ \(_ :: SomeAsyncException) -> return False - handlers = [asyncH, someAsyncH, h] + someAsyncH _ = Handler $ \(_ :: SomeAsyncException) -> return False + handlers = [asyncH, someAsyncH] #else - handlers = [asyncH, h] + handlers = [asyncH] #endif - asyncH _ = Handler $ \ (_ :: AsyncException) -> return False - h _ = Handler $ \ (_ :: SomeException) -> return True ------------------------------------------------------------------------------- @@ -519,7 +578,8 @@ -- | Helper function for constructing handler functions of the form required -- by 'recovering'. logRetries - :: (Monad m, Show e, Exception e) + :: ( Monad m + , Exception e) => (e -> m Bool) -- ^ Test for whether action is to be retried -> (Bool -> e -> RetryStatus -> m ()) @@ -534,12 +594,12 @@ return result -- | For use with 'logRetries'. -defaultLogMsg :: (Show e, Exception e) => Bool -> e -> RetryStatus -> String +defaultLogMsg :: (Exception e) => Bool -> e -> RetryStatus -> String defaultLogMsg shouldRetry err status = - "[retry:" <> iter <> "] Encountered " <> show err <> ". " <> next + "[retry:" <> iter <> "] Encountered " <> show err <> ". " <> nextMsg where iter = show $ rsIterNumber status - next = if shouldRetry then "Retrying." else "Crashing." + nextMsg = if shouldRetry then "Retrying." else "Crashing." ------------------------------------------------------------------------------- @@ -564,8 +624,8 @@ simulatePolicyPP :: Int -> RetryPolicyM IO -> IO () simulatePolicyPP n p = do ps <- simulatePolicy n p - forM_ ps $ \ (n, res) -> putStrLn $ - show n <> ": " <> maybe "Inhibit" ppTime res + forM_ ps $ \ (iterNo, res) -> putStrLn $ + show iterNo <> ": " <> maybe "Inhibit" ppTime res putStrLn $ "Total cumulative delay would be: " <> (ppTime $ boundedSum $ (mapMaybe snd) ps) @@ -573,8 +633,8 @@ ------------------------------------------------------------------------------- ppTime :: (Integral a, Show a) => a -> String ppTime n | n < 1000 = show n <> "us" - | n < 1000000 = show (fromIntegral n / 1000) <> "ms" - | otherwise = show (fromIntegral n / 1000) <> "ms" + | n < 1000000 = show ((fromIntegral n / 1000) :: Double) <> "ms" + | otherwise = show ((fromIntegral n / 1000) :: Double) <> "ms" ------------------------------------------------------------------------------- -- Bounded arithmetic diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/retry-0.7.4.2/test/Main.hs new/retry-0.7.6.2/test/Main.hs --- old/retry-0.7.4.2/test/Main.hs 1970-01-01 01:00:00.000000000 +0100 +++ new/retry-0.7.6.2/test/Main.hs 2018-02-05 21:10:48.000000000 +0100 @@ -0,0 +1,22 @@ +module Main + ( main + ) where + + +------------------------------------------------------------------------------- +import Test.Tasty +------------------------------------------------------------------------------- +import qualified Tests.Control.Retry +------------------------------------------------------------------------------- + + + +main :: IO () +main = defaultMain tests + + +------------------------------------------------------------------------------- +tests :: TestTree +tests = testGroup "retry" + [ Tests.Control.Retry.tests + ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/retry-0.7.4.2/test/Tests/Control/Retry.hs new/retry-0.7.6.2/test/Tests/Control/Retry.hs --- old/retry-0.7.4.2/test/Tests/Control/Retry.hs 1970-01-01 01:00:00.000000000 +0100 +++ new/retry-0.7.6.2/test/Tests/Control/Retry.hs 2018-02-05 21:10:48.000000000 +0100 @@ -0,0 +1,371 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ScopedTypeVariables #-} +module Tests.Control.Retry + ( tests + ) where + +------------------------------------------------------------------------------- +import Control.Applicative +import Control.Concurrent +import Control.Concurrent.STM as STM +import Control.Exception (AsyncException (..), IOException, + MaskingState (..), + getMaskingState, throwTo) +import Control.Monad.Catch +import Control.Monad.Identity +import Control.Monad.IO.Class +import Control.Monad.Writer.Strict +import Data.Default.Class (def) +import Data.Either +import Data.IORef +import Data.List +import Data.Maybe +import Data.Time.Clock +import Data.Time.LocalTime () +import Data.Typeable +import Hedgehog as HH +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range +import System.IO.Error +import Test.Tasty +import Test.Tasty.Hedgehog +import Test.Tasty.HUnit (assertBool, testCase, (@?=)) +------------------------------------------------------------------------------- +import Control.Retry +------------------------------------------------------------------------------- + + +tests :: TestTree +tests = testGroup "Control.Retry" + [ recoveringTests + , monoidTests + , retryStatusTests + , quadraticDelayTests + , policyTransformersTests + , maskingStateTests + , capDelayTests + , limitRetriesByCumulativeDelayTests + ] + + +------------------------------------------------------------------------------- +recoveringTests :: TestTree +recoveringTests = testGroup "recovering" + [ testProperty "recovering test without quadratic retry delay" $ property $ do + startTime <- liftIO getCurrentTime + timeout <- forAll (Gen.int (Range.linear 0 15)) + retries <- forAll (Gen.int (Range.linear 0 50)) + res <- liftIO $ try $ recovering + (constantDelay timeout <> limitRetries retries) + testHandlers + (const $ throwM (userError "booo")) + endTime <- liftIO getCurrentTime + HH.assert (isLeftAnd isUserError res) + let ms' = (fromInteger . toInteger $ (timeout * retries)) / 1000000.0 + HH.assert (diffUTCTime endTime startTime >= ms') + , testGroup "exception hierarchy semantics" + [ testCase "does not catch async exceptions" $ do + counter <- newTVarIO (0 :: Int) + done <- newEmptyMVar + let work = atomically (modifyTVar' counter succ) >> threadDelay 1000000 + + tid <- forkIO $ + recoverAll (limitRetries 2) (const work) `finally` putMVar done () + + atomically (STM.check . (== 1) =<< readTVar counter) + throwTo tid UserInterrupt + + takeMVar done + + count <- atomically (readTVar counter) + count @?= 1 + + , testCase "recovers from custom exceptions" $ do + f <- mkFailN Custom1 2 + res <- try $ recovering + (constantDelay 5000 <> limitRetries 3) + [const $ Handler $ \ Custom1 -> return shouldRetry] + f + (res :: Either Custom1 ()) @?= Right () + + + , testCase "fails beyond policy using custom exceptions" $ do + f <- mkFailN Custom1 3 + res <- try $ recovering + (constantDelay 5000 <> limitRetries 2) + [const $ Handler $ \ Custom1 -> return shouldRetry] + f + (res :: Either Custom1 ()) @?= Left Custom1 + + + , testCase "does not recover from unhandled exceptions" $ do + f <- mkFailN Custom2 2 + res <- try $ recovering + (constantDelay 5000 <> limitRetries 5) + [const $ Handler $ \ Custom1 -> return shouldRetry] + f + (res :: Either Custom2 ()) @?= Left Custom2 + + + , testCase "recovers in presence of multiple handlers" $ do + f <- mkFailN Custom2 2 + res <- try $ recovering + (constantDelay 5000 <> limitRetries 5) + [ const $ Handler $ \ Custom1 -> return shouldRetry + , const $ Handler $ \ Custom2 -> return shouldRetry ] + f + (res :: Either Custom2 ()) @?= Right () + + + , testCase "general exceptions catch specific ones" $ do + f <- mkFailN Custom2 2 + res <- try $ recovering + (constantDelay 5000 <> limitRetries 5) + [ const $ Handler $ \ (_::SomeException) -> return shouldRetry ] + f + (res :: Either Custom2 ()) @?= Right () + + + , testCase "(redundant) even general catchers don't go beyond policy" $ do + f <- mkFailN Custom2 3 + res <- try $ recovering + (constantDelay 5000 <> limitRetries 2) + [ const $ Handler $ \ (_::SomeException) -> return shouldRetry ] + f + (res :: Either Custom2 ()) @?= Left Custom2 + + + , testCase "rethrows in presence of failed exception casts" $ do + f <- mkFailN Custom2 3 + final <- try $ do + res <- try $ recovering + (constantDelay 5000 <> limitRetries 2) + [ const $ Handler $ \ (_::SomeException) -> return shouldRetry ] + f + (res :: Either Custom1 ()) @?= Left Custom1 + final @?= Left Custom2 + ] + ] + + +------------------------------------------------------------------------------- +monoidTests :: TestTree +monoidTests = testGroup "Policy is a monoid" + [ testProperty "left identity" $ property $ + propIdentity (\p -> mempty <> p) id + , testProperty "right identity" $ property $ + propIdentity (\p -> p <> mempty) id + , testProperty "associativity" $ property $ + propAssociativity (\x y z -> x <> (y <> z)) (\x y z -> (x <> y) <> z) + ] + where + propIdentity left right = do + retryStatus <- forAll genRetryStatus + fixedDelay <- forAll (Gen.maybe (Gen.int (Range.linear 0 maxBound))) + let calculateDelay _rs = fixedDelay + let applyPolicy' f = getRetryPolicyM (f $ retryPolicy calculateDelay) retryStatus + validRes = maybe True (>= 0) + l <- liftIO $ applyPolicy' left + r <- liftIO $ applyPolicy' right + if validRes r && validRes l + then l === r + else return () + propAssociativity left right = do + retryStatus <- forAll genRetryStatus + let genDelay = Gen.maybe (Gen.int (Range.linear 0 maxBound)) + delayA <- forAll genDelay + delayB <- forAll genDelay + delayC <- forAll genDelay + let applyPolicy' f = liftIO $ getRetryPolicyM (f (retryPolicy (const delayA)) (retryPolicy (const delayB)) (retryPolicy (const delayC))) retryStatus + res <- liftIO (liftA2 (==) (applyPolicy' left) (applyPolicy' right)) + assert res + + +------------------------------------------------------------------------------- +retryStatusTests :: TestTree +retryStatusTests = testGroup "retry status" + [ testCase "passes the correct retry status each time" $ do + let policy = limitRetries 2 <> constantDelay 100 + rses <- gatherStatuses policy + rsIterNumber <$> rses @?= [0, 1, 2] + rsCumulativeDelay <$> rses @?= [0, 100, 200] + rsPreviousDelay <$> rses @?= [Nothing, Just 100, Just 100] + ] + + +------------------------------------------------------------------------------- +policyTransformersTests :: TestTree +policyTransformersTests = testGroup "policy transformers" + [ testProperty "always produces positive delay with positive constants (no rollover)" $ property $ do + delay <- forAll (Gen.int (Range.linear 0 maxBound)) + let res = runIdentity (simulatePolicy 1000 (exponentialBackoff delay)) + delays = catMaybes (snd <$> res) + mnDelay = if null delays + then Nothing + else Just (minimum delays) + case mnDelay of + Nothing -> return () + Just n -> do + footnote (show n ++ " is not >= 0") + HH.assert (n >= 0) + , testProperty "positive, nonzero exponential backoff is always incrementing" $ property $ do + delay <- forAll (Gen.int (Range.linear 1 maxBound)) + let res = runIdentity (simulatePolicy 1000 (limitRetriesByDelay maxBound (exponentialBackoff delay))) + delays = catMaybes (snd <$> res) + sort delays === delays + length (group delays) === length delays + ] + + +------------------------------------------------------------------------------- +maskingStateTests :: TestTree +maskingStateTests = testGroup "masking state" + [ testCase "shouldn't change masking state in a recovered action" $ do + maskingState <- getMaskingState + final <- try $ recovering def testHandlers $ const $ do + maskingState' <- getMaskingState + maskingState' @?= maskingState + fail "Retrying..." + assertBool + ("Expected IOException but didn't get one") + (isLeft (final :: Either IOException ())) + + , testCase "should mask asynchronous exceptions in exception handlers" $ do + let checkMaskingStateHandlers = + [ const $ Handler $ \(_ :: SomeException) -> do + maskingState <- getMaskingState + maskingState @?= MaskedInterruptible + return shouldRetry + ] + final <- try $ recovering def checkMaskingStateHandlers $ const $ fail "Retrying..." + assertBool + ("Expected IOException but didn't get one") + (isLeft (final :: Either IOException ())) + ] + + +------------------------------------------------------------------------------- +capDelayTests :: TestTree +capDelayTests = testGroup "capDelay" + [ testProperty "respects limitRetries" $ property $ do + retries <- forAll (Gen.int (Range.linear 1 100)) + cap <- forAll (Gen.int (Range.linear 1 maxBound)) + let policy = capDelay cap (limitRetries retries) + let delays = runIdentity (simulatePolicy (retries + 1) policy) + let Just lastDelay = lookup (retries - 1) delays + let Just gaveUp = lookup retries delays + let noDelay = 0 + lastDelay === Just noDelay + gaveUp === Nothing + , testProperty "does not allow any delays higher than the given delay" $ property $ do + cap <- forAll (Gen.int (Range.linear 1 maxBound)) + baseDelay <- forAll (Gen.int (Range.linear 1 100)) + basePolicy <- forAllWith (const "RetryPolicy") (genScalingPolicy baseDelay) + let policy = capDelay cap basePolicy + let delays = catMaybes (snd <$> runIdentity (simulatePolicy 100 policy)) + let baddies = filter (> cap) delays + baddies === [] + ] + + +------------------------------------------------------------------------------- +-- | Generates policies that increase on each iteration +genScalingPolicy :: (Alternative m) => Int -> m (RetryPolicyM Identity) +genScalingPolicy baseDelay = + (pure (exponentialBackoff baseDelay) <|> pure (fibonacciBackoff baseDelay)) + + +------------------------------------------------------------------------------- +limitRetriesByCumulativeDelayTests :: TestTree +limitRetriesByCumulativeDelayTests = testGroup "limitRetriesByCumulativeDelay" + [ testProperty "never exceeds the given cumulative delay" $ property $ do + baseDelay <- forAll (Gen.int (Range.linear 1 100)) + basePolicy <- forAllWith (const "RetryPolicy") (genScalingPolicy baseDelay) + cumulativeDelayMax <- forAll (Gen.int (Range.linear 1 10000)) + let policy = limitRetriesByCumulativeDelay cumulativeDelayMax basePolicy + let delays = catMaybes (snd <$> runIdentity (simulatePolicy 100 policy)) + footnoteShow delays + let actualCumulativeDelay = sum delays + footnote (show actualCumulativeDelay <> " <= " <> show cumulativeDelayMax) + HH.assert (actualCumulativeDelay <= cumulativeDelayMax) + + ] + +------------------------------------------------------------------------------- +quadraticDelayTests :: TestTree +quadraticDelayTests = testGroup "quadratic delay" + [ testProperty "recovering test with quadratic retry delay" $ property $ do + startTime <- liftIO getCurrentTime + timeout <- forAll (Gen.int (Range.linear 0 15)) + retries <- forAll (Gen.int (Range.linear 0 8)) + res <- liftIO $ try $ recovering + (exponentialBackoff timeout <> limitRetries retries) + [const $ Handler (\(_::SomeException) -> return True)] + (const $ throwM (userError "booo")) + endTime <- liftIO getCurrentTime + HH.assert (isLeftAnd isUserError res) + let tmo = if retries > 0 then timeout * 2 ^ (retries - 1) else 0 + let ms' = ((fromInteger . toInteger $ tmo) / 1000000.0) + HH.assert (diffUTCTime endTime startTime >= ms') + ] + +------------------------------------------------------------------------------- +isLeftAnd :: (a -> Bool) -> Either a b -> Bool +isLeftAnd f ei = case ei of + Left v -> f v + _ -> False + +testHandlers :: [a -> Handler IO Bool] +testHandlers = [const $ Handler (\(_::SomeException) -> return shouldRetry)] + + +data Custom1 = Custom1 deriving (Eq,Show,Read,Ord,Typeable) +data Custom2 = Custom2 deriving (Eq,Show,Read,Ord,Typeable) + + +instance Exception Custom1 +instance Exception Custom2 + + +------------------------------------------------------------------------------- +genRetryStatus :: MonadGen m => m RetryStatus +genRetryStatus = do + n <- Gen.int (Range.linear 0 maxBound) + d <- Gen.int (Range.linear 0 maxBound) + l <- Gen.maybe (Gen.int (Range.linear 0 d)) + return $ defaultRetryStatus { rsIterNumber = n + , rsCumulativeDelay = d + , rsPreviousDelay = l} + + +------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------- +-- | Create an action that will fail exactly N times with the given +-- exception and will then return () in any subsequent calls. +mkFailN :: (Exception e) => e -> Int -> IO (s -> IO ()) +mkFailN e n = do + r <- newIORef 0 + return $ const $ do + old <- atomicModifyIORef' r $ \ old -> (old+1, old) + case old >= n of + True -> return () + False -> throwM e + + +------------------------------------------------------------------------------- +gatherStatuses + :: MonadIO m + => RetryPolicyM (WriterT [RetryStatus] m) + -> m [RetryStatus] +gatherStatuses policy = execWriterT $ + retrying policy (\_ _ -> return shouldRetry) + (\rs -> tell [rs]) + + +------------------------------------------------------------------------------- +-- | Just makes things a bit easier to follow instead of a magic value +-- of @return True@ +shouldRetry :: Bool +shouldRetry = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/retry-0.7.4.2/test/main.hs new/retry-0.7.6.2/test/main.hs --- old/retry-0.7.4.2/test/main.hs 2016-11-23 19:32:49.000000000 +0100 +++ new/retry-0.7.6.2/test/main.hs 1970-01-01 01:00:00.000000000 +0100 @@ -1,2 +0,0 @@ -{-# OPTIONS_GHC -F -pgmF hspec-discover #-} -