packages feed

hedgehog 1.1.2 → 1.2

raw patch · 12 files changed

+728/−103 lines, 12 filesdep ~mtldep ~primitivedep ~resourcetPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: mtl, primitive, resourcet, template-haskell, text, transformers

API changes (from Hackage documentation)

- Hedgehog.Internal.Report: [failureSeed] :: FailureReport -> !Seed
- Hedgehog.Internal.Report: [failureSize] :: FailureReport -> !Size
+ Hedgehog: data Skip
+ Hedgehog: recheckAt :: MonadIO m => Seed -> Skip -> Property -> m ()
+ Hedgehog: withSkip :: Skip -> Property -> Property
+ Hedgehog.Gen: subset :: MonadGen m => Set a -> m (Set a)
+ Hedgehog.Internal.Config: SkipNothing :: Skip
+ Hedgehog.Internal.Config: SkipToShrink :: TestCount -> ShrinkPath -> Skip
+ Hedgehog.Internal.Config: SkipToTest :: TestCount -> Skip
+ Hedgehog.Internal.Config: data Skip
+ Hedgehog.Internal.Config: detectSkip :: MonadIO m => m Skip
+ Hedgehog.Internal.Config: resolveSkip :: MonadIO m => Maybe Skip -> m Skip
+ Hedgehog.Internal.Gen: subset :: MonadGen m => Set a -> m (Set a)
+ Hedgehog.Internal.Property: ShrinkPath :: [Int] -> ShrinkPath
+ Hedgehog.Internal.Property: SkipNothing :: Skip
+ Hedgehog.Internal.Property: SkipToShrink :: TestCount -> ShrinkPath -> Skip
+ Hedgehog.Internal.Property: SkipToTest :: TestCount -> Skip
+ Hedgehog.Internal.Property: [propertySkip] :: PropertyConfig -> Maybe Skip
+ Hedgehog.Internal.Property: data Skip
+ Hedgehog.Internal.Property: instance Data.String.IsString Hedgehog.Internal.Property.Skip
+ Hedgehog.Internal.Property: instance GHC.Classes.Eq Hedgehog.Internal.Property.ShrinkPath
+ Hedgehog.Internal.Property: instance GHC.Classes.Eq Hedgehog.Internal.Property.Skip
+ Hedgehog.Internal.Property: instance GHC.Classes.Ord Hedgehog.Internal.Property.ShrinkPath
+ Hedgehog.Internal.Property: instance GHC.Classes.Ord Hedgehog.Internal.Property.Skip
+ Hedgehog.Internal.Property: instance GHC.Show.Show Hedgehog.Internal.Property.ShrinkPath
+ Hedgehog.Internal.Property: instance GHC.Show.Show Hedgehog.Internal.Property.Skip
+ Hedgehog.Internal.Property: instance Language.Haskell.TH.Syntax.Lift Hedgehog.Internal.Property.ShrinkPath
+ Hedgehog.Internal.Property: instance Language.Haskell.TH.Syntax.Lift Hedgehog.Internal.Property.Skip
+ Hedgehog.Internal.Property: instance Language.Haskell.TH.Syntax.Lift Hedgehog.Internal.Property.TestCount
+ Hedgehog.Internal.Property: newtype ShrinkPath
+ Hedgehog.Internal.Property: shrinkPathCompress :: ShrinkPath -> String
+ Hedgehog.Internal.Property: shrinkPathDecompress :: String -> Maybe ShrinkPath
+ Hedgehog.Internal.Property: skipCompress :: Skip -> String
+ Hedgehog.Internal.Property: skipDecompress :: String -> Maybe Skip
+ Hedgehog.Internal.Property: withSkip :: Skip -> Property -> Property
+ Hedgehog.Internal.Report: [failureShrinkPath] :: FailureReport -> !ShrinkPath
+ Hedgehog.Internal.Report: [reportSeed] :: Report a -> !Seed
+ Hedgehog.Internal.Runner: recheckAt :: MonadIO m => Seed -> Skip -> Property -> m ()
- Hedgehog.Internal.Property: PropertyConfig :: !DiscardLimit -> !ShrinkLimit -> !ShrinkRetries -> !TerminationCriteria -> PropertyConfig
+ Hedgehog.Internal.Property: PropertyConfig :: !DiscardLimit -> !ShrinkLimit -> !ShrinkRetries -> !TerminationCriteria -> Maybe Skip -> PropertyConfig
- Hedgehog.Internal.Report: FailureReport :: !Size -> !Seed -> !ShrinkCount -> !Maybe (Coverage CoverCount) -> ![FailedAnnotation] -> !Maybe Span -> !String -> !Maybe Diff -> ![String] -> FailureReport
+ Hedgehog.Internal.Report: FailureReport :: !ShrinkCount -> !ShrinkPath -> !Maybe (Coverage CoverCount) -> ![FailedAnnotation] -> !Maybe Span -> !String -> !Maybe Diff -> ![String] -> FailureReport
- Hedgehog.Internal.Report: Report :: !TestCount -> !DiscardCount -> !Coverage CoverCount -> !a -> Report a
+ Hedgehog.Internal.Report: Report :: !TestCount -> !DiscardCount -> !Coverage CoverCount -> !Seed -> !a -> Report a
- Hedgehog.Internal.Report: mkFailure :: Size -> Seed -> ShrinkCount -> Maybe (Coverage CoverCount) -> Maybe Span -> String -> Maybe Diff -> [Log] -> FailureReport
+ Hedgehog.Internal.Report: mkFailure :: ShrinkCount -> ShrinkPath -> Maybe (Coverage CoverCount) -> Maybe Span -> String -> Maybe Diff -> [Log] -> FailureReport

Files

