hspec 1.11.4 → 1.12.0
raw patch · 23 files changed
+694/−394 lines, 23 filesdep +stringbuilderdep ~hspec-meta
Dependencies added: stringbuilder
Dependency ranges changed: hspec-meta
Files
- hspec-discover/src/Run.hs +20/−18
- hspec-discover/test/RunSpec.hs +29/−18
- hspec.cabal +7/−2
- src/Test/Hspec.hs +12/−9
- src/Test/Hspec/Config.hs +11/−65
- src/Test/Hspec/Core.hs +3/−0
- src/Test/Hspec/Core/Type.hs +27/−9
- src/Test/Hspec/Discover.hs +82/−0
- src/Test/Hspec/Formatters.hs +28/−13
- src/Test/Hspec/Formatters/Internal.hs +16/−17
- src/Test/Hspec/Options.hs +75/−51
- src/Test/Hspec/Runner.hs +67/−60
- src/Test/Hspec/Runner/Eval.hs +40/−33
- src/Test/Hspec/Runner/Tree.hs +20/−7
- test/Helper.hs +16/−0
- test/HelperSpec.hs +18/−0
- test/Test/Hspec/DiscoverSpec.hs +72/−0
- test/Test/Hspec/FormattersSpec.hs +64/−56
- test/Test/Hspec/HUnitSpec.hs +15/−10
- test/Test/Hspec/OptionsSpec.hs +10/−10
- test/Test/Hspec/RunnerSpec.hs +12/−4
- test/Test/Hspec/UtilSpec.hs +2/−2
- test/Test/HspecSpec.hs +48/−10
hspec-discover/src/Run.hs view
@@ -5,6 +5,7 @@ run -- exported for testing+, Spec(..) , importList , fileToSpec , findSpecs@@ -29,7 +30,10 @@ instance IsString ShowS where fromString = showString -type Spec = String+data Spec = Spec {+ specFile :: FilePath+, specModule :: String+} deriving (Eq, Show) run :: [String] -> IO () run args_ = do@@ -49,16 +53,17 @@ mkSpecModule :: FilePath -> Config -> [Spec] -> String mkSpecModule src c nodes =- ( "{-# LINE 1 " . shows src . " #-}"+ ( "{-# LINE 1 " . shows src . " #-}\n"+ . showString "{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}\n" . showString ("module " ++ module_ ++" where\n") . importList nodes- . maybe driver (driverWithFormatter (null nodes)) (configFormatter c)+ . showString "import Test.Hspec.Discover\n"+ . maybe driver driverWithFormatter (configFormatter c) . formatSpecs nodes ) "\n" where driver =- showString "import Test.Hspec\n"- . case configNoMain c of+ case configNoMain c of False -> showString "main :: IO ()\n" . showString "main = hspec $ "@@ -73,11 +78,9 @@ toUpper m:ms -driverWithFormatter :: Bool -> String -> ShowS-driverWithFormatter isEmpty f =- (if isEmpty then id else "import Test.Hspec\n")- . showString "import Test.Hspec.Runner\n"- . showString "import qualified " . showString (moduleName f) . showString "\n"+driverWithFormatter :: String -> ShowS+driverWithFormatter f =+ showString "import qualified " . showString (moduleName f) . showString "\n" . showString "main :: IO ()\n" . showString "main = hspecWithFormatter " . showString f . showString " $ " @@ -89,7 +92,7 @@ importList = foldr (.) "" . map f where f :: Spec -> ShowS- f name = "import qualified " . showString name . "Spec\n"+ f spec = "import qualified " . showString (specModule spec) . "Spec\n" -- | Combine a list of strings with (>>). sequenceS :: [ShowS] -> ShowS@@ -103,19 +106,18 @@ -- | Convert a spec to code. formatSpec :: Spec -> ShowS-formatSpec name = "describe " . shows name . " " . showString name . "Spec.spec"+formatSpec (Spec file name) = "postProcessSpec " . shows file . " (describe " . shows name . " " . showString name . "Spec.spec)" findSpecs :: FilePath -> IO [Spec] findSpecs src = do let (dir, file) = splitFileName src- mapMaybe fileToSpec . filter (/= file) <$> getFilesRecursive dir+ mapMaybe (fileToSpec dir) . filter (/= file) <$> getFilesRecursive dir -fileToSpec :: FilePath -> Maybe String-fileToSpec f = intercalate "." . reverse <$> case reverse $ splitDirectories f of+fileToSpec :: FilePath -> FilePath -> Maybe Spec+fileToSpec dir file = case reverse $ splitDirectories file of x:xs -> case stripSuffix "Spec.hs" x <|> stripSuffix "Spec.lhs" x of- Nothing -> Nothing- Just "" -> Nothing- Just ys -> Just (ys : xs)+ Just name | (not . null) name -> Just . Spec (dir </> file) $ (intercalate "." . reverse) (name : xs)+ _ -> Nothing _ -> Nothing where stripSuffix :: Eq a => [a] -> [a] -> Maybe [a]
hspec-discover/test/RunSpec.hs view
@@ -8,7 +8,8 @@ import System.FilePath import Data.List (intercalate, sort) -import Run+import Run hiding (Spec)+import qualified Run main :: IO () main = hspec spec@@ -27,20 +28,28 @@ it "generates test driver" $ withTempFile $ \f -> do run ["hspec-discover/test-data/nested-spec/Spec.hs", "", f] readFile f `shouldReturn` unlines [- "{-# LINE 1 \"hspec-discover/test-data/nested-spec/Spec.hs\" #-}module Main where"+ "{-# LINE 1 \"hspec-discover/test-data/nested-spec/Spec.hs\" #-}"+ , "{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}"+ , "module Main where" , "import qualified Foo.Bar.BazSpec" , "import qualified Foo.BarSpec" , "import qualified FooSpec"- , "import Test.Hspec"+ , "import Test.Hspec.Discover" , "main :: IO ()"- , "main = hspec $ describe \"Foo.Bar.Baz\" Foo.Bar.BazSpec.spec >> describe \"Foo.Bar\" Foo.BarSpec.spec >> describe \"Foo\" FooSpec.spec"+ , "main = hspec $" ++ unwords [+ " postProcessSpec \"hspec-discover/test-data/nested-spec/Foo/Bar/BazSpec.hs\" (describe \"Foo.Bar.Baz\" Foo.Bar.BazSpec.spec)"+ , ">> postProcessSpec \"hspec-discover/test-data/nested-spec/Foo/BarSpec.hs\" (describe \"Foo.Bar\" Foo.BarSpec.spec)"+ , ">> postProcessSpec \"hspec-discover/test-data/nested-spec/FooSpec.hs\" (describe \"Foo\" FooSpec.spec)"+ ] ] it "generates test driver for an empty directory" $ withTempFile $ \f -> do run ["hspec-discover/test-data/empty-dir/Spec.hs", "", f] readFile f `shouldReturn` unlines [- "{-# LINE 1 \"hspec-discover/test-data/empty-dir/Spec.hs\" #-}module Main where"- , "import Test.Hspec"+ "{-# LINE 1 \"hspec-discover/test-data/empty-dir/Spec.hs\" #-}"+ , "{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}"+ , "module Main where"+ , "import Test.Hspec.Discover" , "main :: IO ()" , "main = hspec $ return ()" ]@@ -56,34 +65,34 @@ describe "fileToSpec" $ do it "converts path to spec name" $ do- fileToSpec "FooSpec.hs" `shouldBe` Just "Foo"+ fileToSpec "" "FooSpec.hs" `shouldBe` Just (spec_ "FooSpec.hs" "Foo") it "rejects spec with empty name" $ do- fileToSpec "Spec.hs" `shouldBe` Nothing+ fileToSpec "" "Spec.hs" `shouldBe` Nothing it "works for lhs files" $ do- fileToSpec "FooSpec.lhs" `shouldBe` Just "Foo"+ fileToSpec "" "FooSpec.lhs" `shouldBe` Just (spec_ "FooSpec.lhs" "Foo") it "returns Nothing for invalid spec name" $ do- fileToSpec "foo" `shouldBe` Nothing+ fileToSpec "" "foo" `shouldBe` Nothing context "when path has directory component" $ do it "converts path to spec name" $ do- fileToSpec ("Foo" </> "Bar" </> "BazSpec.hs") `shouldBe` Just "Foo.Bar.Baz"+ let file = "Foo" </> "Bar" </> "BazSpec.hs"+ fileToSpec "" file `shouldBe` Just (spec_ file "Foo.Bar.Baz") it "rejects spec with empty name" $ do- fileToSpec ("Foo" </> "Bar" </> "Spec.hs") `shouldBe` Nothing+ fileToSpec "" ("Foo" </> "Bar" </> "Spec.hs") `shouldBe` Nothing describe "findSpecs" $ do it "finds specs" $ do- findSpecs "hspec-discover/test-data/nested-spec/Spec.hs" `shouldReturn` ["Foo.Bar.Baz","Foo.Bar","Foo"]+ let dir = "hspec-discover/test-data/nested-spec"+ findSpecs (dir </> "Spec.hs") `shouldReturn` [spec_ (dir </> "Foo/Bar/BazSpec.hs") "Foo.Bar.Baz", spec_ (dir </> "Foo/BarSpec.hs") "Foo.Bar", spec_ (dir </> "FooSpec.hs") "Foo"] describe "driverWithFormatter" $ do it "generates a test driver that uses a custom formatter" $ do- driverWithFormatter False "Some.Module.formatter" "" `shouldBe` intercalate "\n" [- "import Test.Hspec"- , "import Test.Hspec.Runner"- , "import qualified Some.Module"+ driverWithFormatter "Some.Module.formatter" "" `shouldBe` intercalate "\n" [+ "import qualified Some.Module" , "main :: IO ()" , "main = hspecWithFormatter Some.Module.formatter $ " ]@@ -94,7 +103,9 @@ describe "importList" $ do it "generates imports for a list of specs" $ do- importList ["Foo", "Bar"] "" `shouldBe` unlines [+ importList [spec_ "FooSpec.hs" "Foo", spec_ "BarSpec.hs" "Bar"] "" `shouldBe` unlines [ "import qualified FooSpec" , "import qualified BarSpec" ]+ where+ spec_ = Run.Spec
hspec.cabal view
@@ -1,5 +1,5 @@ name: hspec-version: 1.11.4+version: 1.12.0 license: MIT license-file: LICENSE copyright: (c) 2011-2014 Simon Hengel,@@ -66,6 +66,7 @@ Test.Hspec.Formatters Test.Hspec.HUnit Test.Hspec.QuickCheck+ Test.Hspec.Discover other-modules: Test.Hspec.Util Test.Hspec.Compat@@ -104,10 +105,12 @@ other-modules: Mock Helper+ HelperSpec Test.HspecSpec Test.Hspec.CompatSpec Test.Hspec.Core.QuickCheckUtilSpec Test.Hspec.Core.TypeSpec+ Test.Hspec.DiscoverSpec Test.Hspec.FailureReportSpec Test.Hspec.FormattersSpec Test.Hspec.HUnitSpec@@ -134,8 +137,10 @@ , hspec-expectations , async - , hspec-meta == 1.11.4+ , hspec-meta == 1.12.0 , process+ , directory+ , stringbuilder , ghc-paths test-suite example
src/Test/Hspec.hs view
@@ -25,6 +25,7 @@ , before , beforeAll , after+, afterAll , around , parallel , runIO@@ -44,7 +45,7 @@ -- | Combine a list of specs into a larger spec. describe :: String -> Spec -> Spec-describe label action = fromSpecList [Core.describe label [BuildSpecs $ runSpecM action]]+describe label spec = runIO (runSpecM spec) >>= fromSpecList . return . Core.describe label -- | An alias for `describe`. context :: String -> Spec -> Spec@@ -83,7 +84,7 @@ example :: Expectation -> Expectation example = id --- | Run examples of given spec in parallel.+-- | Run spec items of given `Spec` in parallel. parallel :: Spec -> Spec parallel = mapSpecItem $ \item -> item {itemIsParallelizable = True} @@ -91,14 +92,12 @@ before :: IO () -> Spec -> Spec before action = around (action >>) --- | Run a custom action before all spec items.+-- | Run a custom action before the first spec item. beforeAll :: IO () -> Spec -> Spec-beforeAll action = fromSpecList . return . BuildSpecs . go- where- go spec = do- mvar <- newMVar Nothing- let action_ = memoize mvar action- runSpecM $ before action_ spec+beforeAll action spec = do+ mvar <- runIO (newMVar Nothing)+ let action_ = memoize mvar action+ before action_ spec memoize :: MVar (Maybe a) -> IO a -> IO a memoize mvar action = modifyMVar mvar $ \ma -> case ma of@@ -110,6 +109,10 @@ -- | Run a custom action after every spec item. after :: IO () -> Spec -> Spec after action = around (`finally` action)++-- | Run a custom action after the last spec item.+afterAll :: IO () -> Spec -> Spec+afterAll action spec = runIO (runSpecM spec) >>= fromSpecList . return . SpecWithCleanup action -- | Run a custom action before and/or after every spec item. around :: (IO () -> IO ()) -> Spec -> Spec
src/Test/Hspec/Config.hs view
@@ -1,5 +1,6 @@ module Test.Hspec.Config ( Config (..)+, ColorMode(..) , defaultConfig , getConfig , configAddFilter@@ -7,55 +8,15 @@ ) where import Control.Applicative-import Data.List-import Data.Maybe import System.IO import System.Exit import qualified Test.QuickCheck as QC-import Test.Hspec.Formatters import Test.Hspec.Util import Test.Hspec.Options import Test.Hspec.FailureReport import Test.Hspec.Core.QuickCheckUtil (mkGen) -data Config = Config {- configDryRun :: Bool-, configPrintCpuTime :: Bool-, configFastFail :: Bool---- |--- A predicate that is used to filter the spec before it is run. Only examples--- that satisfy the predicate are run.-, configFilterPredicate :: Maybe (Path -> Bool)-, configQuickCheckSeed :: Maybe Integer-, configQuickCheckMaxSuccess :: Maybe Int-, configQuickCheckMaxDiscardRatio :: Maybe Int-, configQuickCheckMaxSize :: Maybe Int-, configSmallCheckDepth :: Int-, configColorMode :: ColorMode-, configFormatter :: Formatter-, configHtmlOutput :: Bool-, configHandle :: Either Handle FilePath-}--defaultConfig :: Config-defaultConfig = Config {- configDryRun = False-, configPrintCpuTime = False-, configFastFail = False-, configFilterPredicate = Nothing-, configQuickCheckSeed = Nothing-, configQuickCheckMaxSuccess = Nothing-, configQuickCheckMaxDiscardRatio = Nothing-, configQuickCheckMaxSize = Nothing-, configSmallCheckDepth = 5-, configColorMode = ColorAuto-, configFormatter = specdoc-, configHtmlOutput = False-, configHandle = Left stdout-}- -- | Add a filter predicate to config. If there is already a filter predicate, -- then combine them with `||`. configAddFilter :: (Path -> Bool) -> Config -> Config@@ -63,37 +24,22 @@ configFilterPredicate = Just p1 `filterOr` configFilterPredicate c } -filterOr :: Maybe (Path -> Bool) -> Maybe (Path -> Bool) -> Maybe (Path -> Bool)-filterOr p1_ p2_ = case (p1_, p2_) of- (Just p1, Just p2) -> Just $ \path -> p1 path || p2 path- _ -> p1_ <|> p2_--mkConfig :: Maybe FailureReport -> Options -> Config-mkConfig mFailureReport opts = Config {- configDryRun = optionsDryRun opts- , configPrintCpuTime = optionsPrintCpuTime opts- , configFastFail = optionsFastFail opts- , configFilterPredicate = matchFilter `filterOr` rerunFilter+mkConfig :: Maybe FailureReport -> Config -> Config+mkConfig mFailureReport opts = opts {+ configFilterPredicate = matchFilter `filterOr` rerunFilter , configQuickCheckSeed = mSeed , configQuickCheckMaxSuccess = mMaxSuccess , configQuickCheckMaxDiscardRatio = mMaxDiscardRatio , configQuickCheckMaxSize = mMaxSize- , configSmallCheckDepth = fromMaybe (configSmallCheckDepth defaultConfig) (optionsDepth opts)- , configColorMode = optionsColorMode opts- , configFormatter = optionsFormatter opts- , configHtmlOutput = optionsHtmlOutput opts- , configHandle = maybe (configHandle defaultConfig) Right (optionsOutputFile opts) } where - mSeed = optionsSeed opts <|> (failureReportSeed <$> mFailureReport)- mMaxSuccess = optionsMaxSuccess opts <|> (failureReportMaxSuccess <$> mFailureReport)- mMaxSize = optionsMaxSize opts <|> (failureReportMaxSize <$> mFailureReport)- mMaxDiscardRatio = optionsMaxDiscardRatio opts <|> (failureReportMaxDiscardRatio <$> mFailureReport)+ mSeed = configQuickCheckSeed opts <|> (failureReportSeed <$> mFailureReport)+ mMaxSuccess = configQuickCheckMaxSuccess opts <|> (failureReportMaxSuccess <$> mFailureReport)+ mMaxSize = configQuickCheckMaxSize opts <|> (failureReportMaxSize <$> mFailureReport)+ mMaxDiscardRatio = configQuickCheckMaxDiscardRatio opts <|> (failureReportMaxDiscardRatio <$> mFailureReport) - matchFilter = case optionsMatch opts of- [] -> Nothing- xs -> Just $ foldl1' (\p0 p1 path -> p0 path || p1 path) (map filterPredicate xs)+ matchFilter = configFilterPredicate opts rerunFilter = flip elem . failureReportPaths <$> mFailureReport @@ -118,12 +64,12 @@ setSeed :: Integer -> QC.Args -> QC.Args setSeed n args = args {QC.replay = Just (mkGen (fromIntegral n), 0)} -getConfig :: Options -> String -> [String] -> IO Config+getConfig :: Config -> String -> [String] -> IO Config getConfig opts_ prog args = do case parseOptions opts_ prog args of Left (err, msg) -> exitWithMessage err msg Right opts -> do- r <- if optionsRerun opts then readFailureReport else return Nothing+ r <- if configRerun opts then readFailureReport else return Nothing return (mkConfig r opts) exitWithMessage :: ExitCode -> String -> IO a
src/Test/Hspec/Core.hs view
@@ -19,7 +19,10 @@ -- * Internal representation of a spec tree , SpecTree (..)+, mapSpecTree , Item (..)+, Location (..)+, LocationAccuracy(..) , mapSpecItem , modifyParams , describe
src/Test/Hspec/Core/Type.hs view
@@ -5,8 +5,11 @@ , runSpecM , fromSpecList , SpecTree (..)+, mapSpecTree , Item (..) , mapSpecItem+, Location (..)+, LocationAccuracy(..) , Example (..) , Result (..) , Params (..)@@ -89,23 +92,38 @@ -- | Internal representation of a spec. data SpecTree = SpecGroup String [SpecTree]- | BuildSpecs (IO [SpecTree])- | SpecItem String Item+ | SpecWithCleanup (IO ()) [SpecTree]+ | SpecItem Item data Item = Item {- itemIsParallelizable :: Bool+ itemRequirement :: String+, itemLocation :: Maybe Location+, itemIsParallelizable :: Bool , itemExample :: Params -> (IO () -> IO ()) -> ProgressCallback -> IO Result } +mapSpecTree :: (SpecTree -> SpecTree) -> Spec -> Spec+mapSpecTree f spec = runIO (runSpecM spec) >>= fromSpecList . map f+ mapSpecItem :: (Item -> Item) -> Spec -> Spec-mapSpecItem f = fromSpecList . return . BuildSpecs . fmap (map go) . runSpecM+mapSpecItem f = mapSpecTree go where go :: SpecTree -> SpecTree go spec = case spec of- SpecItem r item -> SpecItem r (f item)- BuildSpecs es -> BuildSpecs (map go <$> es)- SpecGroup d es -> SpecGroup d (map go es)+ SpecGroup d xs -> SpecGroup d (map go xs)+ SpecWithCleanup cleanup xs -> SpecWithCleanup cleanup (map go xs)+ SpecItem item -> SpecItem (f item) +data LocationAccuracy = ExactLocation | BestEffort+ deriving (Eq, Show)++data Location = Location {+ locationFile :: String+, locationLine :: Int+, locationColumn :: Int+, locationAccuracy :: LocationAccuracy+} deriving (Eq, Show)+ -- | The @describe@ function combines a list of specs into a larger spec. describe :: String -> [SpecTree] -> SpecTree describe s = SpecGroup msg@@ -116,9 +134,9 @@ -- | Create a spec item. it :: Example a => String -> a -> SpecTree-it s e = SpecItem msg $ Item False (evaluateExample e)+it s e = SpecItem $ Item requirement Nothing False (evaluateExample e) where- msg+ requirement | null s = "(unspecified behavior)" | otherwise = s
+ src/Test/Hspec/Discover.hs view
@@ -0,0 +1,82 @@+module Test.Hspec.Discover {-# WARNING+ "This module is used by @hspec-discover@. It is not part of the public API and may change at any time."+ #-} (Spec, hspec, hspecWithFormatter, postProcessSpec, describe) where++import Control.Applicative+import Data.Maybe+import Data.List+import Data.Traversable hiding (mapM)+import Control.Monad.Trans.State++import Test.Hspec+import Test.Hspec.Core.Type hiding (describe)+import Test.Hspec.Runner+import Test.Hspec.Runner.Tree+import Test.Hspec.Formatters+import Test.Hspec.Util++hspecWithFormatter :: IsFormatter a => a -> Spec -> IO ()+hspecWithFormatter formatter spec = do+ f <- toFormatter formatter+ hspecWith defaultConfig {configFormatter = Just f} spec++postProcessSpec :: FilePath -> Spec -> Spec+postProcessSpec = locationHeuristicFromFile++locationHeuristicFromFile :: FilePath -> Spec -> Spec+locationHeuristicFromFile file spec = do+ mInput <- either (const Nothing) Just <$> (runIO . safeTry . readFile) file+ let lookupLoc = maybe (\_ _ _ -> Nothing) (lookupLocation file) mInput+ runIO (toTree spec) >>= fromTree . addLoctions lookupLoc++addLoctions :: (Int -> Int -> String -> Maybe Location) -> [Tree Item] -> [Tree Item]+addLoctions lookupLoc = map (fmap f) . enumerate+ where+ f :: ((Int, Int), Item) -> Item+ f ((n, total), item) = item {itemLocation = itemLocation item <|> lookupLoc n total (itemRequirement item)}++type EnumerateM = State [(String, Int)]++enumerate :: [Tree Item] -> [Tree ((Int, Int), Item)]+enumerate tree = (mapM (traverse addPosition) tree >>= mapM (traverse addTotal)) `evalState` []+ where+ addPosition :: Item -> EnumerateM (Int, Item)+ addPosition item = (,) <$> getOccurrence (itemRequirement item) <*> pure item++ addTotal :: (Int, Item) -> EnumerateM ((Int, Int), Item)+ addTotal (n, item) = do+ total <- getTotal (itemRequirement item)+ return ((n, total), item)++ getTotal :: String -> EnumerateM Int+ getTotal requirement = do+ gets $ fromMaybe err . lookup requirement+ where+ err = error ("Test.Hspec.Discover.getTotal: No entry for requirement " ++ show requirement ++ "!")++ getOccurrence :: String -> EnumerateM Int+ getOccurrence requirement = do+ xs <- get+ let n = maybe 1 succ (lookup requirement xs)+ put ((requirement, n) : filter ((/= requirement) . fst) xs)+ return n++lookupLocation :: FilePath -> String -> Int -> Int -> String -> Maybe Location+lookupLocation file input n total requirement = loc+ where+ loc :: Maybe Location+ loc = Location file <$> line <*> pure 0 <*> pure BestEffort++ line :: Maybe Int+ line = case occurrences of+ xs | length xs == total -> Just (xs !! pred n)+ _ -> Nothing++ occurrences :: [Int]+ occurrences = map fst (filter p inputLines)+ where+ p :: (Int, String) -> Bool+ p = isInfixOf (show requirement) . snd++ inputLines :: [(Int, String)]+ inputLines = zip [1..] (lines input)
src/Test/Hspec/Formatters.hs view
@@ -41,6 +41,7 @@ , newParagraph -- ** Dealing with colors+, withInfoColor , withSuccessColor , withPendingColor , withFailColor@@ -61,8 +62,10 @@ import Data.Maybe import Test.Hspec.Util+import Test.Hspec.Core.Type (Location(..), LocationAccuracy(..)) import Text.Printf-import Control.Monad (unless, forM_)+import Control.Monad (when, unless)+import Data.Foldable (forM_) import Control.Applicative import System.IO (hPutStr, hFlush) @@ -91,6 +94,7 @@ , writeLine , newParagraph + , withInfoColor , withSuccessColor , withPendingColor , withFailColor@@ -108,7 +112,7 @@ silent :: Formatter silent = Formatter { headerFormatter = return ()-, exampleGroupStarted = \_ _ _ -> return ()+, exampleGroupStarted = \_ _ -> return () , exampleGroupDone = return () , exampleProgress = \_ _ _ -> return () , exampleSucceeded = \_ -> return ()@@ -125,17 +129,9 @@ headerFormatter = do writeLine "" -, exampleGroupStarted = \n nesting name -> do-- -- separate groups with an empty line- unless (n == 0) $ do- newParagraph-+, exampleGroupStarted = \nesting name -> do writeLine (indentationFor nesting ++ name) -, exampleGroupDone = do- newParagraph- , exampleProgress = \h _ p -> do hPutStr h (formatProgress p) hFlush h@@ -178,26 +174,45 @@ defaultFailedFormatter :: FormatM () defaultFailedFormatter = do- newParagraph+ writeLine "" failures <- getFailMessages forM_ (zip [1..] failures) $ \x -> do formatFailure x writeLine ""++ when (hasBestEffortLocations failures) $ do+ withInfoColor $ writeLine "Source locations marked with \"best-effort\" are calculated heuristically and may be incorrect."+ writeLine ""+ unless (null failures) $ do write "Randomized with seed " >> usedSeed >>= writeLine . show writeLine "" where+ hasBestEffortLocations :: [FailureRecord] -> Bool+ hasBestEffortLocations = any p+ where+ p :: FailureRecord -> Bool+ p failure = (locationAccuracy <$> failureRecordLocation failure) == Just BestEffort+ formatFailure :: (Int, FailureRecord) -> FormatM ()- formatFailure (n, FailureRecord path reason) = do+ formatFailure (n, FailureRecord mLoc path reason) = do write (show n ++ ") ") writeLine (formatRequirement path) withFailColor $ do unless (null err) $ do writeLine err+ forM_ mLoc $ \loc -> do+ writeLine ""+ withInfoColor $ writeLine (formatLoc loc) where err = either (("uncaught exception: " ++) . formatException) id reason+ formatLoc (Location file line _column accuracy) = "# " ++ file ++ ":" ++ show line ++ bestEffortMarking+ where+ bestEffortMarking = case accuracy of+ ExactLocation -> ""+ BestEffort -> " (best-effort)" defaultFooter :: FormatM () defaultFooter = do
src/Test/Hspec/Formatters/Internal.hs view
@@ -21,6 +21,7 @@ , writeLine , newParagraph +, withInfoColor , withSuccessColor , withPendingColor , withFailColor@@ -36,7 +37,7 @@ import qualified System.IO as IO import System.IO (Handle)-import Control.Monad (when, unless)+import Control.Monad import Control.Applicative import Control.Exception (SomeException, AsyncException(..), bracket_, try, throwIO) import System.Console.ANSI@@ -47,7 +48,7 @@ import Test.Hspec.Util (Path) import Test.Hspec.Compat-import Test.Hspec.Core.Type (Progress)+import Test.Hspec.Core.Type (Progress, Location) -- | A lifted version of `Control.Monad.Trans.State.gets` gets :: (FormatterState -> a) -> FormatM a@@ -63,7 +64,6 @@ stateHandle :: Handle , stateUseColor :: Bool , produceHTML :: Bool-, lastIsEmptyLine :: Bool -- True, if last line was empty , successCount :: Int , pendingCount :: Int , failCount :: Int@@ -90,7 +90,7 @@ runFormatM useColor produceHTML_ printCpuTime seed handle (FormatM action) = do time <- getPOSIXTime cpuTime <- if printCpuTime then Just <$> CPUTime.getCPUTime else pure Nothing- st <- newIORef (FormatterState handle useColor produceHTML_ False 0 0 0 [] seed cpuTime time)+ st <- newIORef (FormatterState handle useColor produceHTML_ 0 0 0 [] seed cpuTime time) evalStateT action st -- | Increase the counter for successful examples@@ -122,15 +122,16 @@ getTotalCount = gets totalCount -- | Append to the list of accumulated failure messages.-addFailMessage :: Path -> Either SomeException String -> FormatM ()-addFailMessage p m = modify $ \s -> s {failMessages = FailureRecord p m : failMessages s}+addFailMessage :: Maybe Location -> Path -> Either SomeException String -> FormatM ()+addFailMessage loc p m = modify $ \s -> s {failMessages = FailureRecord loc p m : failMessages s} -- | Get the list of accumulated failure messages. getFailMessages :: FormatM [FailureRecord] getFailMessages = reverse `fmap` gets failMessages data FailureRecord = FailureRecord {- failureRecordPath :: Path+ failureRecordLocation :: Maybe Location+, failureRecordPath :: Path , failureRecordMessage :: Either SomeException String } @@ -141,7 +142,7 @@ -- | evaluated before each test group -- -- The given number indicates the position within the parent group.-, exampleGroupStarted :: Int -> [String] -> String -> FormatM ()+, exampleGroupStarted :: [String] -> String -> FormatM () , exampleGroupDone :: FormatM () @@ -171,21 +172,14 @@ -- -- Calling this multiple times has the same effect as calling it once. newParagraph :: FormatM ()-newParagraph = do- f <- gets lastIsEmptyLine- unless f $ do- writeLine ""- setLastIsEmptyLine True--setLastIsEmptyLine :: Bool -> FormatM ()-setLastIsEmptyLine f = modify $ \s -> s {lastIsEmptyLine = f}+newParagraph = writeLine ""+{-# DEPRECATED newParagraph "use `writeLine \"\"` instead" #-} -- | Append some output to the report. write :: String -> FormatM () write s = do h <- gets stateHandle liftIO $ IO.hPutStr h s- setLastIsEmptyLine False -- | The same as `write`, but adds a newline character. writeLine :: String -> FormatM ()@@ -205,6 +199,11 @@ -- default color. withPendingColor :: FormatM a -> FormatM a withPendingColor = withColor (SetColor Foreground Dull Yellow) "hspec-pending"++-- | Set output color to cyan, run given action, and finally restore the+-- default color.+withInfoColor :: FormatM a -> FormatM a+withInfoColor = withColor (SetColor Foreground Dull Cyan) "hspec-info" -- | Set a color, run an action, and finally reset colors. withColor :: SGR -> String -> FormatM a -> FormatM a
src/Test/Hspec/Options.hs view
@@ -1,14 +1,14 @@ module Test.Hspec.Options (- Options (..)+ Config(..) , ColorMode (..)-, defaultOptions+, defaultConfig+, filterOr , parseOptions---- exported to silence warnings-, Arg (..) ) where +import Control.Applicative import Data.List+import System.IO import System.Exit import System.Console.GetOpt @@ -16,47 +16,71 @@ import Test.Hspec.Compat import Test.Hspec.Util -data Options = Options {- optionsDryRun :: Bool-, optionsPrintCpuTime :: Bool-, optionsRerun :: Bool-, optionsFastFail :: Bool-, optionsMatch :: [String]-, optionsMaxSuccess :: Maybe Int-, optionsDepth :: Maybe Int-, optionsSeed :: Maybe Integer-, optionsMaxSize :: Maybe Int-, optionsMaxDiscardRatio :: Maybe Int-, optionsColorMode :: ColorMode-, optionsFormatter :: Formatter-, optionsHtmlOutput :: Bool-, optionsOutputFile :: Maybe FilePath+data Config = Config {+ configDryRun :: Bool+, configPrintCpuTime :: Bool+, configFastFail :: Bool++-- |+-- A predicate that is used to filter the spec before it is run. Only examples+-- that satisfy the predicate are run.+, configRerun :: Bool+, configFilterPredicate :: Maybe (Path -> Bool)+, configQuickCheckSeed :: Maybe Integer+, configQuickCheckMaxSuccess :: Maybe Int+, configQuickCheckMaxDiscardRatio :: Maybe Int+, configQuickCheckMaxSize :: Maybe Int+, configSmallCheckDepth :: Int+, configColorMode :: ColorMode+, configFormatter :: Maybe Formatter+, configHtmlOutput :: Bool+, configOutputFile :: Either Handle FilePath } -addMatch :: String -> Options -> Options-addMatch s c = c {optionsMatch = s : optionsMatch c}+defaultConfig :: Config+defaultConfig = Config {+ configDryRun = False+, configPrintCpuTime = False+, configFastFail = False+, configRerun = False+, configFilterPredicate = Nothing+, configQuickCheckSeed = Nothing+, configQuickCheckMaxSuccess = Nothing+, configQuickCheckMaxDiscardRatio = Nothing+, configQuickCheckMaxSize = Nothing+, configSmallCheckDepth = 5+, configColorMode = ColorAuto+, configFormatter = Nothing+, configHtmlOutput = False+, configOutputFile = Left stdout+} -setDepth :: Int -> Options -> Options-setDepth n c = c {optionsDepth = Just n}+filterOr :: Maybe (Path -> Bool) -> Maybe (Path -> Bool) -> Maybe (Path -> Bool)+filterOr p1_ p2_ = case (p1_, p2_) of+ (Just p1, Just p2) -> Just $ \path -> p1 path || p2 path+ _ -> p1_ <|> p2_ -setMaxSuccess :: Int -> Options -> Options-setMaxSuccess n c = c {optionsMaxSuccess = Just n}+addMatch :: String -> Config -> Config+addMatch s c = c {configFilterPredicate = Just (filterPredicate s) `filterOr` configFilterPredicate c} -setMaxSize :: Int -> Options -> Options-setMaxSize n c = c {optionsMaxSize = Just n}+setDepth :: Int -> Config -> Config+setDepth n c = c {configSmallCheckDepth = n} -setMaxDiscardRatio :: Int -> Options -> Options-setMaxDiscardRatio n c = c {optionsMaxDiscardRatio = Just n}+setMaxSuccess :: Int -> Config -> Config+setMaxSuccess n c = c {configQuickCheckMaxSuccess = Just n} -setSeed :: Integer -> Options -> Options-setSeed n c = c {optionsSeed = Just n}+setMaxSize :: Int -> Config -> Config+setMaxSize n c = c {configQuickCheckMaxSize = Just n} +setMaxDiscardRatio :: Int -> Config -> Config+setMaxDiscardRatio n c = c {configQuickCheckMaxDiscardRatio = Just n}++setSeed :: Integer -> Config -> Config+setSeed n c = c {configQuickCheckSeed = Just n}+ data ColorMode = ColorAuto | ColorNever | ColorAlways deriving (Eq, Show) -defaultOptions :: Options-defaultOptions = Options False False False False [] Nothing Nothing Nothing Nothing Nothing ColorAuto specdoc False Nothing- formatters :: [(String, Formatter)] formatters = [ ("specdoc", specdoc)@@ -68,14 +92,14 @@ formatHelp :: String formatHelp = unlines (addLineBreaks "use a custom formatter; this can be one of:" ++ map ((" " ++) . fst) formatters) -type Result = Either NoConfig Options+type Result = Either NoConfig Config data NoConfig = Help | InvalidArgument String String data Arg a = Arg {- argumentName :: String-, argumentParser :: String -> Maybe a-, argumentSetter :: a -> Options -> Options+ _argumentName :: String+, _argumentParser :: String -> Maybe a+, _argumentSetter :: a -> Config -> Config } mkOption :: [Char] -> String -> Arg a -> String -> OptDescr (Result -> Result)@@ -113,18 +137,18 @@ readFormatter :: String -> Maybe Formatter readFormatter = (`lookup` formatters) - setFormatter :: Formatter -> Options -> Options- setFormatter f c = c {optionsFormatter = f}+ setFormatter :: Formatter -> Config -> Config+ setFormatter f c = c {configFormatter = Just f} - setOutputFile :: String -> Options -> Options- setOutputFile file c = c {optionsOutputFile = Just file}+ setOutputFile :: String -> Config -> Config+ setOutputFile file c = c {configOutputFile = Right file} - setPrintCpuTime x = x >>= \c -> return c {optionsPrintCpuTime = True}- setDryRun x = x >>= \c -> return c {optionsDryRun = True}- setFastFail x = x >>= \c -> return c {optionsFastFail = True}- setRerun x = x >>= \c -> return c {optionsRerun = True}- setNoColor x = x >>= \c -> return c {optionsColorMode = ColorNever}- setColor x = x >>= \c -> return c {optionsColorMode = ColorAlways}+ setPrintCpuTime x = x >>= \c -> return c {configPrintCpuTime = True}+ setDryRun x = x >>= \c -> return c {configDryRun = True}+ setFastFail x = x >>= \c -> return c {configFastFail = True}+ setRerun x = x >>= \c -> return c {configRerun = True}+ setNoColor x = x >>= \c -> return c {configColorMode = ColorNever}+ setColor x = x >>= \c -> return c {configColorMode = ColorAlways} undocumentedOptions :: [OptDescr (Result -> Result)] undocumentedOptions = [@@ -140,9 +164,9 @@ ] where setHtml :: Result -> Result- setHtml x = x >>= \c -> return c {optionsHtmlOutput = True}+ setHtml x = x >>= \c -> return c {configHtmlOutput = True} -parseOptions :: Options -> String -> [String] -> Either (ExitCode, String) Options+parseOptions :: Config -> String -> [String] -> Either (ExitCode, String) Config parseOptions c prog args = case getOpt Permute (options ++ undocumentedOptions) args of (opts, [], []) -> case foldl' (flip id) (Right c) opts of Left Help -> Left (ExitSuccess, usageInfo ("Usage: " ++ prog ++ " [OPTION]...\n\nOPTIONS") options)
src/Test/Hspec/Runner.hs view
@@ -3,8 +3,9 @@ module Test.Hspec.Runner ( -- * Running a spec hspec-, hspecResult , hspecWith+, hspecResult+, hspecWithResult -- * Types , Summary (..)@@ -13,9 +14,6 @@ , Path , defaultConfig , configAddFilter---- * Internals-, hspecWithFormatter ) where import Control.Monad@@ -40,35 +38,50 @@ import Test.Hspec.FailureReport import Test.Hspec.Core.QuickCheckUtil -import Test.Hspec.Options (Options(..), ColorMode(..), defaultOptions) import Test.Hspec.Runner.Tree import Test.Hspec.Runner.Eval -- | Filter specs by given predicate. -- -- The predicate takes a list of "describe" labels and a "requirement".-filterSpecs :: (Path -> Bool) -> [Tree a] -> [Tree a]-filterSpecs p = goSpecs []+filterSpecs :: Config -> [Tree Item] -> [Tree Item]+filterSpecs c = go [] where- goSpecs groups = mapMaybe (goSpec groups)+ p :: Path -> Bool+ p = fromMaybe (const True) (configFilterPredicate c) + go :: [String] -> [Tree Item] -> [Tree Item]+ go groups = mapMaybe (goSpec groups)++ goSpecs :: [String] -> [Tree Item] -> ([Tree Item] -> a) -> Maybe a+ goSpecs groups specs ctor = case go groups specs of+ [] -> Nothing+ xs -> Just (ctor xs)++ goSpec :: [String] -> Tree Item -> Maybe (Tree Item) goSpec groups spec = case spec of- Leaf requirement _ -> guard (p (groups, requirement)) >> return spec- Node group specs -> case goSpecs (groups ++ [group]) specs of- [] -> Nothing- xs -> Just (Node group xs)+ Leaf item -> guard (p (groups, itemRequirement item)) >> return spec+ Node group specs -> goSpecs (groups ++ [group]) specs (Node group)+ NodeWithCleanup action specs -> goSpecs groups specs (NodeWithCleanup action) +applyDryRun :: Config -> [Tree Item] -> [Tree Item]+applyDryRun c+ | configDryRun c = map (removeCleanup . fmap markSuccess)+ | otherwise = id+ where+ markSuccess :: Item -> Item+ markSuccess item = item {itemExample = evaluateExample Success}++ removeCleanup :: Tree Item -> Tree Item+ removeCleanup spec = case spec of+ Node x xs -> Node x (map removeCleanup xs)+ NodeWithCleanup _ xs -> NodeWithCleanup (return ()) (map removeCleanup xs)+ leaf@(Leaf _) -> leaf+ -- | Run given spec and write a report to `stdout`. -- Exit with `exitFailure` if at least one spec item fails. hspec :: Spec -> IO ()-hspec = hspecWithOptions defaultOptions---- | This function is used by @hspec-discover@. It is not part of the public--- API and may change at any time.-hspecWithFormatter :: IsFormatter a => a -> Spec -> IO ()-hspecWithFormatter formatter spec = do- f <- toFormatter formatter- hspecWithOptions defaultOptions {optionsFormatter = f} spec+hspec = hspecWith defaultConfig -- Add a seed to given config if there is none. That way the same seed is used -- for all properties. This helps with --seed and --rerun.@@ -81,14 +94,10 @@ -- | Run given spec with custom options. -- This is similar to `hspec`, but more flexible.-hspecWithOptions :: Options -> Spec -> IO ()-hspecWithOptions opts spec = do- prog <- getProgName- args <- getArgs- c <- getConfig opts prog args- withArgs [] {- do not leak command-line arguments to examples -} $ do- r <- hspecWith c spec- unless (summaryFailures r == 0) exitFailure+hspecWith :: Config -> Spec -> IO ()+hspecWith conf spec = do+ r <- hspecWithResult conf spec+ unless (summaryFailures r == 0) exitFailure -- | Run given spec and returns a summary of the test run. --@@ -96,44 +105,45 @@ -- items. If you need this, you have to check the `Summary` yourself and act -- accordingly. hspecResult :: Spec -> IO Summary-hspecResult = hspecWith defaultConfig+hspecResult = hspecWithResult defaultConfig -- | Run given spec with custom options and returns a summary of the test run. ----- /Note/: `hspecWith` does not exit with `exitFailure` on failing spec+-- /Note/: `hspecWithResult` does not exit with `exitFailure` on failing spec -- items. If you need this, you have to check the `Summary` yourself and act -- accordingly.-hspecWith :: Config -> Spec -> IO Summary-hspecWith c_ spec_ = withHandle c_ $ \h -> do- c <- ensureSeed c_- let formatter = configFormatter c- seed = (fromJust . configQuickCheckSeed) c- qcArgs = configQuickCheckArgs c- spec- | configDryRun c = mapSpecItem markSuccess spec_- | otherwise = spec_+hspecWithResult :: Config -> Spec -> IO Summary+hspecWithResult conf spec = do+ prog <- getProgName+ args <- getArgs+ c <- getConfig conf prog args >>= ensureSeed+ withArgs [] {- do not leak command-line arguments to examples -} $ withHandle c $ \h -> do+ let formatter = fromMaybe specdoc (configFormatter c)+ seed = (fromJust . configQuickCheckSeed) c+ qcArgs = configQuickCheckArgs c - useColor <- doesUseColor h c- filteredSpec <- maybe id filterSpecs (configFilterPredicate c) <$> toTree spec+ useColor <- doesUseColor h c - withHiddenCursor useColor h $- runFormatM useColor (configHtmlOutput c) (configPrintCpuTime c) seed h $ do- runFormatter useColor h c formatter filteredSpec `finally_` do- failedFormatter formatter+ filteredSpec <- filterSpecs c . applyDryRun c <$> toTree spec - footerFormatter formatter+ withHiddenCursor useColor h $+ runFormatM useColor (configHtmlOutput c) (configPrintCpuTime c) seed h $ do+ runFormatter useColor h c formatter filteredSpec `finally_` do+ failedFormatter formatter - -- dump failure report- xs <- map failureRecordPath <$> getFailMessages- liftIO $ writeFailureReport FailureReport {- failureReportSeed = seed- , failureReportMaxSuccess = QC.maxSuccess qcArgs- , failureReportMaxSize = QC.maxSize qcArgs- , failureReportMaxDiscardRatio = QC.maxDiscardRatio qcArgs- , failureReportPaths = xs- }+ footerFormatter formatter - Summary <$> getTotalCount <*> getFailCount+ -- dump failure report+ xs <- map failureRecordPath <$> getFailMessages+ liftIO $ writeFailureReport FailureReport {+ failureReportSeed = seed+ , failureReportMaxSuccess = QC.maxSuccess qcArgs+ , failureReportMaxSize = QC.maxSize qcArgs+ , failureReportMaxDiscardRatio = QC.maxDiscardRatio qcArgs+ , failureReportPaths = xs+ }++ Summary <$> getTotalCount <*> getFailCount where withHiddenCursor :: Bool -> Handle -> IO a -> IO a withHiddenCursor useColor h@@ -147,15 +157,12 @@ ColorAlways -> return True withHandle :: Config -> (Handle -> IO a) -> IO a- withHandle c action = case configHandle c of+ withHandle c action = case configOutputFile c of Left h -> action h Right path -> withFile path WriteMode action isDumb :: IO Bool isDumb = maybe False (== "dumb") <$> lookupEnv "TERM"--markSuccess :: Item -> Item-markSuccess item = item {itemExample = evaluateExample Success} -- | Summary of a test run. data Summary = Summary {
src/Test/Hspec/Runner/Eval.hs view
@@ -18,33 +18,29 @@ import Test.Hspec.Timer import Data.Time.Clock.POSIX -type EvalTree = Tree (ProgressCallback -> FormatResult -> IO (FormatM ()))+type EvalTree = Tree (String, Maybe Location, ProgressCallback -> FormatResult -> IO (FormatM ())) -- | Evaluate all examples of a given spec and produce a report. runFormatter :: Bool -> Handle -> Config -> Formatter -> [Tree Item] -> FormatM ()-runFormatter useColor h c formatter specs_ = do+runFormatter useColor h c formatter specs = do headerFormatter formatter chan <- liftIO newChan reportProgress <- liftIO mkReportProgress- run chan reportProgress c formatter specs+ run chan reportProgress c formatter (toEvalTree specs) where mkReportProgress :: IO (Path -> Progress -> IO ()) mkReportProgress | useColor = every 0.05 $ exampleProgress formatter h | otherwise = return $ \_ _ -> return () - specs = map (fmap (parallelize . fmap (applyNoOpAround . applyParams) . unwrapItem)) specs_-- unwrapItem :: Item -> (Bool, Params -> (IO () -> IO ()) -> ProgressCallback -> IO Result)- unwrapItem (Item isParallelizable e) = (isParallelizable, e)-- applyParams :: (Params -> a) -> a- applyParams = ($ params)+ toEvalTree :: [Tree Item] -> [EvalTree]+ toEvalTree = map (fmap f) where- params = Params (configQuickCheckArgs c) (configSmallCheckDepth c)+ f :: Item -> (String, Maybe Location, ProgressCallback -> FormatResult -> IO (FormatM ()))+ f (Item requirement loc isParallelizable e) = (requirement, loc, parallelize isParallelizable $ e params id) - applyNoOpAround :: ((IO () -> IO ()) -> b) -> b- applyNoOpAround = ($ id)+ params :: Params+ params = Params (configQuickCheckArgs c) (configSmallCheckDepth c) -- | Execute given action at most every specified number of seconds. every :: POSIXTime -> (a -> b -> IO ()) -> IO (a -> b -> IO ())@@ -56,8 +52,8 @@ type FormatResult = Either E.SomeException Result -> FormatM () -parallelize :: (Bool, ProgressCallback -> IO Result) -> ProgressCallback -> FormatResult -> IO (FormatM ())-parallelize (isParallelizable, e)+parallelize :: Bool -> (ProgressCallback -> IO Result) -> ProgressCallback -> FormatResult -> IO (FormatM ())+parallelize isParallelizable e | isParallelizable = runParallel e | otherwise = runSequentially e @@ -86,8 +82,8 @@ evalReport mvar ReportResult result -> formatResult result -replaceMVar :: MVar a -> a -> IO ()-replaceMVar mvar p = tryTakeMVar mvar >> putMVar mvar p+ replaceMVar :: MVar a -> a -> IO ()+ replaceMVar mvar p = tryTakeMVar mvar >> putMVar mvar p evalExample :: IO Result -> IO (Either E.SomeException Result) evalExample e = safeTry $ forceResult <$> e@@ -97,24 +93,35 @@ run :: Chan Message -> (Path -> ProgressCallback) -> Config -> Formatter -> [EvalTree] -> FormatM () run chan reportProgress_ c formatter specs = do liftIO $ do- forM_ (zip [0..] specs) (queueSpec [])+ forM_ specs (queueSpec []) writeChan chan Done processMessages (readChan chan) (configFastFail c) where defer :: FormatM () -> IO () defer = writeChan chan . Run - queueSpec :: [String] -> (Int, EvalTree) -> IO ()- queueSpec rGroups (n, Node group xs) = do- defer (exampleGroupStarted formatter n (reverse rGroups) group)- forM_ (zip [0..] xs) (queueSpec (group : rGroups))+ runCleanup :: IO () -> Path -> FormatM ()+ runCleanup action path = do+ r <- liftIO $ safeTry action+ either (failed Nothing path . Left) return r++ queueSpec :: [String] -> EvalTree -> IO ()+ queueSpec rGroups (Node group xs) = do+ defer (exampleGroupStarted formatter (reverse rGroups) group)+ forM_ xs (queueSpec (group : rGroups)) defer (exampleGroupDone formatter)- queueSpec rGroups (_, Leaf requirement e) =- queueExample (reverse rGroups, requirement) e+ queueSpec rGroups (NodeWithCleanup action xs) = do+ forM_ xs (queueSpec rGroups)+ defer (runCleanup action (reverse rGroups, "afterAll-hook"))+ queueSpec rGroups (Leaf e) =+ queueExample (reverse rGroups) e - queueExample :: Path -> (ProgressCallback -> FormatResult -> IO (FormatM ())) -> IO ()- queueExample path e = e reportProgress formatResult >>= defer+ queueExample :: [String] -> (String, Maybe Location, ProgressCallback -> FormatResult -> IO (FormatM ())) -> IO ()+ queueExample groups (requirement, loc, e) = e reportProgress formatResult >>= defer where+ path :: Path+ path = (groups, requirement)+ reportProgress = reportProgress_ path formatResult :: Either E.SomeException Result -> FormatM ()@@ -126,13 +133,13 @@ Right (Pending reason) -> do increasePendingCount examplePending formatter path reason- Right (Fail err) -> failed (Right err)- Left err -> failed (Left err)- where- failed err = do- increaseFailCount- addFailMessage path err- exampleFailed formatter path err+ Right (Fail err) -> failed loc path (Right err)+ Left err -> failed loc path (Left err)++ failed loc path err = do+ increaseFailCount+ addFailMessage loc path err+ exampleFailed formatter path err processMessages :: IO Message -> Bool -> FormatM () processMessages getMessage fastFail = go
src/Test/Hspec/Runner/Tree.hs view
@@ -1,18 +1,31 @@-{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-} module Test.Hspec.Runner.Tree where import Control.Applicative+import Data.Foldable (Foldable)+import Data.Traversable (Traversable) import Test.Hspec.Core.Type data Tree a = Node !String [Tree a]- | Leaf !String a- deriving (Eq, Show, Functor)+ | NodeWithCleanup (IO ()) [Tree a]+ | Leaf a+ deriving (Functor, Foldable, Traversable) toTree :: Spec -> IO [Tree Item]-toTree spec = concat <$> (runSpecM spec >>= mapM go)+toTree spec = map f <$> runSpecM spec where+ f :: SpecTree -> Tree Item+ f x = case x of+ SpecGroup label xs -> Node label (map f xs)+ SpecWithCleanup cleanup xs -> NodeWithCleanup cleanup (map f xs)+ SpecItem item -> Leaf item++fromTree :: [Tree Item] -> Spec+fromTree = fromSpecList . map go+ where+ go :: Tree Item -> SpecTree go x = case x of- SpecGroup label xs -> return . Node label . concat <$> mapM go xs- BuildSpecs xs -> concat <$> (xs >>= mapM go)- SpecItem r item -> return [Leaf r item]+ Node label xs -> SpecGroup label (map go xs)+ NodeWithCleanup action xs -> SpecWithCleanup action (map go xs)+ Leaf item -> SpecItem item
test/Helper.hs view
@@ -12,11 +12,14 @@ , ignoreExitCode , ignoreUserInterrupt+, throwException , shouldStartWith , shouldEndWith , shouldUseArgs++, withFileContent ) where import Data.List@@ -29,6 +32,8 @@ import Control.Concurrent import qualified Control.Exception as E import qualified System.Timeout as System+import System.IO+import System.Directory import Data.Time.Clock.POSIX import System.IO.Silently @@ -40,6 +45,9 @@ import qualified Test.Hspec.Runner as H import Test.Hspec.Core.QuickCheckUtil (mkGen) +throwException :: IO ()+throwException = E.throwIO (E.ErrorCall "foobar")+ ignoreExitCode :: IO () -> IO () ignoreExitCode action = action `E.catch` \e -> let _ = e :: ExitCode in return () @@ -84,3 +92,11 @@ H.it "foo" False (silence . ignoreExitCode . withArgs args . H.hspec) spec readIORef spy >>= (`shouldSatisfy` p)++withFileContent :: String -> (FilePath -> IO a) -> IO a+withFileContent input action = do+ dir <- getTemporaryDirectory+ (file, h) <- openTempFile dir "temp"+ hPutStr h input+ hClose h+ action file `E.finally` removeFile file
+ test/HelperSpec.hs view
@@ -0,0 +1,18 @@+module HelperSpec (main, spec) where++import Helper+import System.IO.Error (isDoesNotExistError)++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ describe "withFileContent" $ do+ it "creates a file with specified content and runs specified action" $ do+ withFileContent "foo" $ \file -> do+ readFile file `shouldReturn` "foo"++ it "removes file after action has been run" $ do+ file <- withFileContent "foo" return+ readFile file `shouldThrow` isDoesNotExistError
+ test/Test/Hspec/DiscoverSpec.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-deprecations #-}+module Test.Hspec.DiscoverSpec (main, spec) where++import Helper+import Data.String+import Data.String.Builder++import qualified Test.Hspec as H (it)+import Test.Hspec.Core (Item(..), Location(..), LocationAccuracy(..))+import Test.Hspec.Runner.Tree+import qualified Test.Hspec.Discover as H++infix 1 `shouldHaveLocation`++shouldHaveLocation :: Item -> (String, Int) -> Expectation+item `shouldHaveLocation` (src, line) = itemLocation item `shouldBe` Just (Location src line 0 BestEffort)++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ describe "postProcessSpec" $ do+ it "adds heuristic source locations" $ do+ let c = build $ do+ ""+ strlit "foo"+ ""+ strlit "bar"+ ""+ strlit "baz"+ withFileContent c $ \src -> do+ [Leaf item1, Leaf item2, Leaf item3] <- toTree . H.postProcessSpec src $ do+ H.it "foo" True+ H.it "bar" True+ H.it "baz" True+ item1 `shouldHaveLocation` (src, 2)+ item2 `shouldHaveLocation` (src, 4)+ item3 `shouldHaveLocation` (src, 6)++ context "when same requirement is used multiple times" $ do+ it "assigns locations sequentially" $ do+ let c = build $ do+ strlit "foo"+ strlit "foo"+ strlit "foo"+ withFileContent c $ \src -> do+ [Leaf item1, Leaf item2, Leaf item3] <- toTree . H.postProcessSpec src $ do+ H.it "foo" True+ H.it "foo" True+ H.it "foo" True+ item1 `shouldHaveLocation` (src, 1)+ item2 `shouldHaveLocation` (src, 2)+ item3 `shouldHaveLocation` (src, 3)++ context "when a requirement occurs more often in the spec tree than in the source file" $ do+ it "assigns Nothing" $ do+ let c = build $ do+ strlit "foo"+ strlit "foo"+ withFileContent c $ \src -> do+ [Leaf item1, Leaf item2, Leaf item3] <- toTree . H.postProcessSpec src $ do+ H.it "foo" True+ H.it "foo" True+ H.it "foo" True+ itemLocation item1 `shouldBe` Nothing+ itemLocation item2 `shouldBe` Nothing+ itemLocation item3 `shouldBe` Nothing+ where+ strlit :: String -> Builder+ strlit = fromString . show
test/Test/Hspec/FormattersSpec.hs view
@@ -4,7 +4,7 @@ import Helper import qualified Test.Hspec as H-import qualified Test.Hspec.Core as H (Result(..))+import qualified Test.Hspec.Core as H (Result(..), Location(..), LocationAccuracy(..), Item(..), mapSpecItem) import qualified Test.Hspec.Runner as H import qualified Test.Hspec.Formatters as H @@ -28,7 +28,7 @@ spec :: Spec spec = do describe "silent" $ do- let runSpec = fmap fst . capture . H.hspecWith H.defaultConfig {H.configFormatter = H.silent}+ let runSpec = fmap fst . capture . H.hspecWithResult H.defaultConfig {H.configFormatter = Just H.silent} it "produces no output" $ do runSpec testSpec `shouldReturn` "" @@ -36,7 +36,7 @@ failed_examplesSpec H.failed_examples describe "progress" $ do- let runSpec = captureLines . H.hspecWith H.defaultConfig {H.configFormatter = H.progress}+ let runSpec = captureLines . H.hspecWithResult H.defaultConfig {H.configFormatter = Just H.progress} it "produces '..F...FF.F' style output" $ do r <- runSpec testSpec@@ -46,7 +46,7 @@ failed_examplesSpec H.progress describe "specdoc" $ do- let runSpec = captureLines . H.hspecWith H.defaultConfig {H.configFormatter = H.specdoc}+ let runSpec = captureLines . H.hspecWithResult H.defaultConfig {H.configFormatter = Just H.specdoc} it "displays a header for each thing being described" $ do _:x:_ <- runSpec testSpec@@ -71,15 +71,12 @@ , "List as a Monoid" , " mappend" , " - is associative"- , "" , " mempty" , " - is a left identity" , " - is a right identity"- , "" , "Maybe as a Monoid" , " mappend" , " - is associative"- , "" , " mempty" , " - is a left identity" , " - is a right identity"@@ -88,50 +85,6 @@ , "6 examples, 0 failures" ] - it "prints an empty line before each group" $ do- r <- runSpec $ do- H.describe "foo" $ do- H.it "example 1" True- H.it "example 2" True- H.describe "bar" $ do- H.it "example 3" True- H.it "example 4" True- normalizeSummary r `shouldBe` [- ""- , "foo"- , " - example 1"- , " - example 2"- , ""- , " bar"- , " - example 3"- , " - example 4"- , ""- , "Finished in 0.0000 seconds"- , "4 examples, 0 failures"- ]-- it "prints an empty line after each group" $ do- r <- runSpec $ do- H.describe "foo" $ do- H.describe "bar" $ do- H.it "example 1" True- H.it "example 2" True- H.it "example 3" True- H.it "example 4" True- normalizeSummary r `shouldBe` [- ""- , "foo"- , " bar"- , " - example 1"- , " - example 2"- , ""- , " - example 3"- , " - example 4"- , ""- , "Finished in 0.0000 seconds"- , "4 examples, 0 failures"- ]- it "outputs an empty line at the beginning (even for non-nested specs)" $ do r <- runSpec $ do H.it "example 1" True@@ -154,12 +107,33 @@ r <- runSpec testSpec r `shouldSatisfy` any (== " # PENDING: pending message") + context "with an empty group" $ do+ it "omits that group from the report" $ do+ r <- runSpec $ do+ H.describe "foo" $ do+ H.it "example 1" True+ H.describe "bar" $ do+ return ()+ H.describe "baz" $ do+ H.it "example 2" True++ normalizeSummary r `shouldBe` [+ ""+ , "foo"+ , " - example 1"+ , "baz"+ , " - example 2"+ , ""+ , "Finished in 0.0000 seconds"+ , "2 examples, 0 failures"+ ]+ context "same as failed_examples" $ do failed_examplesSpec H.progress failed_examplesSpec :: H.Formatter -> Spec failed_examplesSpec formatter = do- let runSpec = captureLines . H.hspecWith H.defaultConfig {H.configFormatter = formatter}+ let runSpec = captureLines . H.hspecWithResult H.defaultConfig {H.configFormatter = Just formatter} it "summarizes the time it takes to finish" $ do r <- runSpec (return ())@@ -185,6 +159,40 @@ H.it "baz" False r `shouldSatisfy` any (== "1) foo.bar baz") ++ context "when a failed example has a source location" $ do+ let bestEffortExplanation = "Source locations marked with \"best-effort\" are calculated heuristically and may be incorrect."++ context "when source location is exact" $ do+ it "includes that source locations" $ do+ let loc = H.Location "test/FooSpec.hs" 23 0 H.ExactLocation+ addLoc e = e {H.itemLocation = Just loc}+ r <- runSpec $ H.mapSpecItem addLoc $ do+ H.it "foo" False+ r `shouldSatisfy` any (== "# test/FooSpec.hs:23")++ it "does not include 'best-effort' explanation" $ do+ let loc = H.Location "test/FooSpec.hs" 23 0 H.ExactLocation+ addLoc e = e {H.itemLocation = Just loc}+ r <- runSpec $ H.mapSpecItem addLoc $ do+ H.it "foo" False+ r `shouldSatisfy` all (/= bestEffortExplanation)++ context "when source location is best-effort" $ do+ it "marks that source location as 'best-effort'" $ do+ let loc = H.Location "test/FooSpec.hs" 23 0 H.BestEffort+ addLoc e = e {H.itemLocation = Just loc}+ r <- runSpec $ H.mapSpecItem addLoc $ do+ H.it "foo" False+ r `shouldSatisfy` any (== "# test/FooSpec.hs:23 (best-effort)")++ it "includes 'best-effort' explanation" $ do+ let loc = H.Location "test/FooSpec.hs" 23 0 H.BestEffort+ addLoc e = e {H.itemLocation = Just loc}+ r <- runSpec $ H.mapSpecItem addLoc $ do+ H.it "foo" False+ r `shouldSatisfy` any (== bestEffortExplanation)+ it "summarizes the number of examples and failures" $ do r <- runSpec testSpec r `shouldSatisfy` any (== "6 examples, 4 failures, 1 pending")@@ -193,22 +201,22 @@ -- colorized output, hence the following tests do not work on Windows. #ifndef mingw32_HOST_OS it "shows summary in green if there are no failures" $ do- r <- captureLines $ H.hspecWith H.defaultConfig {H.configColorMode = H.ColorAlways} $ do+ r <- captureLines $ H.hspecWithResult H.defaultConfig {H.configColorMode = H.ColorAlways} $ do H.it "foobar" True r `shouldSatisfy` any (== (green ++ "1 example, 0 failures" ++ reset)) it "shows summary in yellow if there are pending examples" $ do- r <- captureLines $ H.hspecWith H.defaultConfig {H.configColorMode = H.ColorAlways} $ do+ r <- captureLines $ H.hspecWithResult H.defaultConfig {H.configColorMode = H.ColorAlways} $ do H.it "foobar" H.pending r `shouldSatisfy` any (== (yellow ++ "1 example, 0 failures, 1 pending" ++ reset)) it "shows summary in red if there are failures" $ do- r <- captureLines $ H.hspecWith H.defaultConfig {H.configColorMode = H.ColorAlways} $ do+ r <- captureLines $ H.hspecWithResult H.defaultConfig {H.configColorMode = H.ColorAlways} $ do H.it "foobar" False r `shouldSatisfy` any (== (red ++ "1 example, 1 failure" ++ reset)) it "shows summary in red if there are both failures and pending examples" $ do- r <- captureLines $ H.hspecWith H.defaultConfig {H.configColorMode = H.ColorAlways} $ do+ r <- captureLines $ H.hspecWithResult H.defaultConfig {H.configColorMode = H.ColorAlways} $ do H.it "foo" False H.it "bar" H.pending r `shouldSatisfy` any (== (red ++ "2 examples, 1 failure, 1 pending" ++ reset))
test/Test/Hspec/HUnitSpec.hs view
@@ -1,7 +1,9 @@+{-# OPTIONS_GHC -fno-warn-orphans #-} module Test.Hspec.HUnitSpec (main, spec) where import Helper hiding (example) +import Test.Hspec.Core (Item(..)) import Test.Hspec.Runner.Tree import Test.Hspec.HUnit import Test.HUnit@@ -9,32 +11,35 @@ main :: IO () main = hspec spec -shouldYield :: Test -> [Tree ()] -> Expectation-a `shouldYield` b = map (() <$) <$> toTree (fromHUnitTest a) `shouldReturn` b+shouldYield :: Test -> [Tree String] -> Expectation+a `shouldYield` b = map (Blind . fmap itemRequirement) <$> toTree (fromHUnitTest a) `shouldReturn` map Blind b +instance Eq a => Eq (Tree a) where+ x == y = case (x, y) of+ (Node s1 xs, Node s2 ys) -> s1 == s2 && xs == ys+ (Leaf x1, Leaf x2) -> x1 == x2+ _ -> False+ spec :: Spec spec = do describe "fromHUnitTest" $ do let e = TestCase $ pure () it "works for a TestCase" $ do- e `shouldYield` [example "<unlabeled>"]+ e `shouldYield` [Leaf "<unlabeled>"] it "works for a labeled TestCase" $ do TestLabel "foo" e- `shouldYield` [example "foo"]+ `shouldYield` [Leaf "foo"] it "works for a TestCase with nested labels" $ do (TestLabel "foo" . TestLabel "bar") e- `shouldYield` [Node "foo" [example "bar"]]+ `shouldYield` [Node "foo" [Leaf "bar"]] it "works for a flat TestList" $ do TestList [e, e, e]- `shouldYield` [example "<unlabeled>", example "<unlabeled>", example "<unlabeled>"]+ `shouldYield` [Leaf "<unlabeled>", Leaf "<unlabeled>", Leaf "<unlabeled>"] it "works for a nested TestList" $ do (TestLabel "foo" . TestLabel "bar" . TestList) [TestLabel "one" e, TestLabel "two" e, TestLabel "three" e]- `shouldYield` [Node "foo" [Node "bar" [example "one", example "two", example "three"]]]- where- example :: String -> Tree ()- example r = Leaf r ()+ `shouldYield` [Node "foo" [Node "bar" [Leaf "one", Leaf "two", Leaf "three"]]]
test/Test/Hspec/OptionsSpec.hs view
@@ -17,22 +17,22 @@ spec = do describe "parseOptions" $ do - let parseOptions = Options.parseOptions defaultOptions "my-spec"+ let parseOptions = Options.parseOptions defaultConfig "my-spec" - it "sets optionsColorMode to ColorAuto" $ do- optionsColorMode <$> parseOptions [] `shouldBe` Right ColorAuto+ it "sets configColorMode to ColorAuto" $ do+ configColorMode <$> parseOptions [] `shouldBe` Right ColorAuto context "with --no-color" $ do- it "sets optionsColorMode to ColorNever" $ do- optionsColorMode <$> parseOptions ["--no-color"] `shouldBe` Right ColorNever+ it "sets configColorMode to ColorNever" $ do+ configColorMode <$> parseOptions ["--no-color"] `shouldBe` Right ColorNever context "with --color" $ do- it "sets optionsColorMode to ColorAlways" $ do- optionsColorMode <$> parseOptions ["--color"] `shouldBe` Right ColorAlways+ it "sets configColorMode to ColorAlways" $ do+ configColorMode <$> parseOptions ["--color"] `shouldBe` Right ColorAlways context "with --out" $ do- it "sets optionsOutputFile" $ do- optionsOutputFile <$> parseOptions ["--out", "foo"] `shouldBe` Right (Just "foo")+ it "sets configOutputFile" $ do+ either (const Nothing) Just . configOutputFile <$> parseOptions ["--out", "foo"] `shouldBe` Right (Just "foo") context "with --qc-max-success" $ do context "when given an invalid argument" $ do@@ -41,4 +41,4 @@ context "with --depth" $ do it "sets depth parameter for SmallCheck" $ do- optionsDepth <$> parseOptions ["--depth", "23"] `shouldBe` Right (Just 23)+ configSmallCheckDepth <$> parseOptions ["--depth", "23"] `shouldBe` Right 23
test/Test/Hspec/RunnerSpec.hs view
@@ -183,8 +183,9 @@ r `shouldSatisfy` any ((78 <=) . length) context "with --dry-run" $ do+ let withDryRun = captureLines . withArgs ["--dry-run"] . H.hspec it "produces a report" $ do- r <- captureLines . withArgs ["--dry-run"] . H.hspec $ do+ r <- withDryRun $ do H.it "foo" True H.it "bar" True normalizeSummary r `shouldBe` [@@ -198,11 +199,18 @@ it "does not verify anything" $ do e <- newMock- _ <- captureLines . withArgs ["--dry-run"] . H.hspec $ do+ _ <- withDryRun $ do H.it "foo" (mockAction e) H.it "bar" False mockCounter e `shouldReturn` 0 + it "ignores afterAll-hooks" $ do+ ref <- newIORef False+ _ <- withDryRun $ do+ H.afterAll (writeIORef ref True) $ do+ H.it "bar" True+ readIORef ref `shouldReturn` False+ context "with --fail-fast" $ do it "stops after first failure" $ do r <- captureLines . ignoreExitCode . withArgs ["--fail-fast", "--seed", "23"] . H.hspec $ do@@ -362,7 +370,7 @@ it "treats uncaught exceptions as failure" $ do silence . H.hspecResult $ do- H.it "foobar" (E.throwIO (E.ErrorCall "foobar") >> pure ())+ H.it "foobar" throwException `shouldReturn` H.Summary 1 1 it "uses the specdoc formatter by default" $ do@@ -372,7 +380,7 @@ r `shouldBe` "Foo.Bar" it "can use a custom formatter" $ do- r <- capture_ . H.hspecWith H.defaultConfig {H.configFormatter = H.silent} $ do+ r <- capture_ . H.hspecWithResult H.defaultConfig {H.configFormatter = Just H.silent} $ do H.describe "Foo.Bar" $ do H.it "some example" True r `shouldBe` ""
test/Test/Hspec/UtilSpec.hs view
@@ -45,8 +45,8 @@ e `shouldBe` 23 it "returns Left on exception" $ do- Left e <- safeTry (E.throwIO E.DivideByZero :: IO Int)- show e `shouldBe` "divide by zero"+ Left e <- safeTry throwException+ show e `shouldBe` "foobar" it "evaluates result to weak head normal form" $ do Left e <- safeTry (return undefined)
test/Test/HspecSpec.hs view
@@ -44,11 +44,11 @@ length r `shouldBe` 3 it "can be nested" $ do- [Node foo [Node bar [Leaf baz _]]] <- toTree $ do+ [Node foo [Node bar [Leaf _]]] <- toTree $ do H.describe "foo" $ do H.describe "bar" $ do H.it "baz" True- (foo, bar, baz) `shouldBe` ("foo", "bar", "baz")+ (foo, bar) `shouldBe` ("foo", "bar") context "when no description is given" $ do it "uses a default description" $ do@@ -57,17 +57,17 @@ describe "it" $ do it "takes a description of a desired behavior" $ do- [Leaf requirement _] <- toTree (H.it "whatever" True)- requirement `shouldBe` "whatever"+ [Leaf item] <- toTree (H.it "whatever" True)+ itemRequirement item `shouldBe` "whatever" it "takes an example of that behavior" $ do- [Leaf _ item] <- toTree (H.it "whatever" True)+ [Leaf item] <- toTree (H.it "whatever" True) itemExample item defaultParams id noOpProgressCallback `shouldReturn` Success context "when no description is given" $ do it "uses a default description" $ do- [Leaf requirement _] <- toTree (H.it "" True)- requirement `shouldBe` "(unspecified behavior)"+ [Leaf item] <- toTree (H.it "" True)+ itemRequirement item `shouldBe` "(unspecified behavior)" describe "example" $ do it "fixes the type of an expectation" $ do@@ -78,11 +78,11 @@ describe "parallel" $ do it "marks examples for parallel execution" $ do- [Leaf _ item] <- toTree . H.parallel $ H.it "whatever" True+ [Leaf item] <- toTree . H.parallel $ H.it "whatever" True itemIsParallelizable item `shouldBe` True it "is applied recursively" $ do- [Node _ [Node _ [Leaf _ item]]] <- toTree . H.parallel $ do+ [Node _ [Node _ [Leaf item]]] <- toTree . H.parallel $ do H.describe "foo" $ do H.describe "bar" $ do H.it "baz" True@@ -113,7 +113,7 @@ describe "beforeAll" $ do it "runs an action before the first spec item" $ do- ref <- newIORef ([] :: [String])+ ref <- newIORef [] let append n = modifyIORef ref (++ return n) silence $ H.hspec $ H.beforeAll (append "beforeAll") $ do H.it "foo" $ do@@ -126,6 +126,14 @@ , "bar" ] + context "when used with an empty list of examples" $ do+ it "does not run specified action" $ do+ ref <- newIORef []+ let append n = modifyIORef ref (++ return n)+ silence $ H.hspec $ H.beforeAll (append "beforeAll") $ do+ return ()+ readIORef ref `shouldReturn` []+ describe "after" $ do it "runs an action after each spec item" $ do mock <- newMock@@ -142,6 +150,36 @@ H.it "foo" $ do ioError $ userError "foo" :: IO () mockCounter mock `shouldReturn` 1++ describe "afterAll" $ do+ it "runs an action after the last spec item" $ do+ ref <- newIORef []+ let append n = modifyIORef ref (++ return n)+ silence $ H.hspec $ H.afterAll (append "afterAll") $ do+ H.it "foo" $ do+ append "foo"+ H.it "bar" $ do+ append "bar"+ readIORef ref `shouldReturn` [+ "foo"+ , "bar"+ , "afterAll"+ ]++ context "when used with an empty list of examples" $ do+ it "does not run specified action" $ do+ ref <- newIORef []+ let append n = modifyIORef ref (++ return n)+ silence $ H.hspec $ H.afterAll (append "afterAll") $ do+ return ()+ readIORef ref `shouldReturn` []++ context "when action throws an exception" $ do+ it "reports a failure" $ do+ r <- runSpec $ do+ H.afterAll throwException $ do+ H.it "foo" True+ r `shouldSatisfy` any (== "- afterAll-hook FAILED [1]") describe "around" $ do it "wraps each spec item with an action" $ do