packages feed

chuchu 0.3 → 0.4.1

raw patch · 12 files changed

+321/−183 lines, 12 filesdep +lifted-basedep +monad-controldep ~unixPVP ok

version bump matches the API change (PVP)

Dependencies added: lifted-base, monad-control

Dependency ranges changed: unix

API changes (from Hackage documentation)

- Test.Chuchu: chuchuMain :: (MonadIO m, Applicative m) => Chuchu m -> (m () -> IO ()) -> IO ()
+ Test.Chuchu: chuchuMain :: (MonadBaseControl IO m, MonadIO m, Applicative m) => Chuchu m -> (m () -> IO ()) -> IO ()

Files

chuchu.cabal view
@@ -1,5 +1,5 @@ name: chuchu-version: 0.3+version: 0.4.1 cabal-version: >= 1.8 build-type: Simple license: OtherLicense@@ -22,7 +22,10 @@   tests/data/calculator.feature,   tests/data/prefix.feature,   tests/data/multiple_scenarios.feature,-  tests/data/background_and_multiple_scenarios.feature+  tests/data/background_and_multiple_scenarios.feature,+  tests/data/multiple_features1.feature,+  tests/data/multiple_features2.feature,+  tests/data/should_fail.feature  source-repository head   type:     git@@ -36,16 +39,25 @@     Test.Chuchu.Parser   other-modules:     Test.Chuchu.Parsec,-    Test.Chuchu.Email+    Test.Chuchu.Email,+    Test.Chuchu.OutputPrinter   build-depends:-    base >= 4 && < 5,-    text >= 0.11 && < 0.12,-    transformers >= 0.3 && < 0.4,-    parsec >= 3.1 && < 3.2,-    cmdargs >= 0.9 && < 0.10,-    ansi-wl-pprint == 0.6.*,-    abacate >= 0.0 && < 0.1-  extensions: DeriveDataTypeable, GADTs, GeneralizedNewtypeDeriving, OverloadedStrings+    base               >= 4    && < 5,+    text               >= 0.11 && < 0.12,+    transformers       >= 0.3  && < 0.4,+    parsec             >= 3.1  && < 3.2,+    cmdargs            >= 0.9  && < 0.10,+    ansi-wl-pprint     == 0.6.*,+    abacate            >= 0.0  && < 0.1,+    monad-control      >= 0.3  && < 0.4,+    lifted-base        >= 0.2  && < 0.3+  extensions:+    DeriveDataTypeable+    FlexibleContexts+    GADTs+    GeneralizedNewtypeDeriving+    OverloadedStrings+    ScopedTypeVariables   ghc-options: -Wall  Test-Suite environment@@ -54,7 +66,7 @@   hs-source-dirs: tests   build-depends:     base >= 4 && < 5,-    unix >= 2.5 && < 2.6,+    unix >= 2.5,     text,     chuchu,     HUnit >= 1.2 && < 1.3@@ -100,6 +112,30 @@ Test-Suite background_and_multiple_scenarios   type: exitcode-stdio-1.0   main-is: background_and_multiple_scenarios.hs+  hs-source-dirs: tests+  build-depends:+    base >= 4 && < 5,+    transformers >= 0.3 && < 0.4,+    chuchu,+    HUnit >= 1.2 && < 1.3+  extensions: OverloadedStrings+  ghc-options: -Wall++Test-Suite multiple_features+  type: exitcode-stdio-1.0+  main-is: multiple_features.hs+  hs-source-dirs: tests+  build-depends:+    base >= 4 && < 5,+    transformers >= 0.3 && < 0.4,+    chuchu,+    HUnit >= 1.2 && < 1.3+  extensions: OverloadedStrings+  ghc-options: -Wall++Test-Suite should_fail+  type: exitcode-stdio-1.0+  main-is: should_fail.hs   hs-source-dirs: tests   build-depends:     base >= 4 && < 5,
src/Test/Chuchu.hs view
@@ -77,56 +77,49 @@   (chuchuMain, module Test.Chuchu.Types, module Test.Chuchu.Parser)   where --- base-import Control.Applicative-import Control.Monad-import System.Environment-import System.Exit-import System.IO-import qualified Data.IORef as I---- text-import qualified Data.Text as T---- transformers-import Control.Monad.IO.Class-import Control.Monad.Trans.Class-import Control.Monad.Trans.Reader---- parsec-import Text.Parsec-import Text.Parsec.Text---- cmdargs-import System.Console.CmdArgs---- ansi-wl-pprint-import qualified Text.PrettyPrint.ANSI.Leijen as D---- abacate+import Control.Applicative ((<$>), Applicative((<*>)))+import Control.Monad (unless)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Control (MonadBaseControl)+import Control.Monad.Trans.Reader (ReaderT(..), ask)+import Data.Either (partitionEithers) import Language.Abacate hiding (StepKeyword (..))+import System.Console.CmdArgs+import System.Environment (getProgName)+import System.Exit (exitFailure, exitWith, ExitCode(ExitFailure))+import qualified Control.Exception.Lifted as E+import qualified Data.IORef as I+import qualified Text.Parsec as P --- chuchu import Test.Chuchu.Types import Test.Chuchu.Parser+import Test.Chuchu.OutputPrinter   ----------------------------------------------------------------------  --- | The main function for the test file.  It expects the @.feature@ file as the--- first parameter on the command line.  If you want to use it inside a library,--- consider using 'withArgs'.-chuchuMain :: (MonadIO m, Applicative m) => Chuchu m -> (m () -> IO ()) -> IO ()-chuchuMain cc runMIO-  = do-    path <- getPath-    parsed <- parseFile path-    case parsed of-      Right abacate -> do-        ret <- processAbacate cc runMIO abacate-        unless ret exitFailure-      Left e -> error $ "Could not parse " ++ path ++ ": " ++ show e+-- | The main function for the test file. It expects one or more+-- @.feature@ file as parameters on the command line. If you want to+-- use it inside a library, consider using 'withArgs'.+chuchuMain :: (MonadBaseControl IO m, MonadIO m, Applicative m) =>+              Chuchu m -> (m () -> IO ()) -> IO ()+chuchuMain stepDefinitions runMIO = do+  listOfPaths <- getPaths+  parsedFiles <- mapM parseFile listOfPaths+  let result = partitionEithers parsedFiles+  case result of+    -- no error in parsing, execute all files+    ([], filesToExecute) -> do+      rets <- concat <$> mapM (processAbacate stepDefinitions runMIO) filesToExecute+      let n = length $ filter not rets+      unless (n == 0) $ exitWith (ExitFailure (min 255 n)) -- the size of a Unix error code is 1 byte+    -- there were errors, print them and execute nothing+    (filesWithError, _)  -> do+      warn "Could not parse the following files: "+      mapM_ (warn . flip (++) "\n" . show) filesWithError+      exitFailure   ----------------------------------------------------------------------@@ -153,23 +146,19 @@  -- | Monad used when executing a feature's scenario.  'ReaderT' -- is used to carry along the step parser.-type Execution m a = ReaderT (Parser (m ())) m a----- | Print a 'D.Doc' describing what we're currently processing.-putDoc :: (MonadIO m, Applicative m) => D.Doc -> m ()-putDoc = liftIO . D.putDoc . (D.<> D.linebreak)+type Execution m a = ReaderT (ParseStep m) m a  --- | Same as 'D.text' but using 'T.Text'.-t2d :: T.Text -> D.Doc-t2d = D.text . T.unpack+-- | A function that parses a step and, if successful, returns+-- the corresponding action to be executed.+type ParseStep m = Step -> Either P.ParseError (m ())   -- | Run the 'Execution' monad. runExecution :: (MonadIO m, Applicative m) =>                 Chuchu m -> (m () -> IO ()) -> Execution m () -> IO ()-runExecution cc runMIO act = runMIO $ runReaderT act $ runChuchu cc+runExecution stepDefinitions runMIO act = runMIO $ runReaderT act parseStep+  where parseStep = P.parse (runChuchu stepDefinitions) "Step definitions" . stBody   ----------------------------------------------------------------------@@ -178,137 +167,99 @@ -- | Process a whole Abacate file, that is, a whole feature. -- Runs each background+scenario combination on a different -- instance of the 'Execution' monad.-processAbacate :: (MonadIO m, Applicative m) =>+processAbacate :: (MonadBaseControl IO m, MonadIO m, Applicative m) =>                   Chuchu m                -> (m () -> IO ())                -> Abacate-               -> IO Bool-processAbacate cc runMIO feature = do+               -> IO [Bool]+processAbacate stepDefinitions runMIO feature = do   -- Print feature description.   putDoc $ describeAbacate feature    -- Execute features.   let plans = createExecutionPlans feature-  retVar <- liftIO $ I.newIORef True-  let checkRet ret = unless ret $ liftIO $ I.writeIORef retVar False-  mapM_ (runExecution cc runMIO . (>>= checkRet) . processExecutionPlan) plans-  liftIO $ I.readIORef retVar+  retVar <- liftIO $ I.newIORef []+  let addRet ret = liftIO $ I.modifyIORef retVar (ret:)+  mapM_ (runExecution stepDefinitions runMIO . (>>= addRet) . processExecutionPlan) plans+  reverse <$> liftIO (I.readIORef retVar)   -- | Process a single execution plan, a combination of -- background+scenario, inside the 'Execution' monad.-processExecutionPlan :: (MonadIO m, Applicative m) => ExecutionPlan -> Execution m Bool+processExecutionPlan :: (MonadBaseControl IO m, MonadIO m, Applicative m) =>+                        ExecutionPlan -> Execution m Bool processExecutionPlan (ExecutionPlan mbackground scenario) = do-  putDoc D.empty -- empty line+  liftIO $ putStrLn "" -- empty line (TODO: move into OutputPrinter somehow)   (&&) <$> maybe (return True) (processBasicScenario BackgroundKind) mbackground        <*> processFeatureElement scenario  --- | Creates a pretty description of the feature.-describeAbacate :: Abacate -> D.Doc-describeAbacate feature =-  D.vsep $-  describeTags (fTags feature) ++ [D.white $ t2d $ fHeader feature]----- | Creates a vertical list of tags.-describeTags :: Tags -> [D.Doc]-describeTags = map (D.dullcyan . ("@" D.<>) . t2d)-- ----------------------------------------------------------------------  -processFeatureElement :: (MonadIO m, Applicative m) => FeatureElement -> Execution m Bool+processFeatureElement :: (MonadBaseControl IO m, MonadIO m, Applicative m) =>+                         FeatureElement -> Execution m Bool processFeatureElement (FESO _)-  = liftIO (hPutStrLn stderr "Scenario Outlines are not supported yet.")-    >> return False+  = warn "Scenario Outlines are not supported yet." >> return False processFeatureElement (FES sc) =   processBasicScenario (ScenarioKind $ scTags sc) $ scBasicScenario sc  -data BasicScenarioKind = BackgroundKind | ScenarioKind Tags---processBasicScenario :: (MonadIO m, Applicative m) => BasicScenarioKind -> BasicScenario -> Execution m Bool+processBasicScenario :: (MonadBaseControl IO m, MonadIO m, Applicative m) =>+                        BasicScenarioKind -> BasicScenario -> Execution m Bool processBasicScenario kind scenario = do   putDoc $ describeBasicScenario kind scenario   processSteps (bsSteps scenario)  --- | Creates a pretty description of the basic scenario's header.-describeBasicScenario :: BasicScenarioKind -> BasicScenario -> D.Doc-describeBasicScenario kind scenario =-  D.indent 2 $-  prettyTags kind $-  D.bold ((describeBasicScenarioKind kind) D.<+> t2d (bsName scenario))-    where describeBasicScenarioKind BackgroundKind   = "Background:"-          describeBasicScenarioKind (ScenarioKind _) = "Scenario:"--          prettyTags BackgroundKind      = id-          prettyTags (ScenarioKind tags) = D.vsep . (describeTags tags ++) . (:[])-- ----------------------------------------------------------------------  -processSteps :: (MonadIO m, Applicative m) => Steps -> Execution m Bool-processSteps steps-  = do-    codes <- mapM processStep steps-    return $ and codes---processStep :: (MonadIO m, Applicative m) => Step -> Execution m Bool-processStep step-  = do-    cc <- ask-    case parse cc "processStep" $ stBody step of-      Left e-        -> do-          putDoc $ describeStep UnknownStep step-          liftIO-            $ hPutStrLn stderr-            $ "The step "-              ++ show (stBody step)-              ++ " doesn't match any step definitions I know."-              ++ show e-          return False-      Right m -> do-        -- TODO: Catch failures and treat them nicely.-        putDoc $ describeStep SuccessfulStep step-        lift m-        return True+processSteps :: (MonadBaseControl IO m, MonadIO m, Applicative m) => Steps -> Execution m Bool+processSteps steps = mapShortCircuitM processStep steps  -data StepResult = SuccessfulStep | UnknownStep+mapShortCircuitM :: (Monad m) => (a -> m Bool) -> [a] -> m Bool+mapShortCircuitM _ []     = return True+mapShortCircuitM f (x:xs) = do+  ret <- f x+  if ret then mapShortCircuitM f xs+         else return False  --- | Pretty-prints a step that has already finished executing.-describeStep :: StepResult -> Step -> D.Doc-describeStep result step =-  D.indent 4 $-  color result (D.text (show $ stStepKeyword step) D.<+> t2d (stBody step))-    where-      color SuccessfulStep = D.green-      color UnknownStep    = D.yellow+-- | Executes the parser of each step and prints the result on the+-- screen.+processStep :: (MonadBaseControl IO m, MonadIO m, Applicative m) => Step -> Execution m Bool+processStep step = do+  parseStep <- ask+  case parseStep step of+    Left e -> do+      putDoc $ describeStep UnknownStep step+      liftIO $ warn $ concat [ "The step "+                             , show (stBody step)+                             , " doesn't match any step definitions I know."+                             , show e ]+      return False+    Right m -> do+      r <- E.catches (lift m >> return SuccessfulStep)+             [ E.Handler $ \(e :: E.AsyncException) -> E.throw (e :: E.AsyncException)+             , E.Handler $ \(_ :: E.SomeException)  -> return FailedStep ]+      putDoc (describeStep r step)+      return (r == SuccessfulStep)   ----------------------------------------------------------------------   data Options-  = Options {file_ :: FilePath}+  = Options {file_ :: [FilePath]}     deriving (Eq, Show, Typeable, Data)  -getPath :: IO FilePath-getPath-  = do-    progName <- getProgName-    file_-      <$> cmdArgs-        (Options (def &= typ "PATH" &= argPos 0)-          &= program progName-          &= details-            ["Run test scenarios specified on the abacate file at PATH."])+-- Gets the feature files as arguments from the command-line.+getPaths :: IO [FilePath]+getPaths = do+  progName <- getProgName+  file_ <$> cmdArgs (Options (def &= typ "PATH" &= args)+                     &= program progName+                     &= details ["Run one or more abacate files."])
src/Test/Chuchu/Email.hs view
@@ -13,12 +13,10 @@ -- <http://porg.es/blog/email-address-validation-simpler-faster-more-correct>. module Test.Chuchu.Email (addrSpecSimple) where --- base-import Control.Applicative hiding ((<|>), many, optional)+import Control.Applicative ((<$>), (<|>))+import Text.Parsec (oneOf, many1, string, alphaNum)+import Text.Parsec.Text (Parser) --- parsec-import Text.Parsec-import Text.Parsec.Text  -- | Parses a simplified e-mail address and return everything that was parsed as -- a simple 'String'.
+ src/Test/Chuchu/OutputPrinter.hs view
@@ -0,0 +1,74 @@+module Test.Chuchu.OutputPrinter+  ( putDoc+  , warn+  , describeAbacate+  , describeBasicScenario+  , BasicScenarioKind(..)+  , describeStep+  , StepResult(..)+  ) where++import Control.Monad.IO.Class (MonadIO(liftIO))+import Language.Abacate hiding (StepKeyword (..))+import System.IO (hPutStrLn, stderr)+import qualified Data.Text as T+import qualified Text.PrettyPrint.ANSI.Leijen as D+++-- | Print a 'D.Doc' describing what we're currently processing.+putDoc :: MonadIO m => D.Doc -> m ()+putDoc = liftIO . D.putDoc . (D.<> D.linebreak)+++-- | Print a warning message.+warn :: MonadIO m => String -> m ()+warn = liftIO . hPutStrLn stderr+++----------------------------------------------------------------------+++-- | Same as 'D.text' but using 'T.Text'.+t2d :: T.Text -> D.Doc+t2d = D.text . T.unpack+++-- | Creates a pretty description of the feature.+describeAbacate :: Abacate -> D.Doc+describeAbacate feature =+  D.vsep $+  describeTags (fTags feature) ++ [D.white $ t2d $ fHeader feature]+++-- | Creates a vertical list of tags.+describeTags :: Tags -> [D.Doc]+describeTags = map (D.dullcyan . ("@" D.<>) . t2d)+++-- | Creates a pretty description of the basic scenario's header.+describeBasicScenario :: BasicScenarioKind -> BasicScenario -> D.Doc+describeBasicScenario kind scenario =+  D.indent 2 $+  prettyTags kind $+  D.bold ((describeBasicScenarioKind kind) D.<+> t2d (bsName scenario))+    where describeBasicScenarioKind BackgroundKind   = "Background:"+          describeBasicScenarioKind (ScenarioKind _) = "Scenario:"++          prettyTags BackgroundKind      = id+          prettyTags (ScenarioKind tags) = D.vsep . (describeTags tags ++) . (:[])+++data BasicScenarioKind = BackgroundKind | ScenarioKind Tags+++-- | Pretty-prints a step that has already finished executing.+describeStep :: StepResult -> Step -> D.Doc+describeStep result step =+  D.indent 4 $+  color result (D.text (show $ stStepKeyword step) D.<+> t2d (stBody step))+    where+      color SuccessfulStep = D.green+      color FailedStep     = D.red+      color UnknownStep    = D.yellow++data StepResult = SuccessfulStep | FailedStep | UnknownStep deriving (Eq)
src/Test/Chuchu/Parsec.hs view
@@ -37,12 +37,12 @@  module Test.Chuchu.Parsec (stringLiteral, natFloat, int) where --- base-import Data.Char---- parsec+import Control.Applicative ((<|>), many)+import Data.Char (digitToInt) import Text.Parsec-import Text.Parsec.Text+       ( (<?>), between, choice, digit, char, hexDigit, many1+       , octDigit, oneOf, option, satisfy, space, string, try, upper )+import Text.Parsec.Text (Parser)  {-# ANN module ("HLint: ignore" :: String) #-} -- | 'lexeme' removed.
src/Test/Chuchu/Parser.hs view
@@ -19,20 +19,19 @@   , try   ) where -import Control.Applicative hiding ((<|>))-import Text.Parsec hiding (try)+import Control.Applicative ((<$>)) import qualified Data.Text as T-import qualified Text.Parsec as Parsec+import qualified Text.Parsec as P  import Test.Chuchu.Types (ChuchuParser(..)) import Test.Chuchu.Email-import qualified Test.Chuchu.Parsec as P+import qualified Test.Chuchu.Parsec as P'   -- | Parses a floating point number, with the same syntax as accepted by -- Haskell. number :: ChuchuParser Double-number = ChuchuParser $ nofToDouble <$> P.natFloat+number = ChuchuParser $ nofToDouble <$> P'.natFloat   nofToDouble :: Either Integer Double -> Double@@ -42,17 +41,17 @@  -- | Parses an integer. int :: ChuchuParser Integer-int = ChuchuParser P.int+int = ChuchuParser P'.int   -- | Parses a quoted string, with the same syntax as accepted by Haskell. text :: ChuchuParser T.Text-text = T.pack <$> ChuchuParser P.stringLiteral+text = T.pack <$> ChuchuParser P'.stringLiteral   -- | Parses anything until the string passed as parameter, and also the string. wildcard :: T.Text -> ChuchuParser T.Text-wildcard = fmap T.pack . ChuchuParser . manyTill anyChar . Parsec.try . string . T.unpack+wildcard = fmap T.pack . ChuchuParser . P.manyTill P.anyChar . P.try . P.string . T.unpack   -- | Parses a simplified e-mail address and return everything that was parsed as@@ -65,4 +64,4 @@  -- | Same as Parsec's 'Parsec.try' but for 'ChuchuParser'. try :: ChuchuParser a -> ChuchuParser a-try (ChuchuParser x) = ChuchuParser (Parsec.try x)+try (ChuchuParser x) = ChuchuParser (P.try x)
src/Test/Chuchu/Types.hs view
@@ -11,24 +11,21 @@   (ChuchuParser (..), Chuchu, ChuchuM (Given, When, Then, And, But), runChuchu)   where --- base-import Control.Applicative hiding ((<|>))+import Control.Applicative (Applicative((<*)), Alternative((<|>)), (<$>)) import Control.Monad (MonadPlus)-import Data.String---- parsec-import Text.Parsec-import Text.Parsec.Text+import Data.String (IsString(..))+import qualified Text.Parsec as P+import qualified Text.Parsec.Text as P (Parser)   -- | @newtype@ for Parsec's 'Parser' used on this library.  The -- main reason for not using 'Parser' directly is to be able to -- define the 'IsString' instance.-newtype ChuchuParser a = ChuchuParser (Parser a)+newtype ChuchuParser a = ChuchuParser (P.Parser a)   deriving (Functor, Applicative, Alternative, Monad, MonadPlus)  instance (a ~ ()) => IsString (ChuchuParser a) where-  fromString s = ChuchuParser (try (string s) >> return ())+  fromString s = ChuchuParser (P.try (P.string s) >> return ())   -- | The most common use case where the return value of the Monad is ignored.@@ -54,8 +51,8 @@ -- | Converts the Monad into a single 'Parser' that executes the specified -- action if the parser is executed correctly.  It includes an 'eof' on the -- parser of each step to avoid it from accepting prefixes of the desired rule.-runChuchu :: ChuchuM m a -> Parser (m ())-runChuchu Nil = unexpected "Unknown step"+runChuchu :: ChuchuM m a -> P.Parser (m ())+runChuchu Nil = P.unexpected "Unknown step" runChuchu (Cons cc1 cc2) = runChuchu cc1 <|> runChuchu cc2 runChuchu (Given p f) = apply p f runChuchu (When p f) = apply p f@@ -63,5 +60,5 @@ runChuchu (And p f) = apply p f runChuchu (But p f) = apply p f -apply :: ChuchuParser a -> (a -> m ()) -> Parser (m ())-apply (ChuchuParser p) f = try $ f <$> p <* eof+apply :: ChuchuParser a -> (a -> m ()) -> P.Parser (m ())+apply (ChuchuParser p) f = P.try $ f <$> p <* P.eof
+ tests/data/multiple_features1.feature view
@@ -0,0 +1,7 @@+Feature: Multiple features 1++  This feature is intended to be used with another one to test if+  Chuchu executes more than one feature file.++  Scenario: Anybody can use the bathroom+    Given that "Bob" has entered the bathroom and locked the door
+ tests/data/multiple_features2.feature view
@@ -0,0 +1,7 @@+Feature: Multiple features 2++  This feature is intended to be used with another one to test if+  Chuchu executes more than one feature file.++  Scenario: Anybody can use the bathroom+    Given that "Alice" has entered the bathroom and locked the door
+ tests/data/should_fail.feature view
@@ -0,0 +1,7 @@+Feature: Chuchu should know how to catch failure++  Scenario: Unknown step definition must stop execution+    Given non-existent step++  Scenario: Step that fails must stop execution+    Given failing step
+ tests/multiple_features.hs view
@@ -0,0 +1,26 @@+import Control.Applicative+import Control.Monad.Trans.State+import System.Environment+import Test.Chuchu+++type Bathroom = StateT Door IO+++data Door = Open | Locked+++defs :: Chuchu Bathroom+defs = do++  Given ("that " *> text <* " has entered the bathroom and locked the door") $ \_ -> do+    door <- get+    case door of+      Open -> put Locked+      Locked -> fail "Door was already locked."+++main :: IO ()+main = withArgs [ "tests/data/multiple_features1.feature"+                , "tests/data/multiple_features2.feature" ] $+       chuchuMain defs (`evalStateT` Open)
+ tests/should_fail.hs view
@@ -0,0 +1,36 @@+import Control.Exception (handle)+import System.Environment (withArgs)+import System.Exit (exitWith, ExitCode(ExitSuccess, ExitFailure))+import Test.Chuchu+++defs :: Chuchu IO+defs = do+  Given "failing step" $ \() -> do+    fail "Error"+++-- | This functions verifies if the behaviour of Chuchu was+-- correct. The correct behaviour is to have 'n' number of failed+-- scenarios (this number comes from the exit code). If no scenarios+-- fail or the number of failed scenarios is different than 'n' then+-- Chuchu failed.+scenariosShouldFail :: Int -> IO () -> IO ()+scenariosShouldFail n = handle exit+    where exit ExitSuccess                  = failWith 255+          exit (ExitFailure n') | n /= n'   = failWith n'+                                | otherwise = putStrLn "Okay, everything failed!"+          failWith n' = do+            putStrLn $ concat+              [ "\n"+              , show n+              , " `scenariosShouldFail` _: failure, exiting with code "+              , show n'+              , "." ]+            exitWith (ExitFailure n')+++main :: IO ()+main = withArgs [ "tests/data/should_fail.feature" ] $+         2 `scenariosShouldFail` chuchuMain defs id+