CHANGELOG.md view
@@ -1,11 +1,17 @@-## Version 1.1.2 (2022-09-02)+## Version 1.2 (2022-08-28) +* Allow skipping to a specific test number or shrink result ([#454][454], [@ChickenProp][ChickenProp])+  * Export shrinkPathCompress and shrinkPathDecompress ([#462][462], [@mbg][mbg]) * Support GHC 9.4 ([#461][461], [@ysangkok][ysangkok]) * Allow newer dependencies ([#457][457], [@ysangkok][ysangkok])+* Add Gen.subset ([#451][451], [@chris-martin][chris-martin])+* Add example for Gen.subsequence ([#450][450], [@chris-martin][chris-martin])+* Don't drop actions depending on shrunk predecessors ([#453][453], [@ChickenProp][ChickenProp])  ## Version 1.1.1 (2022-01-29)  * Support using fixed seed via `HEDGEHOG_SEED` ([#446][446], [@simfleischman][simfleischman] / [@moodmosaic][moodmosaic])+* Compatibility with text-2.0 ([#443][443], [@sjakobi][sjakobi]) * Better 'cover' example code in haddocks ([#423][423], [@jhrcek][jhrcek])  ## Version 1.1 (2022-01-27)@@ -246,17 +252,34 @@   https://github.com/patrickt [simfleischman]:   https://github.com/simfleischman+[ChickenProp]:+  https://github.com/ChickenProp [ysangkok]:-   https://github.com/ysangkok+  https://github.com/ysangkok+[mbg]:+  https://github.com/mbg [jhrcek]:   https://github.com/jhrcek ++[462]:+  https://github.com/hedgehogqa/haskell-hedgehog/pull/462 [461]:   https://github.com/hedgehogqa/haskell-hedgehog/pull/461 [457]:   https://github.com/hedgehogqa/haskell-hedgehog/pull/457+[454]:+  https://github.com/hedgehogqa/haskell-hedgehog/pull/454+[453]:+  https://github.com/hedgehogqa/haskell-hedgehog/pull/453+[451]:+  https://github.com/hedgehogqa/haskell-hedgehog/pull/451+[450]:+  https://github.com/hedgehogqa/haskell-hedgehog/pull/450 [446]:   https://github.com/hedgehogqa/haskell-hedgehog/pull/446+[443]:+  https://github.com/hedgehogqa/haskell-hedgehog/pull/443 [436]:   https://github.com/hedgehogqa/haskell-hedgehog/pull/436 [423]:
hedgehog.cabal view
@@ -1,4 +1,4 @@-version: 1.1.2+version: 1.2  name:   hedgehog@@ -65,7 +65,7 @@     , lifted-async                    >= 0.7        && < 0.11     , mmorph                          >= 1.0        && < 1.3     , monad-control                   >= 1.0        && < 1.1-    , mtl                             >= 2.1        && < 2.3+    , mtl                             >= 2.1        && < 2.4     , pretty-show                     >= 1.6        && < 1.11     , primitive                       >= 0.6        && < 0.8     , random                          >= 1.1        && < 1.3@@ -74,7 +74,7 @@     , template-haskell                >= 2.10       && < 2.20     , text                            >= 1.1        && < 2.1     , time                            >= 1.4        && < 1.13-    , transformers                    >= 0.5        && < 0.6+    , transformers                    >= 0.5        && < 0.7     , transformers-base               >= 0.4.5.1    && < 0.5     , wl-pprint-annotated             >= 0.0        && < 0.2 @@ -136,6 +136,7 @@     Test.Hedgehog.Filter     Test.Hedgehog.Maybe     Test.Hedgehog.Seed+    Test.Hedgehog.Skip     Test.Hedgehog.Text     Test.Hedgehog.Zip @@ -144,10 +145,10 @@     , base                            >= 3          && < 5     , containers                      >= 0.4        && < 0.7     , mmorph                          >= 1.0        && < 1.3-    , mtl                             >= 2.1        && < 2.3+    , mtl                             >= 2.1        && < 2.4     , pretty-show                     >= 1.6        && < 1.11-    , text                            >= 1.1        && < 1.3-    , transformers                    >= 0.3        && < 0.6+    , text                            >= 1.1        && < 2.1+    , transformers                    >= 0.3        && < 0.7    default-language:     Haskell2010
src/Hedgehog.hs view
@@ -60,6 +60,7 @@    , check   , recheck+  , recheckAt    , discover   , discoverPrefix@@ -82,6 +83,9 @@   , withRetries   , ShrinkRetries +  , withSkip+  , Skip+   -- * Generating Test Data   , Gen   , GenT@@ -185,11 +189,12 @@ import           Hedgehog.Internal.Property (Confidence, verifiedTermination, withConfidence) import           Hedgehog.Internal.Property (ShrinkLimit, withShrinks) import           Hedgehog.Internal.Property (ShrinkRetries, withRetries)+import           Hedgehog.Internal.Property (Skip, withSkip) import           Hedgehog.Internal.Property (Test, TestT, property, test) import           Hedgehog.Internal.Property (TestLimit, withTests) import           Hedgehog.Internal.Property (collect, label) import           Hedgehog.Internal.Range (Range, Size(..))-import           Hedgehog.Internal.Runner (check, recheck, checkSequential, checkParallel)+import           Hedgehog.Internal.Runner (check, recheck, recheckAt, checkSequential, checkParallel) import           Hedgehog.Internal.Seed (Seed(..)) import           Hedgehog.Internal.State (Command(..), Callback(..)) import           Hedgehog.Internal.State (Action, Sequential(..), Parallel(..))
src/Hedgehog/Gen.hs view
@@ -94,6 +94,7 @@    -- ** Combinations & Permutations   , subsequence+  , subset   , shuffle    -- ** Abstract State Machine
src/Hedgehog/Internal/Config.hs view
@@ -18,11 +18,15 @@   , WorkerCount(..)   , resolveWorkers +  , Skip(..)+  , resolveSkip+   , detectMark   , detectColor   , detectSeed   , detectVerbosity   , detectWorkers+  , detectSkip   ) where  import           Control.Monad.IO.Class (MonadIO(..))@@ -33,6 +37,7 @@  import           Hedgehog.Internal.Seed (Seed(..)) import qualified Hedgehog.Internal.Seed as Seed+import           Hedgehog.Internal.Property (Skip(..), skipDecompress)  import           Language.Haskell.TH.Syntax (Lift) @@ -166,6 +171,22 @@       Just env ->         pure $ WorkerCount env +detectSkip :: MonadIO m => m Skip+detectSkip =+  liftIO $ do+    menv <- lookupEnv "HEDGEHOG_SKIP"+    case menv of+      Nothing ->+        pure SkipNothing+      Just env ->+        case skipDecompress env of+          Nothing ->+            -- It's clearer for the user if we error out here, rather than+            -- silently defaulting to SkipNothing.+            error "HEDGEHOG_SKIP is not a valid Skip."+          Just skip ->+            pure skip+ resolveColor :: MonadIO m => Maybe UseColor -> m UseColor resolveColor = \case   Nothing ->@@ -191,5 +212,12 @@ resolveWorkers = \case   Nothing ->     detectWorkers+  Just x ->+    pure x++resolveSkip :: MonadIO m => Maybe Skip -> m Skip+resolveSkip = \case+  Nothing ->+    detectSkip   Just x ->     pure x
src/Hedgehog/Internal/Gen.hs view
@@ -130,6 +130,7 @@    -- ** Combinations & Permutations   , subsequence+  , subset   , shuffle   , shuffleSeq @@ -216,6 +217,7 @@ import           Data.Sequence (Seq) import qualified Data.Sequence as Seq import           Data.Set (Set)+import qualified Data.Set as Set import           Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text@@ -1662,9 +1664,36 @@  -- | Generates a random subsequence of a list. --+-- For example:+--+-- @+-- Gen.print (Gen.subsequence [1..5])+-- @+--+--   > === Outcome ===+--   > [1,2,4]+--   > === Shrinks ===+--   > []+--   > [2,4]+--   > [1,4]+--   > [1,2]+-- subsequence :: MonadGen m => [a] -> m [a] subsequence xs =   shrink Shrink.list $ filterM (const bool_) xs++-- | Generates a random subset of a set.+--+--  /This shrinks towards the empty set./+--+subset :: MonadGen m => Set a -> m (Set a)+-- Set.fromDistinctAscList has an unchecked precondition that the list+-- must be strictly ascending. This precondition is satisfied because+-- Set.toAscList produces a strictly ascending list, and the 'subsequence'+-- generator only removes elements from the list; it never adds or+-- rearranges elements, so the strictly ascending property is undisturbed.+subset =+  fmap Set.fromDistinctAscList . subsequence . Set.toAscList  -- | Generates a random permutation of a list. --
src/Hedgehog/Internal/Property.hs view
@@ -34,11 +34,14 @@   , DiscardCount(..)   , ShrinkLimit(..)   , ShrinkCount(..)+  , Skip(..)+  , ShrinkPath(..)   , ShrinkRetries(..)   , withTests   , withDiscards   , withShrinks   , withRetries+  , withSkip   , property   , test   , forAll@@ -47,6 +50,10 @@   , forAllWithT   , defaultMinTests   , discard+  , skipCompress+  , shrinkPathCompress+  , skipDecompress+  , shrinkPathDecompress    -- * Group   , Group(..)@@ -165,7 +172,7 @@ import qualified Data.Map.Strict as Map import           Data.Number.Erf (invnormcdf) import qualified Data.List as List-import           Data.String (IsString)+import           Data.String (IsString(..)) import           Data.Ratio ((%)) import           Data.Typeable (typeOf) @@ -179,7 +186,10 @@  import           Language.Haskell.TH.Syntax (Lift) +import qualified Numeric +import           Text.Read (readMaybe)+ ------------------------------------------------------------------------  -- | A property test, along with some configurable limits like how many times@@ -273,6 +283,10 @@     , propertyShrinkLimit :: !ShrinkLimit     , propertyShrinkRetries :: !ShrinkRetries     , propertyTerminationCriteria :: !TerminationCriteria++    -- | If this is 'Nothing', we take the Skip from the environment variable+    --   @HEDGEHOG_SKIP@.+    , propertySkip :: Maybe Skip     } deriving (Eq, Ord, Show, Lift)  -- | The number of successful tests that need to be run before a property test@@ -292,7 +306,7 @@ -- newtype TestCount =   TestCount Int-  deriving (Eq, Ord, Show, Num, Enum, Real, Integral)+  deriving (Eq, Ord, Show, Num, Enum, Real, Integral, Lift)  -- | The number of tests a property had to discard. --@@ -331,6 +345,169 @@   ShrinkCount Int   deriving (Eq, Ord, Show, Num, Enum, Real, Integral) +-- | Where to start running a property's tests.+--+data Skip =+  -- | Don't skip anything.+  --+    SkipNothing++  -- | Skip to a specific test number. If it fails, shrink as normal. If it+  --   passes, move on to the next test. Coverage checks are disabled.+  --+  | SkipToTest TestCount++  -- | Skip to a specific test number and shrink state. If it fails, stop+  --   without shrinking further. If it passes, the property will pass without+  --   running any more tests.+  --+  --   Due to implementation details, all intermediate shrink states - those on+  --   the direct path from the original test input to the target state - will+  --   be tested too, and their results discarded.+  --+  | SkipToShrink TestCount ShrinkPath+  deriving (Eq, Ord, Show, Lift)++-- | We use this instance to support usage like+--+-- @+--   withSkip "3:aB"+-- @+--+--   It throws an error if the input is not a valid compressed 'Skip'.+--+instance IsString Skip where+  fromString s =+    case skipDecompress s of+      Nothing ->+        error $ "fromString: Not a valid Skip: " ++ s+      Just skip ->+        skip++-- | The path taken to reach a shrink state.+--+newtype ShrinkPath =+  ShrinkPath [Int]+  deriving (Eq, Ord, Show, Lift)++-- | Compress a Skip into a hopefully-short alphanumeric string.+--+--   The bit that might be long is the 'ShrinkPath' in 'SkipToShrink'. For that,+--   we encode the path components in base 26, alternating between uppercase and+--   lowercase alphabets to distinguish list elements. Additionally when we have+--   runs of equal components, we use the normal base 10 encoding to indicate+--   the length.+--+--   This gives something which is hopefully quite short, but a human can+--   roughly interpret it by eyeball.+--+skipCompress :: Skip -> String+skipCompress = \case+  SkipNothing ->+    ""+  SkipToTest (TestCount n) ->+    show n+  SkipToShrink (TestCount n) sp ->+    show n ++ ":" ++ shrinkPathCompress sp++-- | Compress a 'ShrinkPath' into a hopefully-short alphanumeric string.+--+--   We encode the path components in base 26, alternating between uppercase and+--   lowercase alphabets to distinguish list elements. Additionally when we have+--   runs of equal components, we use the normal base 10 encoding to indicate+--   the length.+shrinkPathCompress :: ShrinkPath -> String+shrinkPathCompress (ShrinkPath sp) =+  let+    groups = List.map (\l -> (head l, length l)) $ List.group sp+  in+    (mconcat+      $ zipWith+          (\alphabet (loc, count) ->+              Numeric.showIntAtBase 26 (alphabet !!) loc+              <> if count == 1 then mempty else shows count+          )+          (cycle [['a'..'z'], ['A'..'Z']])+          groups+    )+      ""++-- | Decompress a 'Skip'.+--+--   This satisfies+--+-- @+--   skipDecompress (skipCompress a) == Just a+-- @+--+skipDecompress :: String -> Maybe Skip+skipDecompress str =+  if null str then+    Just SkipNothing+  else do+    let+      (tcStr, spStr)+        = span (/= ':') str+    tc <- TestCount <$> readMaybe tcStr+    if null spStr then+      Just $ SkipToTest tc+    else do+      sp <- shrinkPathDecompress $ drop 1 spStr+      Just $ SkipToShrink tc sp++-- | Decompress a 'ShrinkPath'.+--+--   This satisfies+--+-- @+--   shrinkPathDecompress (shrinkPathCompress a) == Just a+-- @+shrinkPathDecompress :: String -> Maybe ShrinkPath+shrinkPathDecompress str =+  let+    isDigit c = '0' <= c && c <= '9'+    isLower c = 'a' <= c && c <= 'z'+    isUpper c = 'A' <= c && c <= 'Z'+    classifyChar c = (isDigit c, isLower c, isUpper c)++    readSNum "" = []+    readSNum s@(c1:_) =+      if isDigit c1 then+        Numeric.readInt 10 isDigit (\c -> fromEnum c - fromEnum '0') s+      else if isLower c1 then+        Numeric.readInt 26 isLower (\c -> fromEnum c - fromEnum 'a') s+      else if isUpper c1 then+        Numeric.readInt 26 isUpper (\c -> fromEnum c - fromEnum 'A') s+      else+        []++    readNumMaybe s =+      case readSNum s of+        [(num, "")] -> Just num+        _ -> Nothing++    spGroups :: [(Maybe Int, Maybe Int)] =+      let+        go [] =+          []+        go (c1:cs) =+          let+            (hd, tl1) =+              span (\c -> classifyChar c == classifyChar c1) cs+            (digs, tl2) =+              span isDigit tl1+          in+            ( readNumMaybe (c1:hd)+            , readNumMaybe $ if null digs then "1" else digs+            )+            : go tl2+      in+        go str+  in do+    sp <- concat <$>+      traverse (\(mNum, mCount) -> replicate <$> mCount <*> mNum) spGroups+    Just $ ShrinkPath sp+ -- | The number of times to re-run a test during shrinking. This is useful if --   you are testing something which fails non-deterministically and you want to --   increase the change of getting a good shrink.@@ -991,6 +1168,8 @@         0     , propertyTerminationCriteria =         NoConfidenceTermination defaultMinTests+    , propertySkip =+        Nothing     }  -- | The minimum amount of tests to run for a 'Property'@@ -1076,6 +1255,12 @@ withRetries :: ShrinkRetries -> Property -> Property withRetries n =   mapConfig $ \config -> config { propertyShrinkRetries = n }++-- | Set the target that a property will skip to before it starts to run.+--+withSkip :: Skip -> Property -> Property+withSkip s =+  mapConfig $ \config -> config { propertySkip = Just s }  -- | Creates a property with the default configuration. --
src/Hedgehog/Internal/Report.hs view
@@ -62,10 +62,10 @@ import           Hedgehog.Internal.Property (TestCount(..), DiscardCount(..)) import           Hedgehog.Internal.Property (coverPercentage, coverageFailures) import           Hedgehog.Internal.Property (labelCovered)+import           Hedgehog.Internal.Property (ShrinkPath(..), skipCompress)  import           Hedgehog.Internal.Show import           Hedgehog.Internal.Source-import           Hedgehog.Range (Size)  import           System.Console.ANSI (ColorIntensity(..), Color(..)) import           System.Console.ANSI (ConsoleLayer(..), ConsoleIntensity(..))@@ -91,9 +91,8 @@  data FailureReport =   FailureReport {-      failureSize :: !Size-    , failureSeed :: !Seed-    , failureShrinks :: !ShrinkCount+      failureShrinks :: !ShrinkCount+    , failureShrinkPath :: !ShrinkPath     , failureCoverage :: !(Maybe (Coverage CoverCount))     , failureAnnotations :: ![FailedAnnotation]     , failureLocation :: !(Maybe Span)@@ -127,6 +126,7 @@       reportTests :: !TestCount     , reportDiscards :: !DiscardCount     , reportCoverage :: !(Coverage CoverCount)+    , reportSeed :: !Seed     , reportStatus :: !a     } deriving (Show, Functor, Foldable, Traversable) @@ -267,16 +267,15 @@     Nothing  mkFailure ::-     Size-  -> Seed-  -> ShrinkCount+     ShrinkCount+  -> ShrinkPath   -> Maybe (Coverage CoverCount)   -> Maybe Span   -> String   -> Maybe Diff   -> [Log]   -> FailureReport-mkFailure size seed shrinks mcoverage location message diff logs =+mkFailure shrinks shrinkPath mcoverage location message diff logs =   let     inputs =       mapMaybe takeAnnotation logs@@ -284,7 +283,7 @@     footnotes =       mapMaybe takeFootnote logs   in-    FailureReport size seed shrinks mcoverage inputs location message diff footnotes+    FailureReport shrinks shrinkPath mcoverage inputs location message diff footnotes  ------------------------------------------------------------------------ -- Pretty Printing@@ -326,6 +325,18 @@   ShrinkCount n ->     ppShow n <+> "shrinks" +-- | Render a compressed 'Skip'.+--+ppSkip :: Skip -> Doc a+ppSkip =+  WL.text . skipCompress++-- | Render a compressed 'Skip', such that it can be read back in.+--+ppSkipReadable :: Skip -> Doc a+ppSkipReadable =+  WL.text . show . skipCompress+ ppRawPropertyCount :: PropertyCount -> Doc a ppRawPropertyCount (PropertyCount n) =   ppShow n@@ -580,15 +591,15 @@       in         WL.vsep (ppLocation : ppLines) -ppReproduce :: Maybe PropertyName -> Size -> Seed -> Doc Markup-ppReproduce name size seed =+ppReproduce :: Maybe PropertyName -> Seed -> Skip -> Doc Markup+ppReproduce name seed skip =   WL.vsep [       markup ReproduceHeader         "This failure can be reproduced by running:"     , gutter ReproduceGutter . markup ReproduceSource $-        "recheck" <+>-        WL.text (showsPrec 11 size "") <+>+        "recheckAt" <+>         WL.text (showsPrec 11 seed "") <+>+        ppSkipReadable skip <+>         maybe "<property>" (WL.text . unPropertyName) name     ] @@ -611,8 +622,8 @@ ppTextLines =   fmap WL.text . List.lines -ppFailureReport :: MonadIO m => Maybe PropertyName -> TestCount -> FailureReport -> m [Doc Markup]-ppFailureReport name tests (FailureReport size seed _ mcoverage inputs0 mlocation0 msg mdiff msgs0) = do+ppFailureReport :: MonadIO m => Maybe PropertyName -> TestCount -> Seed -> FailureReport -> m [Doc Markup]+ppFailureReport name tests seed (FailureReport _ shrinkPath mcoverage inputs0 mlocation0 msg mdiff msgs0) = do   let     basic =       -- Move the failure message to the end section if we have@@ -684,7 +695,10 @@         f xs      bottom =-      maybe [ppReproduce name size seed] (const []) mcoverage+      maybe+        [ppReproduce name seed (SkipToShrink tests shrinkPath)]+        (const [])+        mcoverage    pure .     whenSome (mempty :) .@@ -712,7 +726,7 @@     WL.text name  ppProgress :: MonadIO m => Maybe PropertyName -> Report Progress -> m (Doc Markup)-ppProgress name (Report tests discards coverage status) =+ppProgress name (Report tests discards coverage _ status) =   case status of     Running ->       pure . WL.vsep $ [@@ -735,10 +749,10 @@         "(shrinking)"  ppResult :: MonadIO m => Maybe PropertyName -> Report Result -> m (Doc Markup)-ppResult name (Report tests discards coverage result) = do+ppResult name (Report tests discards coverage seed result) = do   case result of     Failed failure -> do-      pfailure <- ppFailureReport name tests failure+      pfailure <- ppFailureReport name tests seed failure       pure . WL.vsep $ [           icon FailedIcon '✗' . WL.align . WL.annotate FailedText $             ppName name <+>@@ -746,7 +760,9 @@             "after" <+>             ppTestCount tests <>             ppShrinkDiscard (failureShrinks failure) discards <>-            "."+            "." <#>+            "shrink path:" <+>+            ppSkip (SkipToShrink tests $ failureShrinkPath failure)         ] ++         ppCoverage tests coverage ++         pfailure
src/Hedgehog/Internal/Runner.hs view
@@ -13,6 +13,7 @@   -- * Running Individual Properties     check   , recheck+  , recheckAt    -- * Running Groups of Properties   , RunnerConfig(..)@@ -30,6 +31,7 @@ import qualified Control.Concurrent.STM.TVar as TVar import           Control.Monad.Catch (MonadCatch(..), catchAll) import           Control.Monad.IO.Class (MonadIO(..))+import           Data.Maybe (isJust)  import           Hedgehog.Internal.Config import           Hedgehog.Internal.Gen (evalGenT)@@ -39,12 +41,13 @@ import           Hedgehog.Internal.Property (Journal(..), Coverage(..), CoverCount(..)) import           Hedgehog.Internal.Property (Property(..), PropertyConfig(..), PropertyName(..)) import           Hedgehog.Internal.Property (PropertyT(..), Failure(..), runTestT)-import           Hedgehog.Internal.Property (ShrinkLimit, ShrinkRetries, withTests)+import           Hedgehog.Internal.Property (ShrinkLimit, ShrinkRetries, withTests, withSkip) import           Hedgehog.Internal.Property (TerminationCriteria(..)) import           Hedgehog.Internal.Property (TestCount(..), PropertyCount(..)) import           Hedgehog.Internal.Property (confidenceSuccess, confidenceFailure) import           Hedgehog.Internal.Property (coverageSuccess, journalCoverage) import           Hedgehog.Internal.Property (defaultMinTests)+import           Hedgehog.Internal.Property (ShrinkPath(..)) import           Hedgehog.Internal.Queue import           Hedgehog.Internal.Region import           Hedgehog.Internal.Report@@ -116,41 +119,89 @@  takeSmallest ::      MonadIO m-  => Size-  -> Seed-  -> ShrinkCount+  => ShrinkCount+  -> ShrinkPath   -> ShrinkLimit   -> ShrinkRetries   -> (Progress -> m ())   -> NodeT m (Maybe (Either Failure (), Journal))   -> m Result-takeSmallest size seed shrinks slimit retries updateUI = \case-  NodeT Nothing _ ->-    pure GaveUp+takeSmallest shrinks0 (ShrinkPath shrinkPath0) slimit retries updateUI =+  let+    loop shrinks revShrinkPath = \case+      NodeT Nothing _ ->+        pure GaveUp -  NodeT (Just (x, (Journal logs))) xs ->-    case x of-      Left (Failure loc err mdiff) -> do-        let-          failure =-            mkFailure size seed shrinks Nothing loc err mdiff (reverse logs)+      NodeT (Just (x, (Journal logs))) xs ->+        case x of+          Left (Failure loc err mdiff) -> do+            let+              shrinkPath =+                ShrinkPath $ reverse revShrinkPath+              failure =+                mkFailure shrinks shrinkPath Nothing loc err mdiff (reverse logs) -        updateUI $ Shrinking failure+            updateUI $ Shrinking failure -        if shrinks >= fromIntegral slimit then-          -- if we've hit the shrink limit, don't shrink any further-          pure $ Failed failure-        else-          findM xs (Failed failure) $ \m -> do-            o <- runTreeN retries m-            if isFailure o then-              Just <$> takeSmallest size seed (shrinks + 1) slimit retries updateUI o+            if shrinks >= fromIntegral slimit then+              -- if we've hit the shrink limit, don't shrink any further+              pure $ Failed failure             else-              return Nothing+              findM (zip [0..] xs) (Failed failure) $ \(n, m) -> do+                o <- runTreeN retries m+                if isFailure o then+                  Just <$> loop (shrinks + 1) (n : revShrinkPath) o+                else+                  return Nothing -      Right () ->-        return OK+          Right () ->+            return OK+  in+    loop shrinks0 (reverse shrinkPath0) +-- | Follow a given shrink path, instead of searching exhaustively. Assume that+-- the end of the path is minimal, and don't try to shrink any further than+-- that.+--+-- This evaluates the test for all the shrinks on the path, but not ones+-- off-path. Because the generator is mixed with the test code, it's probably+-- not possible to avoid this.+skipToShrink ::+     MonadIO m+  => ShrinkPath+  -> (Progress -> m ())+  -> NodeT m (Maybe (Either Failure (), Journal))+  -> m Result+skipToShrink (ShrinkPath shrinkPath) updateUI =+  let+    loop shrinks [] = \case+      NodeT Nothing _ ->+        pure GaveUp++      NodeT (Just (x, (Journal logs))) _ ->+        case x of+          Left (Failure loc err mdiff) -> do+            let+              failure =+                mkFailure shrinks (ShrinkPath shrinkPath) Nothing loc err mdiff (reverse logs)++            updateUI $ Shrinking failure+            pure $ Failed failure++          Right () ->+            return OK++    loop shrinks (s0:ss) = \case+      NodeT _ xs ->+        case drop s0 xs of+          [] ->+            pure GaveUp+          (x:_) -> do+            o <- runTreeT x+            loop (shrinks + 1) ss o+  in+    loop 0 shrinkPath+ checkReport ::      forall m.      MonadIO m@@ -161,8 +212,19 @@   -> PropertyT m ()   -> (Report Progress -> m ())   -> m (Report Result)-checkReport cfg size0 seed0 test0 updateUI =+checkReport cfg size0 seed0 test0 updateUI = do+  skip <- liftIO $ resolveSkip $ propertySkip cfg+   let+    (mSkipToTest, mSkipToShrink) =+      case skip of+        SkipNothing ->+          (Nothing, Nothing)+        SkipToTest t ->+          (Just t, Nothing)+        SkipToShrink t s ->+          (Just t, Just s)+     test =       catchAll test0 (fail . show) @@ -198,7 +260,7 @@       -> Coverage CoverCount       -> m (Report Result)     loop !tests !discards !size !seed !coverage0 = do-      updateUI $ Report tests discards coverage0 Running+      updateUI $ Report tests discards coverage0 seed0 Running        let         coverageReached =@@ -221,13 +283,12 @@           coverageSuccess tests coverage0          successReport =-          Report tests discards coverage0 OK+          Report tests discards coverage0 seed0 OK          failureReport message =-          Report tests discards coverage0 . Failed $ mkFailure-            size-            seed+          Report tests discards coverage0 seed0 . Failed $ mkFailure             0+            (ShrinkPath [])             (Just coverage0)             Nothing             message@@ -249,54 +310,70 @@         -- at this point, we know that enough tests have been run in order to         -- make a decision on if this was a successful run or not         ---        -- If we have early termination, then we need to check coverageReached / coverageUnreachable-        pure $ case terminationCriteria of-          EarlyTermination _ _ -> confidenceReport-          NoEarlyTermination _ _ -> confidenceReport-          NoConfidenceTermination _ ->-            if labelsCovered then-              successReport-            else-              failureReport $-                "Labels not sufficently covered after " <> show tests <> " tests"+        -- If we have early termination, then we need to check coverageReached /+        -- coverageUnreachable. If we skip tests, we ignore coverage.+        if isJust mSkipToTest then+          pure successReport+        else+          pure $ case terminationCriteria of+            EarlyTermination _ _ -> confidenceReport+            NoEarlyTermination _ _ -> confidenceReport+            NoConfidenceTermination _ ->+              if labelsCovered then+                successReport+              else+                failureReport $+                  "Labels not sufficently covered after " <> show tests <> " tests"        else if discards >= fromIntegral (propertyDiscardLimit cfg) then         -- we've hit the discard limit, give up-        pure $ Report tests discards coverage0 GaveUp+        pure $ Report tests discards coverage0 seed0 GaveUp        else         case Seed.split seed of-          (s0, s1) -> do-            node@(NodeT x _) <--              runTreeT . evalGenT size s0 . runTestT $ unPropertyT test-            case x of-              Nothing ->-                loop tests (discards + 1) (size + 1) s1 coverage0+          (s0, s1) -> case (mSkipToTest, mSkipToShrink) of+            -- If the report says failed "after 32 tests", the test number that+            -- failed was 31, but we want the user to be able to skip to 32 and+            -- start with the one that failed.+            (Just n, _) | n > tests + 1 ->+              loop (tests + 1) discards (size + 1) s1 coverage0+            (Just _, Just shrinkPath) -> do+              node <-+                runTreeT . evalGenT size s0 . runTestT $ unPropertyT test+              let+                mkReport =+                  Report (tests + 1) discards coverage0 seed0+              mkReport <$> skipToShrink shrinkPath (updateUI . mkReport) node+            _ -> do+              node@(NodeT x _) <-+                runTreeT . evalGenT size s0 . runTestT $ unPropertyT test+              case x of+                Nothing ->+                  loop tests (discards + 1) (size + 1) s1 coverage0 -              Just (Left _, _) ->-                let-                  mkReport =-                    Report (tests + 1) discards coverage0-                in-                  fmap mkReport $-                    takeSmallest-                      size-                      seed-                      0-                      (propertyShrinkLimit cfg)-                      (propertyShrinkRetries cfg)-                      (updateUI . mkReport)-                      node+                Just (Left _, _) ->+                  let+                    mkReport =+                      Report (tests + 1) discards coverage0 seed0+                  in+                    fmap mkReport $+                      takeSmallest+                        0+                        (ShrinkPath [])+                        (propertyShrinkLimit cfg)+                        (propertyShrinkRetries cfg)+                        (updateUI . mkReport)+                        node -              Just (Right (), journal) ->-                let-                  coverage =-                    journalCoverage journal <> coverage0-                in-                  loop (tests + 1) discards (size + 1) s1 coverage-  in-    loop 0 0 size0 seed0 mempty+                Just (Right (), journal) ->+                  let+                    coverage =+                      journalCoverage journal <> coverage0+                  in+                    loop (tests + 1) discards (size + 1) s1 coverage +  loop 0 0 size0 seed0 mempty+ checkRegion ::      MonadIO m   => Region@@ -356,6 +433,14 @@   let prop = withTests 1 prop0   _ <- liftIO . displayRegion $ \region ->     checkRegion region color Nothing size seed prop+  pure ()++recheckAt :: MonadIO m => Seed -> Skip -> Property -> m ()+recheckAt seed skip prop0 = do+  color <- detectColor+  let prop = withSkip skip prop0+  _ <- liftIO . displayRegion $ \region ->+    checkRegion region color Nothing 0 seed prop   pure ()  -- | Check a group of properties using the specified runner config.
src/Hedgehog/Internal/State.hs view
@@ -548,6 +548,12 @@     Command mgenInput exec callbacks <-       Gen.element_ $ filter (\c -> commandGenOK c state0) commands +    -- If we shrink the input, we still want to use the same output. Otherwise+    -- any actions using this output as part of their input will be dropped. But+    -- the existing output is still in the context, so `contextNewVar` will+    -- create a new one. To avoid that, we generate the output before the input.+    output <- contextNewVar+     input <-       case mgenInput state0 of         Nothing ->@@ -559,8 +565,6 @@       pure Nothing      else do-      output <- contextNewVar-       contextUpdate $         callbackUpdate callbacks state0 input (Var output) 
+ test/Test/Hedgehog/Skip.hs view
@@ -0,0 +1,246 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}++module Test.Hedgehog.Skip where++import           Control.Monad.IO.Class (MonadIO(..))++import           Data.Foldable (for_)+import           Data.IORef (IORef)+import qualified Data.IORef as IORef++import           Hedgehog+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import qualified Hedgehog.Internal.Config as Config+import           Hedgehog.Internal.Property (Skip(..), ShrinkPath(..), skipCompress, skipDecompress)+import qualified Hedgehog.Internal.Property as Property+import qualified Hedgehog.Internal.Runner as Runner+import           Hedgehog.Internal.Report (Report(..), Result(..), FailureReport(..))++-- | We use this property to help test skipping. It keeps a log of every time it+--   runs in the 'IORef' it's passed.+--+--   It ignores its seed. It fails at size 2. When it shrinks, it initially+--   shrinks to something that will pass, and then to something that will fail.+--+skipTestProperty :: IORef [(Size, Int, Bool)] -> Property+skipTestProperty logRef =+  withTests 5 . property $ do+    val@(curSize, _, shouldPass) <- forAll $ do+      curSize <- Gen.sized pure+      (shouldPass, nShrinks) <-+        (,)+          <$> Gen.shrink (\b -> if b then [] else [True]) (pure $ curSize /= 2)+          <*> Gen.shrink (\n -> reverse [0 .. n-1]) (pure 3)+      pure (curSize, nShrinks, shouldPass)++    -- Fail coverage to make sure we disable it when shrinking.+    cover 100 "Not 4" (curSize /= 4)++    liftIO $ IORef.modifyIORef' logRef (val :)+    assert shouldPass++checkProp :: MonadIO m => Property -> m (Report Result)+checkProp prop = do+  seed <- Config.resolveSeed Nothing+  liftIO $ Runner.checkReport+    (Property.propertyConfig prop)+    0+    seed+    (Property.propertyTest prop)+    (const $ pure ())++prop_SkipNothing :: Property+prop_SkipNothing =+  withTests 1 . property $ do+    logRef <- liftIO $ IORef.newIORef []+    let+      prop =+        withSkip "" $ skipTestProperty logRef++    report <- checkProp prop+    case reportStatus report of+      Failed f -> do+        failureShrinks f === 3+        failureShrinkPath f === ShrinkPath [1, 1, 1]++      _ ->+        failure++    logs <- liftIO $ reverse <$> IORef.readIORef logRef+    logs ===+      [ (0, 3, True)+      , (1, 3, True)+      , (2, 3, False)+      , (2, 3, True)+      , (2, 2, False)+      , (2, 2, True)+      , (2, 1, False)+      , (2, 1, True)+      , (2, 0, False)+      , (2, 0, True)+      ]++prop_SkipToFailingTest :: Property+prop_SkipToFailingTest =+  withTests 1 . property $ do+    logRef <- liftIO $ IORef.newIORef []+    let+      prop =+        withSkip "3" $ skipTestProperty logRef++    report <- checkProp prop+    case reportStatus report of+      Failed f -> do+        failureShrinks f === 3+        failureShrinkPath f === ShrinkPath [1, 1, 1]++      _ ->+        failure++    logs <- liftIO $ reverse <$> IORef.readIORef logRef+    logs ===+      [ (2, 3, False)+      , (2, 3, True)+      , (2, 2, False)+      , (2, 2, True)+      , (2, 1, False)+      , (2, 1, True)+      , (2, 0, False)+      , (2, 0, True)+      ]++prop_SkipPastFailingTest :: Property+prop_SkipPastFailingTest =+  withTests 1 . property $ do+    logRef <- liftIO $ IORef.newIORef []+    let+      prop =+        withSkip "4" $ skipTestProperty logRef++    report <- checkProp prop+    reportStatus report === OK++    logs <- liftIO $ reverse <$> IORef.readIORef logRef+    logs === [(3, 3, True), (4, 3, True)]++prop_SkipToNoShrink :: Property+prop_SkipToNoShrink =+  withTests 1 . property $ do+    logRef <- liftIO $ IORef.newIORef []+    let+      prop =+        withSkip "3:" $ skipTestProperty logRef++    report <- checkProp prop+    case reportStatus report of+      Failed f -> do+        failureShrinks f === 0+        failureShrinkPath f === Property.ShrinkPath []++      _ ->+        failure++    logs <- liftIO $ reverse <$> IORef.readIORef logRef+    logs === [(2, 3, False)]++prop_SkipToFailingShrink :: Property+prop_SkipToFailingShrink =+  withTests 1 . property $ do+    logRef <- liftIO $ IORef.newIORef []+    let+      prop =+        withSkip "3:b2" $ skipTestProperty logRef++    report <- checkProp prop+    case reportStatus report of+      Failed f -> do+        failureShrinks f === 2+        failureShrinkPath f === Property.ShrinkPath [1, 1]++      _ ->+        failure++    logs <- liftIO $ reverse <$> IORef.readIORef logRef+    logs === [(2, 3, False), (2, 2, False), (2, 1, False)]++prop_SkipToPassingShrink :: Property+prop_SkipToPassingShrink =+  withTests 1 . property $ do+    logRef <- liftIO $ IORef.newIORef []+    let+      prop =+        withSkip "3:bA" $ skipTestProperty logRef++    report <- checkProp prop+    reportStatus report === OK++    logs <- liftIO $ reverse <$> IORef.readIORef logRef+    logs === [(2, 3, False), (2, 2, False), (2, 2, True)]++genSkip :: Gen Skip+genSkip =+  let+    range =+      Range.linear 0 100++    genTestCount =+      Property.TestCount <$> Gen.int range++    genShrinkPath =+      Property.ShrinkPath <$> Gen.list range (Gen.int range)+  in+    Gen.choice+      [ pure SkipNothing+      , SkipToTest <$> genTestCount+      , SkipToShrink <$> genTestCount <*> genShrinkPath+      ]++-- | Test that `skipCompress` and `skipDecompress` roundtrip.+prop_compressSkip :: Property+prop_compressSkip =+  property $ do+    skip <- forAll genSkip+    tripping skip Property.skipCompress Property.skipDecompress++-- | Demonstrate some example compressions.+--+--   In general it's probably fine for compressions to change between hedgehog+--   versions. There's not much reason to share them or save them long-term. So+--   breaking this test isn't necessarily a problem, if it's done deliberately.+--+--   But it's useful to have examples, to avoid accidentally changing the+--   compression format and to demonstrate edge cases.+prop_compressDecompressExamples :: Property+prop_compressDecompressExamples =+  withTests 1 . property $ do+    let+      -- Each test case has a Skip, the result of compressing it, and some other+      -- strings that would decompress to the same Skip.+      testCases =+        [ (SkipNothing, "", [])+        , (SkipToTest 3, "3", ["03", "003"])+        , (SkipToTest 197, "197", ["0197", "00197"])+        , ( SkipToShrink 5 $ Property.ShrinkPath [2, 3, 0]+          , "5:cDa"+          , ["5:CdA", "05:c1b0D1A1"]+          )+        , ( SkipToShrink 21 $ Property.ShrinkPath [5, 3, 27, 27, 26]+          , "21:fDbb2BA"+          , ["21:fDbbBBba"]+          )+        ]++    for_ testCases $ \(skip, compressed, otherCompressions) -> do+      skipCompress skip === compressed+      for_ (compressed : otherCompressions) $ \c ->+        skipDecompress c === Just skip++tests :: IO Bool+tests =+  checkParallel $$(discover)
test/test.hs view
@@ -5,6 +5,7 @@ import qualified Test.Hedgehog.Filter import qualified Test.Hedgehog.Maybe import qualified Test.Hedgehog.Seed+import qualified Test.Hedgehog.Skip import qualified Test.Hedgehog.Text import qualified Test.Hedgehog.Zip @@ -17,6 +18,7 @@     , Test.Hedgehog.Filter.tests     , Test.Hedgehog.Maybe.tests     , Test.Hedgehog.Seed.tests+    , Test.Hedgehog.Skip.tests     , Test.Hedgehog.Text.tests     , Test.Hedgehog.Zip.tests     ]